Errors, limits and retries
One set of rules for every call: the error answer and its codes, lists and pages, sending a write safely twice, trying it first, and rate limits.
4 minute read
When something fails
Every failure has the same shape and a stable code your program can act on. The sentence is for people; the code is for code.
{
"error": {
"code": "credit.none_left",
"message": "Your organisation has no credits left.",
"hint": "Buy a pack on the Candidates page.",
"params": { "needed": 12, "balance": 3 },
"request_id": "6a0f...",
"retryable": false
}
}
Keep the request_id: it is how we find your call if you write to support@certo.bio.
| Code | Status | What to do |
|---|---|---|
api.key_invalid | 401 | Check the key; it may have a typo. A Supabase key or a console session is never an API credential |
api.not_for_this_environment | 401 | A certo_sk_test_ key outside production, a certo_sk_live_ key in production |
api.key_expired, api.key_revoked | 401 | Make a new key |
api.key_suspended | 403 | An owner or an admin adopts the key on the API keys page |
api.browser_origin | 403 | Call from a server: a request with an Origin header is a browser's |
api.credential_in_url | 400 | Send the key in Authorization only; revoke a key that was in a URL or a body |
api.organisation_not_allowed | 403 | Certo-Organisation names an organisation this credential does not act for |
api.oauth_consent_missing | 403 | Connect the assistant again and choose the organisation |
api.cursor_invalid | 400 | Pass next_cursor from the previous page, unchanged |
api.unavailable | 503 | Try again later |
api.scope_missing | 403 | Add the scope named in params.scope, or use a key that has it |
api.ip_not_allowed | 403 | Call from an allowed network, or change the key's networks |
api.rate_limited | 429 | Wait as long as Retry-After says |
api.idempotency_key_required | 400 | Send an Idempotency-Key header |
api.idempotency_mismatch | 409 | You reused a key for a different request; use a new one |
api.idempotency_in_progress | 409 | The first call with that key is still running; send it again in a few seconds |
request.unknown_field, request.field_required, request.invalid_value | 400 | The body does not fit the call: params.field names where (the schemas are in /v1/openapi.json) |
credit.none_left | 402 | Buy verifications; params.needed says how many |
organisation.profile_incomplete | 409 | Complete your company profile first |
onboarding.awaiting_certo | 403 | certo has not approved your organisation for issuing yet |
Other codes come from the same rules as the console, such as company_request.requirements_invalid or position.closed_or_missing, each with a hint. When retryable is true, the same call may succeed later.
Lists
Every list answers a page at a time:
{ "data": [ ... ], "next_cursor": "eyJ..." }
Ask for the next page with ?cursor=<next_cursor>, and the size with ?limit= (1 to 100, 25 by default). next_cursor is null on the last page.
Sending a write safely twice
A call that spends a verification or sends an e-mail (an invitation, an ask, an issuing batch) needs an Idempotency-Key header: a UUID you make for that one action. The other writes take one too, if you send it. If your call times out, send it again with the same key and the same body: you get the first answer back, with Idempotent-Replayed: true, and nothing happens twice. A call that failed is not remembered, so you may send it again with the same key. Keys are remembered for 24 hours, for your key only.
Trying it first
Add ?dry_run=true to an invitation, an ask or an issuing batch. certo checks everything and answers row by row, with the verifications it would take, and writes, sends and spends nothing: a row to send reads would_send, a record to issue would_issue, the rest as they would end. Then send the same call without it. Other calls refuse dry_run, so you never make a write by mistake.
Rate limits
| What | Limit |
|---|---|
| Any call, per key | 120 a minute, 10,000 a day |
| Reading a passport, per key | 500 a day |
| Invitation rows, per organisation | 2,000 a day |
| Issued rows, per organisation | 5,000 a day |
| Reading a pasted list | 30 an hour |
Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Over a limit, the answer is 429 api.rate_limited with Retry-After in seconds.