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. ``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/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. 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/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 / ~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).

ColumnMeaning
line_numberLine of the transaction header in your file.
transaction_typeNWR, REV, ISW or EXC.
transaction_sequenceTransaction sequence number from the record prefix.
submitter_work_numberYour own work number for this song.
work_titleThe work title as submitted.
iswcISWC, when the file carries one.
language_codeLanguage of the title.
durationDuration as HH:MM:SS, when present.
musical_work_distribution_categoryPOP, JAZ, SER or UNC.
version_typeORI for an original work, MOD for a modified one.
recorded_indicatorWhether a recording of the work has been released.
publisher_countPublisher records (SPU and OPU) in the transaction.
writer_countWriter records (SWR and OWR) in the transaction.
pr_ownership_totalPerforming-rights ownership across all parties, as a percentage.
mr_ownership_totalMechanical-rights ownership across all parties.
sr_ownership_totalSynchronisation-rights ownership across all parties.
pr_collection_totalPerforming-rights collection shares across all parties.
mr_collection_totalMechanical-rights collection shares across all parties.
sr_collection_totalSynchronisation-rights collection shares across all parties.
share_total_statusok, over or under -- the same verdict the report reaches.
error_countFindings of severity error on this work's records.
warning_countFindings of severity warning on this work's records.
worst_severityerror, 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.

ColumnMeaning
submitter_work_numberYour own work number for the song this party is on.
work_titleThe work title as submitted.
line_numberLine of this party's record in your file.
record_typeSPU, OPU, SWR or OWR.
party_sequencePublisher sequence number, or the writer's position in the work.
party_rolePublisher type (E, AM, SE ...) or writer designation (CA, C, A ...).
controlledyes for the S-prefixed record types, no for the O-prefixed ones.
party_namePublisher name, or writer last name.
party_first_nameWriter first name; blank for publishers.
ip_numberThe submitter's own interested-party number.
ipi_name_numberIPI name number, when supplied.
ipi_base_numberIPI base number, when supplied.
pr_societyPerforming-rights society code affiliated to this party.
pr_ownership_sharePerforming-rights ownership share, as a percentage.
mr_societyMechanical-rights society code.
mr_ownership_shareMechanical-rights ownership share.
sr_societySynchronisation-rights society code.
sr_ownership_shareSynchronisation-rights ownership share.
pr_collection_sharePerforming-rights collection share from this party's first included territory.
mr_collection_shareMechanical-rights collection share from that territory.
sr_collection_shareSynchronisation-rights collection share from that territory.
territoriesTIS codes for this party, semicolon-separated, + included and - excluded.
net_owned_prPublisher's own performing share plus its controlled writers'; blank on writer rows.
net_owned_mrThe same for mechanical rights.
net_owned_srThe same for synchronisation rights.
share_total_statusok, 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.

ColumnMeaning
severityerror or warning.
codeStable identifier for this kind of problem.
titleThe problem in plain words, in the requested language.
line_numberLine in your file; blank for file-level findings.
record_typeThe record type involved, when the finding names one.
fieldThe field involved, when the finding names one.
submitter_work_numberWork number of the song this line belongs to.
work_titleTitle of that song.
messageThe CWR spec's own wording. Always English -- societies quote it back.
plainThe same finding as one sentence, using the record's own names and numbers.
whatWhat the problem actually is.
fixWhat 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.

ColumnMeaning
line_numberLine of the ACK record in the file.
original_transaction_typeThe transaction type being acknowledged.
original_group_idGroup id in the file you submitted.
original_transaction_sequenceTransaction sequence number in the file you submitted.
submitter_work_numberYour own creation number, echoed back.
society_work_numberThe number the society assigned.
creation_titleThe title as the society holds it.
iswcISWC from the echoed original registration, when present.
transaction_statusThe raw transaction status code from the ACK record.
ack_statusaccepted, accepted_with_changes, rejected or unknown.
processing_dateThe date the society processed the transaction.
message_countHow many messages the society sent about this work.
message_sequence1-based position of this message; blank when there are none.
message_typeMessage type code from the MSG record.
message_levelMessage level code from the MSG record.
validation_numberThe society's validation number for the message.
original_record_typeThe record type the message is about.
message_textThe society's own words, verbatim and never translated.
glossed_codeOur 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.

ColumnMeaning
society_codeThe three-digit CISAC society code as written in the file.
society_nameThe society's name; blank until the CISAC table ships.
roleWhich shares reference it: pr, mr, sr, semicolon-separated.
occurrencesHow many society fields in the file carry this code.
first_lineThe first line where it appears.
knownknown or unknown -- whether the code is in our society table.

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,
  "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:

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. T1234567894.
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.