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. ``source`` says which on-site button led to the form (the writer check's summary or side panel, ADR-0033); any other value is dropped. 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. When the catalog builds, the response also carries `writers` (one row per distinct writer, with `paths` into the catalog such as `/works/3/writers/0`) and `writer_advisories` (questions the catalog raises about its writers: one name under several IPI name numbers, one number under several names, a controlled writer with no number). They are advisories: they never change `ok` or block generation. `lang=ja|ko|es|de` localizes their `explain`. Add `writer_check=true` to also look the catalog's writers up on Songlinks, exactly as `/api/validate` does: each row gains `songlinks` with advisory `chips`, and the response carries `writer_check` with `outcome` ok|partial|unavailable and the same `summary` and `audit`. Only valid writer IPI name numbers are sent. Off by default. 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 / ~12,000 registrations each), statelessly. Returns per-file findings tied to line numbers, a work list, and a self-contained HTML report. Nothing is stored. Send **either** `files` (multipart uploads) **or** `text` (one file's contents as a form field, for pasted CWR — reported under the name `pasted.V21`, with the filename-convention warning suppressed because the name is ours, not yours). Supplying both, or neither, returns 422. Each file also carries `songlinks_check`: `null`, or `{writers, listed, url, save_url}` -- the number of writers with a valid IPI name number, how many the links list (at most 50), a songlinks.io/check link that looks them up in public registration data, and a songlinks.io/catalogue/add link that saves them to a free Songlinks catalogue. Nothing is sent to Songlinks by this call; the URLs are only returned. Each file also carries `writers` -- `{distinct, omitted, rows}`, one row per distinct writer with `name`, `ipi`, `ipi_valid`, `controlled`, `works` (in this file), `lines`, `advisories` and `songlinks_url` -- and `writer_advisories`: questions the file raises about its writers (one name under several IPI name numbers, one IPI name number under several names, a controlled writer with no IPI name number). Advisories are not findings: they never change `valid` or the counts. Add `writer_check=true` to also look the file's writers up on Songlinks, server to server: up to 200 valid writer IPI name numbers (never names) in calls of 50, 5 s each. Each row then carries `songlinks` (status, registered names, works, works elsewhere, a person link and advisory `chips`), and the file carries `writer_check` with `outcome` ok|partial|unavailable, a `summary` and an `audit`: the counts the site's Writers summary shows (`total`, `flagged`, `flagged_ipis`, `elsewhere_writers`, `works_elsewhere` counted once per Songlinks person, `spelling`, `split`, `none`, `several`) with `audit_url` (the flagged writers on Songlinks, null when none is flagged) and `save_url`. A failed lookup never fails validation. Off by default. Add `format=csv&table=works|parties|findings|ack|societies` for the same check as a spreadsheet, from either input and never truncated by catalog size. 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
Exports
The same validation, as a spreadsheet. Add
format=csv and table= to
/api/validate and one file comes back as
text/csv with the filename in
Content-Disposition:
curl -X POST \ "https://cwrcheck.com/api/validate?format=csv&table=parties" \ -F "files=@CW260001XXX_BMI.V21" -OJ
Files are UTF-8 with a byte-order mark and
RFC 4180 quoting, so Japanese and Korean titles open correctly in
Excel. Unlike the HTML report, exports are never truncated — the
whole catalog goes in the file. The column contract is versioned:
this is v1, returned on
every export in the X-Export-Contract-Version header.
Columns are only ever appended within a version; removing, renaming,
or reordering one bumps it.
Pass lang=ja|ko|es|de to localize the explanation columns
of the findings table; the CWR spec’s own
message and a society’s
message_text are always verbatim.
works
Every work registration in the file, with its share totals and its finding counts. Row grain: one row per work transaction (NWR, REV, ISW, EXC).
| Column | Meaning |
|---|---|
line_number | Line of the transaction header in your file. |
transaction_type | NWR, REV, ISW or EXC. |
transaction_sequence | Transaction sequence number from the record prefix. |
submitter_work_number | Your own work number for this song. |
work_title | The work title as submitted. |
iswc | ISWC, when the file carries one. |
language_code | Language of the title. |
duration | Duration as HH:MM:SS, when present. |
musical_work_distribution_category | POP, JAZ, SER or UNC. |
version_type | ORI for an original work, MOD for a modified one. |
recorded_indicator | Whether a recording of the work has been released. |
publisher_count | Publisher records (SPU and OPU) in the transaction. |
writer_count | Writer records (SWR and OWR) in the transaction. |
pr_ownership_total | Performing-rights ownership across all parties, as a percentage. |
mr_ownership_total | Mechanical-rights ownership across all parties. |
sr_ownership_total | Synchronisation-rights ownership across all parties. |
pr_collection_total | Performing-rights collection shares across all parties. |
mr_collection_total | Mechanical-rights collection shares across all parties. |
sr_collection_total | Synchronisation-rights collection shares across all parties. |
share_total_status | ok, over or under -- the same verdict the report reaches. |
error_count | Findings of severity error on this work's records. |
warning_count | Findings of severity warning on this work's records. |
worst_severity | error, warning, or clean. |
parties
Every publisher and writer on every work, with ownership, collection, territories, and the net owned share. Row grain: one row per party per work.
| Column | Meaning |
|---|---|
submitter_work_number | Your own work number for the song this party is on. |
work_title | The work title as submitted. |
line_number | Line of this party's record in your file. |
record_type | SPU, OPU, SWR or OWR. |
party_sequence | Publisher sequence number, or the writer's position in the work. |
party_role | Publisher type (E, AM, SE ...) or writer designation (CA, C, A ...). |
controlled | yes for the S-prefixed record types, no for the O-prefixed ones. |
party_name | Publisher name, or writer last name. |
party_first_name | Writer first name; blank for publishers. |
ip_number | The submitter's own interested-party number. |
ipi_name_number | IPI name number, when supplied. |
ipi_base_number | IPI base number, when supplied. |
pr_society | Performing-rights society code affiliated to this party. |
pr_ownership_share | Performing-rights ownership share, as a percentage. |
mr_society | Mechanical-rights society code. |
mr_ownership_share | Mechanical-rights ownership share. |
sr_society | Synchronisation-rights society code. |
sr_ownership_share | Synchronisation-rights ownership share. |
pr_collection_share | Performing-rights collection share from this party's first included territory. |
mr_collection_share | Mechanical-rights collection share from that territory. |
sr_collection_share | Synchronisation-rights collection share from that territory. |
territories | TIS codes for this party, semicolon-separated, + included and - excluded. |
net_owned_pr | Publisher's own performing share plus its controlled writers'; blank on writer rows. |
net_owned_mr | The same for mechanical rights. |
net_owned_sr | The same for synchronisation rights. |
share_total_status | ok, over, under, or unknown when the publisher-writer chain is broken. |
findings
Every finding, with the spec's own wording and the plain-language explanation in your language. Row grain: one row per finding.
| Column | Meaning |
|---|---|
severity | error or warning. |
code | Stable identifier for this kind of problem. |
title | The problem in plain words, in the requested language. |
line_number | Line in your file; blank for file-level findings. |
record_type | The record type involved, when the finding names one. |
field | The field involved, when the finding names one. |
submitter_work_number | Work number of the song this line belongs to. |
work_title | Title of that song. |
message | The CWR spec's own wording. Always English -- societies quote it back. |
plain | The same finding as one sentence, using the record's own names and numbers. |
what | What the problem actually is. |
fix | What to do about it. |
ack
What the society said about each work you sent, one row per message. Row grain: one row per acknowledgement message, or one row per acknowledged work where the society sent no message.
| Column | Meaning |
|---|---|
line_number | Line of the ACK record in the file. |
original_transaction_type | The transaction type being acknowledged. |
original_group_id | Group id in the file you submitted. |
original_transaction_sequence | Transaction sequence number in the file you submitted. |
submitter_work_number | Your own creation number, echoed back. |
society_work_number | The number the society assigned. |
creation_title | The title as the society holds it. |
iswc | ISWC from the echoed original registration, when present. |
transaction_status | The raw transaction status code from the ACK record. |
ack_status | accepted, accepted_with_changes, rejected or unknown. |
processing_date | The date the society processed the transaction. |
message_count | How many messages the society sent about this work. |
message_sequence | 1-based position of this message; blank when there are none. |
message_type | Message type code from the MSG record. |
message_level | Message level code from the MSG record. |
validation_number | The society's validation number for the message. |
original_record_type | The record type the message is about. |
message_text | The society's own words, verbatim and never translated. |
glossed_code | Our finding code where the message maps onto one; blank otherwise. |
societies
Every society code the file references, and how often. Row grain: one row per distinct society code.
| Column | Meaning |
|---|---|
society_code | The three-digit CISAC society code as written in the file. |
society_name | The society's name; blank until the CISAC table ships. |
role | Which shares reference it: pr, mr, sr, semicolon-separated. |
occurrences | How many society fields in the file carry this code. |
first_line | The first line where it appears. |
known | known or unknown -- whether the code is in our society table. |
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,
"writers": {"distinct": 2, "omitted": 0, "rows": [
{"name": "JANE DOE", "ipi": "00014107338", "works": 2,
"advisories": ["writer-name-several-ipis"],
"paths": ["/works/0/writers/0", "/works/2/writers/1"], "…": "…"}
]},
"writer_advisories": [
{"code": "writer-name-several-ipis",
"paths": ["/works/0/writers/0", "/works/2/writers/1"], "…": "…"}
]
}
A clean catalog also comes back with its
writers and the questions it raises about them
(writer_advisories), each pointing at the catalog by JSON
path. They are advisories: they never change ok or block
generation. Add ?writer_check=true to look the writers up
on Songlinks as well, exactly as /api/validate does.
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. T1234567894. |
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.