CCWR Check

Developer reference

The CWR Check API

Everything the site does is an HTTP call you can make yourself: validate CWR files, check a catalog for free, and generate delivery-ready CWR 2.1/2.2. If /api/lint comes back clean, /api/generate is guaranteed to succeed with the same input. Everything is free while the service is in open beta.

Endpoints

EndpointLimitWhat it does
GET/api/admin/leads5/minThe inquiry list, gated by CWRCHECK_ADMIN_TOKEN (set on Render).
GET/api/admin/usage30/minUsage summary (counts and outcomes only), admin-token gated.
POST/api/billing/checkoutStart a Stripe Checkout for an API key subscription. Body: ``{"email": "..."}``. Returns ``{"url": ...}`` to redirect the buyer to Stripe. 503 until Stripe is configured via env.
POST/api/billing/claimExchange a paid checkout session id for the API key, once.
POST/api/generate30/minCatalog in, fixed-width CWR file out, with a convention-conforming filename in Content-Disposition. A clean /api/lint on the same catalog guarantees this call succeeds, and the output passes /api/validate with zero errors. No API key is needed while billing is disabled (open beta); once enabled, pass X-API-Key. Rate limit: 30/min.
POST/api/leads5/minManaged-service inquiry or API key request from the site. The "website" field is a honeypot: humans never see it, bots fill it. Honeypotted submissions are acknowledged but not stored. UTM attribution captured on the page is stored with the lead, and each lead is mirrored to Formspree for the email notification.
POST/api/lint30/minDry run of a JSON catalog: schema validation with JSON paths plus the generator's own checks. A clean lint guarantees /api/generate succeeds with the same input. Rate limit: 30/min.
POST/api/share6/minCreate a shareable URL for one file's validation report. Opt-in persistence: the file is re-validated here and the server-rendered report -- never client-supplied HTML -- is stored for 30 days under an unguessable id. The uploaded file itself is still discarded.
GET/api/usage30/minCurrent billing-period usage for the caller's API key. Includes a per-client breakdown so administrators can re-bill each catalog's owner for exactly the works generated for them.
POST/api/validate12/minValidate up to 20 uploaded CWR files (10 MB each), statelessly. Returns per-file findings tied to line numbers, a work list, and a self-contained HTML report. Nothing is stored. Rate limit: 12/min.

No authentication, no signup: every endpoint is free to call within the rate limits while the service is in open beta.

Interactive documentation

Try every endpoint from the browser with Swagger UI, read the reference in ReDoc, or import the OpenAPI description into Postman, Insomnia, or a code generator.

Quick start

Check a catalog (start from the example):

curl -X POST https://cwrcheck.com/api/lint \
  -H "Content-Type: application/json" \
  -d @catalog.json

Generate and download the CWR file:

curl -X POST https://cwrcheck.com/api/generate \
  -H "Content-Type: application/json" \
  -d @catalog.json -OJ

Validate an existing CWR file:

curl -X POST https://cwrcheck.com/api/validate \
  -F "files=@CW260001XXX_BMI.V21"

Python:

import requests

catalog = {
    "sender": {"type": "PB", "id": 123456789, "name": "EXAMPLE MUSIC"},
    "receiver": "BMI",
    "works": [{
        "title": "EXAMPLE SONG",
        "submitter_work_number": "EX0001",
        "writers": [{"ip_number": "W1", "last_name": "DOE",
                     "pr_share": 100}],
    }],
}

# Free check first: clean lint guarantees generation succeeds.
lint = requests.post("https://cwrcheck.com/api/lint", json=catalog).json()
assert lint["ok"], lint

r = requests.post("https://cwrcheck.com/api/generate", json=catalog)
r.raise_for_status()
open(lint["filename"], "w").write(r.text)

JavaScript:

const catalog = { /* same shape as the Python example */ };

const lint = await fetch("https://cwrcheck.com/api/lint", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(catalog),
}).then((r) => r.json());
if (!lint.ok) throw new Error(JSON.stringify(lint.schema_errors));

const cwr = await fetch("https://cwrcheck.com/api/generate", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(catalog),
});
const file = await cwr.text(); // fixed-width CWR, filename in
                               // the Content-Disposition header

Rate limits

EndpointRequests / minute
/api/lint30
/api/generate30
/api/validate12
/api/share6

Limits are per client, per minute. Exceeding one returns 429 with Retry-After and X-RateLimit-* headers saying exactly when to retry. CORS is enabled, so the API is callable directly from browser apps. Need more throughput? Ask.

Responses and errors

A successful /api/lint looks like this — ok: true is the guarantee that /api/generate will succeed with the same input:

{
  "ok": true,
  "works": 3,
  "filename": "CW260001KTG_BMI.V21",
  "schema_errors": [],
  "generator_error": null
}

