https://api.certo.bio/v1openapi.json

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.

CodeStatusWhat to do
api.key_invalid401Check the key; it may have a typo. A Supabase key or a console session is never an API credential
api.not_for_this_environment401A certo_sk_test_ key outside production, a certo_sk_live_ key in production
api.key_expired, api.key_revoked401Make a new key
api.key_suspended403An owner or an admin adopts the key on the API keys page
api.browser_origin403Call from a server: a request with an Origin header is a browser's
api.credential_in_url400Send the key in Authorization only; revoke a key that was in a URL or a body
api.organisation_not_allowed403Certo-Organisation names an organisation this credential does not act for
api.oauth_consent_missing403Connect the assistant again and choose the organisation
api.cursor_invalid400Pass next_cursor from the previous page, unchanged
api.unavailable503Try again later
api.scope_missing403Add the scope named in params.scope, or use a key that has it
api.ip_not_allowed403Call from an allowed network, or change the key's networks
api.rate_limited429Wait as long as Retry-After says
api.idempotency_key_required400Send an Idempotency-Key header
api.idempotency_mismatch409You reused a key for a different request; use a new one
api.idempotency_in_progress409The first call with that key is still running; send it again in a few seconds
request.unknown_field, request.field_required, request.invalid_value400The body does not fit the call: params.field names where (the schemas are in /v1/openapi.json)
credit.none_left402Buy verifications; params.needed says how many
organisation.profile_incomplete409Complete your company profile first
onboarding.awaiting_certo403certo 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

WhatLimit
Any call, per key120 a minute, 10,000 a day
Reading a passport, per key500 a day
Invitation rows, per organisation2,000 a day
Issued rows, per organisation5,000 a day
Reading a pasted list30 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.