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
| Endpoint | Limit | What it does |
|---|---|---|
| GET/api/admin/leads | 5/min | The inquiry list, gated by CWRCHECK_ADMIN_TOKEN (set on Render). |
| GET/api/admin/usage | 30/min | Usage summary (counts and outcomes only), admin-token gated. |
| POST/api/billing/checkout | Start 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/claim | Exchange a paid checkout session id for the API key, once. | |
| POST/api/generate | 30/min | Catalog 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/leads | 5/min | Managed-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/lint | 30/min | Dry 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/share | 6/min | Create 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/usage | 30/min | Current 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/validate | 12/min | Validate 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
| Endpoint | Requests / minute |
|---|---|
| /api/lint | 30 |
| /api/generate | 30 |
| /api/validate | 12 |
| /api/share | 6 |
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:
| Status | When |
|---|---|
| 413 | A file exceeds the 10 MB limit, or more than 20 files were sent. |
| 422 | The request body is invalid — malformed JSON, a missing mandatory field, or a catalog the generator cannot represent (the detail names the work). |
| 429 | Rate limit exceeded; comes
with Retry-After and X-RateLimit-*
headers. |
| 5xx | Our 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.
| Field | Type | Description |
|---|---|---|
version | 2.1 | 2.2 | CWR version to generate. Default "2.1". |
client | string | Optional 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_type | NWR | REV | NWR for new registrations, REV for revisions. Default "NWR". |
sender required | object | |
sender_code | string | 2-3 character sender code for the delivery filename. Defaults to the first letters of the sender name. |
receiver | string | 2-3 character receiver code for the delivery filename, e.g. BMI. |
file_sequence | integer | Sequential number of this file within the year (filename only). Default 1. |
works required | work[] |
Work
| Field | Type | Description |
|---|---|---|
title required | string | |
submitter_work_number required | string | Your unique identifier for the work. |
iswc | string | International Standard Work Code, e.g. T1234567890. |
language_code | string | |
distribution_category | POP | SER | JAZ | UNC | Musical work distribution category. Default POP. |
version_type | ORI | MOD | ORI for original works, MOD for modifications. Default ORI. |
recorded | boolean | |
duration | string | HH:MM:SS. Required when distribution_category is SER. |
publishers | publisher[] | |
writers | writer[] | |
alternate_titles | alternate title[] |
Writer
| Field | Type | Description |
|---|---|---|
controlled | boolean | true emits an SWR (writer controlled by submitter), false an OWR. Default true. |
ip_number required | string | |
last_name required | string | |
first_name | string | |
designation | CA | 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_number | string | |
pr_society | string | |
mr_society | string | |
sr_society | string | |
pr_share | share | A percentage between 0 and 100, e.g. 33.34. |
mr_share | share | A percentage between 0 and 100, e.g. 33.34. |
sr_share | share | A percentage between 0 and 100, e.g. 33.34. |
publisher_ip_number | string | ip_number of the publisher representing this writer; emits the PWR link. Must match a publisher on the same work. |
territories | territory[] | Collection territories. Omitted: world, collection equal to ownership. |
Publisher
| Field | Type | Description |
|---|---|---|
controlled | boolean | true emits an SPU (publisher controlled by submitter), false an OPU. Default true. |
sequence | integer | Publisher chain number. Default 1. |
ip_number required | string | |
name | string | |
type | E | SE | AM | PA | ES | Publisher type: E original, SE sub-publisher, AM administrator, PA income participant, ES substituted. Default E. |
ipi_name_number | string | |
pr_society | string | |
mr_society | string | |
sr_society | string | |
pr_share | number | Performing-rights ownership share; publishers may hold at most 50%. |
mr_share | share | A percentage between 0 and 100, e.g. 33.34. |
sr_share | share | A percentage between 0 and 100, e.g. 33.34. |
territories | territory[] | Collection territories. Omitted: world, collection equal to ownership (PR capped at 50%). |
Territory
| Field | Type | Description |
|---|---|---|
tis required | integer | CISAC TIS numeric territory code, e.g. 2136 for the world, 840 for the United States. |
include | boolean | true includes the territory, false excludes it. Default true. |
pr_share | share | A percentage between 0 and 100, e.g. 33.34. |
mr_share | share | A percentage between 0 and 100, e.g. 33.34. |
sr_share | share | A percentage between 0 and 100, e.g. 33.34. |
Alternate title
| Field | Type | Description |
|---|---|---|
title required | string | |
type | AT | 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_code | string |
Questions the reference doesn’t answer: alex@kanetechgroup.com — a human replies within one business day.