Create a claim
You need the entity_id of a connected business. See
Connect a business.
partner_claim_id is your id for the case. Use the same value to upload the
documents and to create the claim, and again when you retry.
The examples use the live entity_id aZ4kQ9pX2. With a test key, use
sandbox or an entity_id from GET /entities.
Step 1: upload the documents
This step is optional. Each document needs two requests. Do them before you create the claim.
Get a signed URL
Required: entity_id, partner_claim_id, filename, mime_type.
| Limit | Value |
|---|---|
| File size | 1 byte to 50 MB, or 5 MB until Garfield approves your partner account. max_bytes can set a lower limit. A higher value fails with 400 validation_failed. |
Permitted mime_type | PDF, JPEG, PNG, DOCX, plain text and CSV. Convert other formats to one of these first. The API reference lists the exact values. |
filename extension | Must match mime_type, for example .pdf for application/pdf. Otherwise the request fails with 400 validation_failed. |
curl -X POST "$BASE/documents/upload-url" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"entity_id": "aZ4kQ9pX2",
"partner_claim_id": "case-2026-00412",
"filename": "invoice-00412.pdf",
"mime_type": "application/pdf",
"max_bytes": 5242880
}'
201 Created:
{
"upload_url": "https://storage.googleapis.com/...",
"storage_path": "entities/aZ4kQ9pX2/claims/pc_af66fa4b5cb7283356863b7554d4e140/documents/0TCsxImkxYI.pdf",
"method": "PUT",
"expires_at": "2026-05-31T15:19:05.000Z",
"required_headers": {
"Content-Type": "application/pdf",
"x-goog-content-length-range": "1,5242880"
}
}
The upload_url expires after 15 minutes.
Upload the file
Send the file to upload_url with the required_headers exactly as you got
them. This request goes to Google Cloud Storage, so leave out the bearer token.
Use PUT. Opening the URL in a browser sends a GET, which returns
MalformedSecurityHeader.
curl -X PUT "<upload_url>" \
-H "Content-Type: application/pdf" \
-H "x-goog-content-length-range: 1,5242880" \
--data-binary @invoice-00412.pdf
To add a document to a claim that already exists, upload it the same way with
the claim's partner_claim_id, then send its storage_path in a
message.
Step 2: create the claim
Required: entity_id, partner_claim_id, defendant.name, and one or more
invoices. All other fields are optional.
Put all context for Garfield in message, up to 10,000 characters: the history
of the debt, what the documents are, and your instructions. message and
document_refs become the first message of the claim. That message starts
Garfield's processing. If you send neither, the first message is a fixed note
saying the claim came through the API without a note, so Garfield's processing
still starts.
Garfield acts on every message you send as it would on the business's own. It sends nothing, charges nothing and files nothing at court until the business confirms that step in Garfield.
The connected entity is always the claimant. A firm that acts for clients can raise claims through the API only for its own debts.
curl -X POST "$BASE/claims" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @claim.json
{
"entity_id": "aZ4kQ9pX2",
"partner_claim_id": "case-2026-00412",
"partner_user_id": "acme-crm-7741",
"partner_invoice_ids": ["crm-inv-88", "crm-inv-91"],
"message": "Two overdue invoices, disputed once. Invoice attached.",
"defendant": {
"name": "Debtor Ltd",
"email": "accounts@debtor.example",
"address": {
"first_line": "1 High Street",
"town_city": "London",
"county": "Greater London",
"postcode": "EC1A 1BB",
"country": "United Kingdom"
},
"is_consumer": false,
"is_individual": false,
"company_registration_number": "12345678"
},
"invoices": [
{
"number": "INV-1001",
"total_amount_for_invoice": "1500.00",
"created_date": "2026-01-10",
"due_date": "2026-02-10",
"summary": "Consulting services"
},
{
"number": "INV-1002",
"total_amount_for_invoice": "900.00",
"created_date": "2026-02-01",
"due_date": "2026-03-03",
"summary": "Consulting services, phase 2",
"line_items": [
{
"description": "Consulting, phase 2",
"quantity": "6",
"unit_price": "125.00",
"tax": "150.00"
}
]
}
],
"contract_date": "2026-01-01T00:00:00.000Z",
"is_written": true,
"document_refs": [
{
"storage_path": "entities/aZ4kQ9pX2/claims/pc_af66fa4b5cb7283356863b7554d4e140/documents/0TCsxImkxYI.pdf",
"mime_type": "application/pdf",
"filename": "invoice-00412.pdf"
}
]
}
| Field | Use |
|---|---|
invoices[].number | The number printed on the invoice. Garfield uses it in the claim. |
partner_invoice_ids | Your own ids for the invoices. Only for your reconciliation. GET /claims returns them. |
document_refs | The storage_path, mime_type and filename of each file from step 1, up to 20 files. |
partner_user_id | Must match the partner_user_id the entity connected with, or the API returns 409 partner_user_id_mismatch. |
The defendant address uses town_city and county. The onboarding address
uses locality and region. Do not mix the two.
Dates and amounts
Garfield works out the limitation period and interest from these values, so
the API rejects a malformed one with 400 validation_failed.
- Send
invoices[].created_date,invoices[].due_dateandcontract_dateasYYYY-MM-DD, for example2026-05-31. The API also accepts an ISO-8601 date-time withZor an offset, but the offset can move the date across midnight, so send the plain date. An empty string means unknown, the same asnull. The API rejects anything else, such as31/05/2026,2026-5-1,2026-02-30or a date-time without an offset. - Send
invoices[].total_amount_for_invoiceas a decimal string with at most two decimal places, for example"4200.00"or"4200". The API rejects currency symbols and commas, as in"£4,200".
The error's details.fieldErrors names the field, for example
{"invoices": ["due_date must be a YYYY-MM-DD date or an ISO-8601 date-time with Z or an offset"]}.
Line items
Garfield needs each invoice's line items (description, quantity and unit
price) to take a claim forward. Send them in invoices[].line_items, or attach
the invoice as a document and Garfield reads them from it. Each line item has:
description(required): what the business supplied.quantity(required): a decimal string greater than zero, with at most 9 digits before the decimal point and 4 after it, for example"2"or"1.5".unit_price(required): the price for each unit before tax, in the same format astotal_amount_for_invoice.tax(optional): the tax on that line, in the same format.
Eligibility
The API accepts any well-formed claim. Garfield then checks whether it can take
it forward. If it cannot, the claim shows status: "invalid" with reasons in
GET /claims. Garfield handles:
- Defendants in England and Wales. The defendant's postcode must be in England or Wales, and the contract must not give exclusive jurisdiction to courts elsewhere or require arbitration.
- Small claims. The total, with interest and charges, must be within the £10,000 small claims track limit.
- One claimant and one defendant for each claim.
- Debts within the limitation period. Garfield cannot pursue an invoice that fell due more than six years ago (twelve for a contract made by deed), or that will reach that age within two weeks.
- Money claims for goods and services, refunds, rent arrears and unpaid wages. Claims for damages, personal injury, fraud or regulated consumer credit are out of scope.
201 Created, or 200 OK on a replay:
{
"claim_id": "k4m9t",
"message_id": "a1b2c",
"document_ids": ["d3f4g"],
"already_existed": false
}
Every new claim gets a first message, so a create always returns a message_id.
A replay returns the first call's message_id and document_ids. A replay of a
claim created before Garfield began storing message and document ids returns
message_id: null and document_ids: [].
A replay does not add documents or a message. A replay with a different body
returns 409 idempotency_key_reused. To correct the claim or add documents
later, add a message. See
Errors and limits.
Step 3: list your claims
curl "$BASE/claims?entity_id=aZ4kQ9pX2&limit=100" \
-H "Authorization: Bearer $TOKEN"
| Query parameter | Required | Notes |
|---|---|---|
entity_id | No | The entity. Leave it out to list your claims across every entity. |
updated_since | No | An ISO-8601 date-time with Z or an offset. Only claims updated at or after it. Any other value fails with validation_failed (400). |
limit | No | Page size. Default 100, maximum 200. Below 1 means 1, above 200 means 200, and a value that is not a number means 100. |
page_token | No | The next_page_token from the previous page, unchanged. Any other value fails with validation_failed (400). |
partner_user_id | No | Only the claims with this partner_user_id. Requires entity_id. |
Without updated_since, the newest claim is first. A test key lists its test
claims, and a live key its live ones. Each claim has:
status: where the claim is. See Claim status. Base your logic on it.needs_attentionandreasons: see Claim status.status_label: the stage as the business sees it. It is a label and can change.stage: Garfield's internal stage. Garfield can rename or add internal stages without notice.name: the name of the connected business, which is the claimant, or itsentity_idif it has no name.defendant_name,amount(the sum of the invoice totals, ornullif Garfield cannot read any invoice total) andoutstanding(what is still owed, with interest and charges). Both amounts are pounds as decimal strings, for example"4200.00".outstandingisnulluntil Garfield has worked it out.created_atandlast_updated_at.- Your ids:
partner_user_id,partner_claim_idandpartner_invoice_ids.
Without entity_id, the list covers every entity, and each claim's entity_id
says which one it belongs to. The list leaves out claims on an entity that
disconnected from you or that you can no longer reach, so a page can hold fewer
than limit claims. With the entity_id of an entity that
disconnected from you, the API returns 403 partner_disconnected.
entity_disconnected is deprecated and always false. next_page_token is
null on the last page.
Claim status
status | Meaning |
|---|---|
received | Created. Garfield is checking it. |
in_progress | Garfield is preparing the claim before a letter goes out, or waiting for the business to choose an approach. |
letter_sent | A chaser or letter before action has gone out. Garfield is waiting for, or handling, a reply. |
court | Garfield is preparing court proceedings, or they are under way, up to and including judgment. |
enforcement | Garfield is enforcing a judgment. |
resolved | The claim is closed or closing. |
invalid | Garfield cannot take the claim forward as it stands. reasons says why. |
archived | The business archived the claim. |
These values are stable. Garfield never renames or removes them, and maps new internal stages onto them.
needs_attention is true when the claim will not move on by itself: it is
invalid, or Garfield is waiting on the business for information, a decision
or a document review.
reasons lists why an invalid claim is invalid, as { code, message }. Base
your logic on code. message is one sentence for display. reasons is empty
for every other status, and for claims that became invalid before Garfield
began recording reasons.
code | Why |
|---|---|
wrong_jurisdiction_debtor | The defendant is outside England and Wales, or Garfield could not confirm where they are. |
invalid_postcode | The defendant's postcode is no longer in use. |
amount_too_high | The claim is above the £10,000 small claims track limit. |
invalid_amount | Garfield cannot work out the claim's total from the invoices. |
outside_limitation_period | An invoice is outside, or too close to, the limitation period. |
more_than_one_debtor, more_than_one_claimant | Garfield handles one defendant and one claimant for each claim. |
arbitration_clause, wrong_jurisdiction_clause | The contract sends disputes to arbitration or to courts outside England and Wales. |
damages_claim, fraud_claim, loan_dispute, personal_injury, flight_compensation_claim, not_debt_claim | The claim is not a money claim Garfield handles. |
outside_small_claims_track | The court has allocated, or intends to allocate, it to another track. |
duplicate_claim, not_enforceable | The judgment the claim would enforce already belongs to another claim in Garfield, or has no enforcement route available. |
paused | Garfield's team has paused the claim. |
wrong_jurisdiction_claimant | The claimant is outside England and Wales, or Garfield could not confirm where they are. |
Garfield can add new codes, so treat an unknown one as a general reason.
Poll for changes
Send updated_since to get only the claims that changed. The least recently
updated comes first. Garfield stamps last_updated_at when it starts working on
a claim, which can be a few minutes before it saves the change. So start each
poll from the largest last_updated_at you have seen, minus 15 minutes:
curl "$BASE/claims?updated_since=2026-10-01T09:00:00Z" \
-H "Authorization: Bearer $TOKEN"
Claims in that 15-minute overlap can come back more than once, so make processing the same claim twice harmless.
Read one claim
GET /claims/{claim_id} returns one claim, in the same shape as a claim from
GET /claims:
curl "$BASE/claims/k4m9t" \
-H "Authorization: Bearer $TOKEN"
Once the claim's entity disconnects from you, the API returns 403
partner_disconnected. 404 claim_not_found means you have not created a
claim with that claim_id using a key of this mode (a test key cannot read a
live claim, or the reverse), or the claim is on an entity you can no longer
reach.
Step 4: add to a claim
POST /claims/{claim_id}/messages adds a note, documents or both to a claim you
created. Garfield reads it as it reads the first message and takes the claim
forward. Use it to correct a detail, such as a new defendant postcode, to give
Garfield what it is waiting for when needs_attention is true, or to send a
document you did not have before.
curl -X POST "$BASE/claims/k4m9t/messages" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"partner_message_id": "note-2026-00412-1",
"message": "Correction: the debtor moved to 14 Mill Lane, Sheffield S3 8AB. Signed delivery note attached.",
"document_refs": [
{
"storage_path": "entities/aZ4kQ9pX2/claims/pc_af66fa4b5cb7283356863b7554d4e140/documents/Q2xhaW0xYI.pdf",
"mime_type": "application/pdf",
"filename": "delivery-note-00412.pdf"
}
]
}'
message: free text, up to 10,000 characters.document_refs: up to 20 files, each uploaded first with the claim'spartner_claim_id, with the same checks as onPOST /claims. A file already attached to the claim returns400document_path_invalid. Upload it again to send a new copy.partner_message_id(optional): your id for this message. It makes a retry safe.
Send message, document_refs or both. A request with neither returns 400
validation_failed. You can add messages only while you can still create
claims for the entity, so 403 partner_disconnected and
entity_not_verified mean the same as on POST /claims. A test key adds to
its test claims.
201 Created, or 200 OK on a replay of a partner_message_id:
{ "message_id": "m7p2q", "document_ids": ["d8h3k"], "already_existed": false }
The API stamps the claim's last_updated_at with the time of the call, so the
next poll with updated_since picks the claim up.
Without partner_message_id, every call adds a message, so a retry after a
timeout can add the same note twice. With it, a retry with the same body
returns the first message_id and document_ids and adds nothing. The same
partner_message_id with a different body returns 409
idempotency_key_reused. 409 claim_archived means the business archived
the claim, so you cannot add anything more to it.
Node example
This helper does steps 1 and 2. It uploads each file, then creates the claim.
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'
const BASE = 'https://www.garfield.law/api/partner/v1'
const TOKEN = process.env.GARFIELD_API_KEY! // 'pk_test_<partner_id>.<key_id>.<secret>'
const jsonHeaders = {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
}
// Get a signed URL, then PUT the file to storage.
// Returns the document_refs entry to pass to POST /claims.
async function uploadDocument(
entityId: string,
partnerClaimId: string,
filePath: string,
mimeType: string
) {
const filename = basename(filePath)
const mint = await fetch(`${BASE}/documents/upload-url`, {
method: 'POST',
headers: jsonHeaders,
body: JSON.stringify({
entity_id: entityId,
partner_claim_id: partnerClaimId,
filename,
mime_type: mimeType,
}),
})
if (!mint.ok)
throw new Error(`upload-url failed: ${(await mint.json()).code}`)
const { upload_url, storage_path, required_headers } = await mint.json()
// Send required_headers exactly as returned.
const put = await fetch(upload_url, {
method: 'PUT',
headers: required_headers,
body: await readFile(filePath),
})
if (!put.ok) throw new Error(`upload PUT failed: ${put.status}`)
return { storage_path, mime_type: mimeType, filename }
}
// Upload every file first (in parallel), then create the claim.
export async function createClaimWithDocuments(input: {
entityId: string
partnerClaimId: string
message?: string
defendant: { name: string } & Record<string, unknown>
invoices: Array<Record<string, unknown>>
files: Array<{ path: string; mimeType: string }>
}) {
const document_refs = await Promise.all(
input.files.map((f) =>
uploadDocument(input.entityId, input.partnerClaimId, f.path, f.mimeType)
)
)
const res = await fetch(`${BASE}/claims`, {
method: 'POST',
headers: jsonHeaders,
body: JSON.stringify({
entity_id: input.entityId,
partner_claim_id: input.partnerClaimId,
message: input.message ?? null,
defendant: input.defendant,
invoices: input.invoices,
document_refs,
}),
})
if (!res.ok)
throw new Error(`create claim failed: ${(await res.json()).code}`)
// { claim_id, message_id, document_ids, already_existed }
return res.json()
}