Skip to main content

Requests and conventions

How every API request and answer is shaped, covering JSON, paging, money, times, safe retries, edits and errors.

The API is JSON over HTTPS at https://app.tradesender.co.uk/api/v1. These rules hold for every operation in the API reference.

A request

Send your API key in the Authorization header. A request with a body is JSON, with the record under its own name:

curl -X POST "https://app.tradesender.co.uk/api/v1/customers" \
  -H "Authorization: Bearer $TRADESENDER_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "customer": { "name": "Taylor Home", "email": "taylor@example.test" } }'

An answer

One record comes back under data. A list comes back as an array under data, with meta beside it:

{
  "data": [{ "id": "…", "name": "Taylor Home" }],
  "meta": { "page": 1, "per_page": 50, "total": 1, "total_pages": 1 }
}

A delete answers 204 with no body.

New fields are added to answers without notice. Ignore a field or a value you do not know. See Changes.

Paging

A list returns 50 records a page. Ask for another page with page, and for up to 100 a page with per_page. meta.total_pages says when to stop.

curl "https://app.tradesender.co.uk/api/v1/customers?page=2&per_page=100" \
  -H "Authorization: Bearer $TRADESENDER_KEY"

How values are written

ValueWritten asExample
IdA UUID"0b6f3c1e-…"
MoneyA whole number of pence, in a field ending _cents4500 is £45.00
QuantityA decimal in a string"1.5"
Tax rateBasis points2000 is 20%
TimeISO 8601 with an offset"2026-09-21T09:30:00+01:00"
DateISO 8601"2026-09-21"

A time with no offset is read in the workspace's own time zone.

Deleting archives

DELETE on a customer, a contact, a property, a quote, a job or an invoice archives it. It leaves the lists and stays on the records that name it. Lists take include_archived where that applies. A service is deactivated in the same way.

Some records can be archived only in certain states: a quote while it is a draft, an invoice while it is a draft or void, a job once it is completed or cancelled. Otherwise the API answers 409.

An enquiry, a visit photo, a time entry and time off are removed for good.

Retry safely with Idempotency-Key

Every request that changes something needs an Idempotency-Key header: a value you make up, at most 200 characters, such as a UUID. It lets you repeat a request after a timeout without doing the work twice.

  • Send the same request with the same key within 24 hours and you get the first answer again, with the header Idempotency-Replayed: true. Nothing changes.
  • Use a new key for each new piece of work.
  • A key belongs to the member, so two members can use the same value.
  • A key also belongs to the way it was first sent: with an API key, from a signed-in app, or by an AI assistant. The same key sent another way is refused with idempotency_key_reused, because the three are not sent the same fields.
What you sentStatusCode
A change with no Idempotency-Key422idempotency_key_required
A key longer than 200 characters422invalid_idempotency_key
A key already used for a different request409idempotency_key_reused
A key whose first request is still running409idempotency_request_in_progress

Making an API key is the one change whose answer is never replayed. That operation is closed to keys in any case.

Edit without overwriting someone

Records that people edit carry a lock_version, which goes up by one with each change. Send the lock_version you read when you update a record. If someone else changed it in the meantime the API answers 409 with the code stale_object. Read the record again, apply your change and retry.

Leave lock_version out and your update goes through whatever anyone else did. Send it.

Errors

A refused request answers with a status from 400 to 499 and a list of errors:

{
  "errors": [
    {
      "code": "validation_failed",
      "detail": "Name can't be blank",
      "source": { "pointer": "/data/attributes/name" }
    }
  ]
}

Branch on code. Show detail to a person: its wording may change. source.pointer names the field when one field is at fault, and meta carries more where it helps.

StatusCodeMeaning
400bad_requestThe request is malformed: a missing field, or a date that is not a date.
401unauthorizedThe key is missing, wrong, revoked or expired, or its member has been deactivated.
402plan_limit_reachedThe workspace's plan does not allow this. detail says which limit.
403forbiddenYour role may not do this, or a key may not: see API keys.
404not_foundNo such record in this workspace.
409stale_objectSomeone changed the record since you read it.
409invalid_transitionThe record is in the wrong state for this action.
409conflictThe change clashes with an existing record.
422validation_failedA field is wrong. One error for each problem.
429rate_limitedToo many requests. See Rate limits.

Other codes appear with 409 and 422 for particular operations. Treat a code you do not know by its status.

A status of 500 or more is our fault. Retry with the same Idempotency-Key. A change and its recorded answer are saved together, so the retry either repeats the first answer or does the work once.

Checked against the app on 8 October 2026. Still stuck? Ask usAsk us.