One key, seven routes, JSON. Your score and its history, every scan, every case with its letters and approvals, and every receipt, returned by the same code that draws the portal. The API reads and nothing else, on purpose.
Sign in to your portal, open Controls, and create a key under API key. It is shown once, at the moment it is made, because we store only a hash of it and cannot show it to you again. One key per account. Creating another revokes the one you have, which is the whole of the rotation story: put the new key in place, and the old one stops working the moment you press the button.
A key carries exactly the reach of the account it belongs to and nothing more. Your key sees your ledger, your cases and your money. It cannot see another client's anything, and neither can ours.
A bearer header on every request. There is no OAuth dance, no client secret and no token to refresh, because nothing here is writable and no third party is being granted anything on your behalf.
curl https://clearline.co.uk/api/v1/score \ -H "Authorization: Bearer clk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Every response is JSON with ok: true on success. Nothing is cached: you get the figure as it stands when you ask.
Everything that changes a case, sends a letter or agrees a settlement runs through the portal or our own console, where the approval gates and the audit trail live. An endpoint that could put a letter in front of one of your customers without a person seeing it would be a liability dressed up as a feature, so it does not exist. POST, PUT, PATCH and DELETE all return 405 with a message saying as much.
Scopes are deliberately absent for the same reason. With nothing writable there is nothing to scope down to, and a scope list would imply a permission model we do not have.
| Route | Returns |
|---|---|
| GET /api/v1/account | Who the key belongs to, when the account was opened, and when the key was last used. |
| GET /api/v1/score | The latest Clearline Score, its four components, the trend against your own first scan, and the history. |
| GET /api/v1/scans | Scans, newest first, with totals, counts and the score. ?limit= 1 to 200, default 50. |
| GET /api/v1/scans/:id | One scan in full, including the report exactly as the engine produced it. |
| GET /api/v1/cases | The cases on the account. ?status= open, awaiting_approval, paused, disputed, paid, closed. |
| GET /api/v1/cases/:id | One case with its invoices, its statutory sums, and every letter, approval and receipt on it. |
| GET /api/v1/receipts | Money received and remitted, with the fee on each. ?since=YYYY-MM-DD. |
The one figure most people want on a wall. components carries the four parts and their weights; the method page publishes the exact thresholds, so you can reproduce the number yourself from the same scan.
GET /api/v1/score
{
"ok": true,
"score": 61,
"band": "fair",
"as_at": "2026-09-12T09:14:03.118Z",
"read": "Ageing is healthy; exposure is concentrated in three customers.",
"components": {
"ageing": { "score": 31, "of": 40 },
"spread": { "score": 11, "of": 20 },
"discipline":{ "score": 14, "of": 20 },
"statutory": { "score": 5, "of": 20 }
},
"trend": { "from": 48, "to": 61, "change": 13,
"since": "2026-05-02T10:22:41.006Z", "scans": 4 },
"history": [ { "at": "2026-05-02T10:22:41.006Z", "score": 48, "band": "weak" } ],
"method": "https://clearline.co.uk/method#score"
}The comparison is your account against its own first scan and nothing else. We publish no benchmark against other businesses, because we do not have one worth publishing and an invented one would be worse than none. If a scan has not produced a score yet, score is null and the response says why rather than returning a zero you might plot.
Each row is one ledger scan: what was in it, what the Act put on it, and the score it produced. totals is the same object the report shows, so a figure on your own dashboard and the figure in your portal are the same figure.
GET /api/v1/scans?limit=2
{
"ok": true,
"count": 2,
"scans": [
{
"id": "3f1c…",
"created_at": "2026-09-12T09:14:03.118Z",
"as_at": "2026-09-11",
"filename": "aged-debtors-sep.csv",
"invoices": 148,
"score": 61,
"band": "fair",
"totals": { "overdue": 184250.44, "statutory": 12884.19,
"recoverable": 197134.63, "expected": 128410.55 }
}
]
}Add the id to the path for the whole thing: GET /api/v1/scans/3f1c… returns the same row with report attached, which is the full engine output including every invoice it read and every row it could not.
The list is a summary per case. The single-case route is the one to build on, because it carries the correspondence and the approvals, which is what makes an external dashboard worth looking at.
GET /api/v1/cases/9a2e…
{
"ok": true,
"case": {
"id": "9a2e…", "ref": "CL-24817",
"debtor": "Pennant Construction Limited",
"debtor_company_number": "07733441",
"status": "open", "phase": "pre-legal",
"last_letter": "L2", "next_letter": "L3", "next_due": "2026-09-24",
"principal": 18420.00, "invoices": 3,
"recoverable": 19824.22,
"statutory": { "interest": 1104.22, "compensation": 300,
"total": 1404.22, "daily_accrual": 5.96,
"as_at": "2026-09-17",
"act": "Late Payment of Commercial Debts (Interest) Act 1998" },
"invoice_detail": [ { "ref": "I1", "gross": 12000, "due": "2026-04-01",
"days_late": 169, "base_rate": 3.75, "statutory_rate": 0.1175,
"interest": 652.93, "compensation": 100,
"recoverable": 12752.93,
"limitation": { "expires": "2032-04-01", "days": 2023 } } ],
"letters": [ { "code": "L1", "channel": "email",
"status": "delivered", "sent_at": "2026-08-14T08:02:11Z" } ],
"approvals": [ { "code": "SETTLE", "subject": "Offer at 78% of principal",
"requested_at": "…", "decided_at": "…", "decision": "declined" } ],
"receipts": [ { "amount": 6000, "fee": 900,
"received_at": "…", "remitted_at": "…" } ]
}
}Statutory sums are computed, not stored: the interest figure is correct to the day you ask for it, at the reference rate that applies to that debt, and daily_accrual is what it grows by tomorrow. The method page sets out how. An invoice the engine cannot price, because it has no usable due date or one earlier than the published rate table, appears in invoices_unpriced with the reason rather than being silently dropped from the total.
Every payment, the fee on it and what was remitted to you, with a total across the rows returned. net is the amount less the fee, so a reconciliation does not have to trust your arithmetic or ours.
GET /api/v1/receipts?since=2026-07-01
{ "ok": true, "count": 11, "total": 74250.12,
"receipts": [ { "id": "…", "case_id": "9a2e…", "amount": 6000,
"fee": 900, "net": 5100,
"received_at": "2026-09-02T11:41:00Z",
"remitted_at": "2026-09-04T09:12:00Z",
"collected_by": "clearline" } ] }One shape, always, with a code you can branch on and a message written for the person who will end up reading your logs.
{ "error": { "code": "unknown_key",
"message": "That key is not recognised. It may have been replaced: creating a new one in the portal revokes the old one.",
"docs": "https://clearline.co.uk/developers" } }| Status | Code | When |
|---|---|---|
| 401 | no_key | No Authorization header. |
| 401 | bad_key | Not the shape of a Clearline key. |
| 401 | unknown_key | Not recognised, usually because it has been replaced. |
| 403 | account_inactive | The account the key belongs to is not active. |
| 400 | bad_status | A status filter outside the allowed list. |
| 404 | not_found | No record with that id on this account. It is never a hint that one exists elsewhere. |
| 405 | read_only | Any method other than GET. |
| 429 | rate_limited | Over the limit below. |
| 503 | not_configured | The database is unreachable. Retry rather than treating it as an answer. |
A hundred and twenty requests a minute per key. Nothing here is heavy enough to need more, and if your use is, tell us what it is and we will raise it rather than making you paginate around it.
Fields will be added; fields will not be removed or renamed under /v1. If something has to change in a way that would break a reader, it will appear at /v2 and /v1 will keep answering. Treat any field you have not seen documented as liable to move.
Two requests get everything most dashboards need: /score for the headline and /cases?status=open for the work in progress. Polling either more than once an hour tells you nothing new, because a letter goes out when the ladder says so, not when you ask.
Found something wrong, or want a route that is not here? Tell us. The list above is short because it is the list people have actually asked for.
Run the free scan first. It produces the score and the totals the API returns, and it takes about a minute.