Requests and errors
Ids
Every id is an opaque string with a type prefix, such as clm_0a6NnX3I9. Store them as strings and never parse them; the prefix only tells you what kind of object it names.
| Prefix | Object |
|---|---|
clm_ | Claim |
rom_ | Room |
itm_ | Item (one inventory line) |
rpl_ | Replacement offer for an item |
pho_ | Photo |
exp_ | Export (XLSX or PDF) |
whe_ | Webhook endpoint |
evt_ | Webhook event |
org_ | Organization |
Claims also take your own id as externalRef. Filter by it with ?externalRef= to find the claim for one of your records without storing our id.
Money, rates and time
- Money is an integer number of US cents, in fields ending
Cents:129999is $1,299.99. - Rates are integer basis points, in fields ending
Bp:881is 8.81%,10000is 100%. - Timestamps are ISO 8601 in UTC (
2026-10-24T18:40:52.000Z); calendar dates areYYYY-MM-DD.
Integers on both sides mean no floating-point rounding between your system and ours.
Pagination
Lists return { "object": "list", "data": [...], "hasMore", "nextCursor" }. Ask for up to 100 at a time with limit (default 25), and pass nextCursor back as cursor until hasMore is false.
let cursor;
do {
const url = new URL('https://contents.team/api/v1/claims');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.CONTENTS_API_KEY}` } });
const page = await res.json();
for (const claim of page.data) handle(claim);
cursor = page.hasMore ? page.nextCursor : undefined;
} while (cursor);Syncing changes
To keep a copy in step, note the time before you start, page through with updatedSince set to your last sync time, and use the noted time next run. Overlap is harmless; a gap is not.
Idempotency
Every POST accepts an Idempotency-Key header: 1 to 255 visible ASCII characters you choose, unique per operation (a UUID, or your own record id plus the action). Retrying with the same key and the same body returns the first response instead of acting twice, so a timeout or a dropped connection is always safe to retry. Reusing a key with a different body is a 409. The MCP server's write tools take the same key as an idempotencyKey argument.
Idempotency-Key: job-1042-createErrors
Every error has the same envelope. Branch on type and code, which are stable; message is for people and may change. Quote requestId (also the X-Request-Id header on every response) when you write to us.
{
"error": {
"type": "permission_error",
"code": "insufficient_scope",
"message": "This API key is missing a required scope: claims:read.",
"details": {
"required": [
"claims:read"
]
},
"requestId": "req_5e0c1f7a2b9d44c08a61e3f2"
}
}| Status | type | What to do |
|---|---|---|
| 400 | invalid_request_error | Fix the request. details lists each bad parameter. |
| 401 | authentication_error | The key is missing, wrong, revoked or expired. |
| 403 | permission_error | The key lacks a scope (code insufficient_scope), is a live key or OAuth app waiting for approval (code live_key_pending_approval or oauth_app_pending_approval), or the action is not allowed. |
| 404 | not_found_error | No such object in this organization. |
| 409 | conflict_error | The request conflicts with the current state; read it again, then retry. |
| 429 | rate_limit_error | Slow down; retry after the Retry-After seconds. |
| 500 | api_error | Our fault. Retry with backoff; it is safe with an Idempotency-Key. |
Rate limits
- 300 requests a minute per API key.
- 600 requests a minute per IP address, across keys.
- 60 review writes a minute per API key (approve, unapprove, reject and accepting a replacement, REST and MCP together), inside the 300.
Over either limit you get a 429 with a Retry-After header in seconds. Back off for at least that long, then retry. If an integration needs more, tell us what it does and we will raise it.