A failing one names every problem with its JSON path (schema_errors) or a plain-English generator message. Every non-2xx response is a JSON envelope {"detail": "…"} with a human-readable explanation:

StatusWhen
413A file exceeds the 10 MB limit, or more than 20 files were sent.
422The request body is invalid — malformed JSON, a missing mandatory field, or a catalog the generator cannot represent (the detail names the work).
429Rate limit exceeded; comes with Retry-After and X-RateLimit-* headers.
5xxOur fault. If it persists, email alex@kanetechgroup.com.

The catalog format

A catalog is one JSON object describing your works. All shares are percentages (50 or 33.34); everything the CWR format needs beyond this — record prefixes, sequence numbers, control totals, territory records — is derived for you. The machine-readable source of truth is catalog-v1.json (JSON Schema 2020-12; point your editor's $schema at it for autocomplete). Version policy: v1 only ever gains fields; breaking changes would ship as catalog-v2.json.

Catalog

A work catalog accepted by cwrcheck.com's CWR generation service (POST /api/generate). Version 1: additive changes only; breaking changes will publish catalog-v2.json.

FieldTypeDescription
version2.1 | 2.2CWR version to generate. Default "2.1".
clientstringOptional label for the client this catalog belongs to (for administrators managing multiple catalogs). Usage reporting is broken down by this label so you can re-bill per client. Not written into the CWR file.
transaction_typeNWR | REVNWR for new registrations, REV for revisions. Default "NWR".
sender requiredobject
sender_codestring2-3 character sender code for the delivery filename. Defaults to the first letters of the sender name.
receiverstring2-3 character receiver code for the delivery filename, e.g. BMI.
file_sequenceintegerSequential number of this file within the year (filename only). Default 1.
works requiredwork[]

Work

FieldTypeDescription
title requiredstring
submitter_work_number requiredstringYour unique identifier for the work.
iswcstringInternational Standard Work Code, e.g. T1234567890.
language_codestring
distribution_categoryPOP | SER | JAZ | UNCMusical work distribution category. Default POP.
version_typeORI | MODORI for original works, MOD for modifications. Default ORI.
recordedboolean
durationstringHH:MM:SS. Required when distribution_category is SER.
publisherspublisher[]
writerswriter[]
alternate_titlesalternate title[]

Writer

FieldTypeDescription
controlledbooleantrue emits an SWR (writer controlled by submitter), false an OWR. Default true.
ip_number requiredstring
last_name requiredstring
first_namestring
designationCA | C | A | AR | AD | SR …Writer role: CA composer/author, C composer, A author, AR arranger, AD adaptor, SR sub-author, SA sub-arranger, TR translator, PA income participant. Default CA.
ipi_name_numberstring
pr_societystring
mr_societystring
sr_societystring
pr_shareshareA percentage between 0 and 100, e.g. 33.34.
mr_shareshareA percentage between 0 and 100, e.g. 33.34.
sr_shareshareA percentage between 0 and 100, e.g. 33.34.
publisher_ip_numberstringip_number of the publisher representing this writer; emits the PWR link. Must match a publisher on the same work.
territoriesterritory[]Collection territories. Omitted: world, collection equal to ownership.

Publisher

FieldTypeDescription
controlledbooleantrue emits an SPU (publisher controlled by submitter), false an OPU. Default true.
sequenceintegerPublisher chain number. Default 1.
ip_number requiredstring
namestring
typeE | SE | AM | PA | ESPublisher type: E original, SE sub-publisher, AM administrator, PA income participant, ES substituted. Default E.
ipi_name_numberstring
pr_societystring
mr_societystring
sr_societystring
pr_sharenumberPerforming-rights ownership share; publishers may hold at most 50%.
mr_shareshareA percentage between 0 and 100, e.g. 33.34.
sr_shareshareA percentage between 0 and 100, e.g. 33.34.
territoriesterritory[]Collection territories. Omitted: world, collection equal to ownership (PR capped at 50%).

Territory

FieldTypeDescription
tis requiredintegerCISAC TIS numeric territory code, e.g. 2136 for the world, 840 for the United States.
includebooleantrue includes the territory, false excludes it. Default true.
pr_shareshareA percentage between 0 and 100, e.g. 33.34.
mr_shareshareA percentage between 0 and 100, e.g. 33.34.
sr_shareshareA percentage between 0 and 100, e.g. 33.34.

Alternate title

FieldTypeDescription
title requiredstring
typeAT | TE | FT | IT | TT | PT …Title type: AT alternative, TE first-line-of-text, FT formal, IT incorrect, OL original-language, AL alternative-in-other-language. Default AT.
language_codestring

Questions the reference doesn’t answer: alex@kanetechgroup.com — a human replies within one business day.