Errors

Every error returned by the bind.co API uses this envelope:

{
  "type": "https://api.bind.co/docs/errors#<code>",
  "title": "<short fixed title of the code>",
  "status": 400,
  "error": "<stable code>",
  "error_description": "<human-readable explanation>",
  "error_uri": "https://api.bind.co/docs/errors#<code>",
  "fault": "client_problem | user_problem | server_problem",
  "hint": { "...": "structured diagnostic specifics" },
  "request_id": "req_xxxxxxxxxx",
  "detail": "<same as error_description>"
}

The body carries the RFC 9457 problem-details members: type (equal to error_uri), title, status (the HTTP status) and detail (equal to error_description). The content type stays application/json.

Three things to read first:

  1. error — a stable identifier you can branch on in code. RFC 6749 codes (invalid_client, invalid_grant, etc.) keep their spec wire strings. Four bind.co codes go out as their RFC equivalent, with the precise code in hint.bind_subcode; branch on hint.bind_subcode when it is present, and on error otherwise:
hint.bind_subcode error
client_not_approved access_denied
consent_revoked invalid_grant
redirect_uri_mismatch invalid_request
scope_not_registered invalid_scope

The bind.co code itself is also in type and error_uri, which end in #<code>; in the ?error= that GET /oauth2/authorize sends back to your redirect_uri (for example error=scope_not_registered); and in first_failure.error of the authorize preflight. When client_not_approved or redirect_uri_mismatch stops GET /oauth2/authorize, the user sees a bind.co error page and nothing is sent to your redirect_uri. 2. fault — tells you whose problem it is: - client_problem → you (the integrator) sent something wrong; fix your code or config. - user_problem → the end-user did something (revoked consent, etc.); usually requires re-prompting them. - server_problem → it's us. Retry; if it persists, send the request_id to support@bind.co. 3. hint — structured diagnostic. Common keys: missing (list of missing param names), received and expected (echoes), valid_statuses (allowed enum values), validation_errors (list of per-field validation failures).

Always include request_id when you write to support@bind.co — it lets us find the exact request in our logs.


invalid_request

The request is missing a required parameter, includes an unsupported value, or is otherwise malformed. RFC 6749 §5.2.

Common causes - Forgot a required body/form/query parameter on /oauth2/token or /oauth2/authorize. - Validation failed on a typed body (also surfaces as 422 with hint.validation_errors, each failing field listed once). - An id is not one this API issued for that field: a plain number or UUID, or an id of another type. This is a 400 with hint.field (where it was sent) and hint.expected (the prefix it needs, such as con_…). - A page_token the list did not return, or one edited, or one from another list: HTTP 400 with hint.field set to page_token. - The redirect_uri is not registered for your application. hint.bind_subcode is redirect_uri_mismatch.

How to fix - Read hint.missing for the exact list of missing parameter names. - Read hint.expected for the full required set so you know what to add. - For 422 responses, read hint.validation_errors[].loc to find the failing field by location (query.page_size, body.page_number, …). Each item is a ValidationErrorItem in the reference: loc, msg and type. - For an id refused with 400, send the id exactly as the API returned it; ids are opaque strings.

Example

POST /oauth2/token
grant_type=authorization_code&code=abc&redirect_uri=...&client_id=...
# (forgot client_secret)
{
  "error": "invalid_request",
  "error_description": "Missing required parameter(s): client_secret.",
  "fault": "client_problem",
  "hint": {
    "missing": ["client_secret"],
    "expected": ["code", "redirect_uri", "client_id", "client_secret"],
    "grant_type": "authorization_code"
  }
}

invalid_client

Your application's credentials were refused. RFC 6749 §5.2.

Common causes - A wrong client_id or client_secret at /oauth2/token, /oauth2/revoke, /oauth2/introspect, /v1/oauth2/authorize/preflight or a webhook route. - No Authorization: Basic header at one of those routes. - No application has that client_id. - The application is not APPROVED or IMMUTABLE, so it cannot obtain tokens.

How to fix - Check both client_id and client_secret against the developer portal. - The secret is shown once, when it is created. If you lost it, reset it in the developer portal and use the new one.

Note on security: at /oauth2/token the answer has an empty hint. It never says which of client_id and client_secret was wrong, because that would help someone guess credentials. Check both.


invalid_grant

The provided authorization grant (auth code, refresh token) is invalid, expired, revoked, or doesn't match the redirection URI used in the authorization request. RFC 6749 §5.2.

Returned where a grant is exchanged, at POST /oauth2/token. An access token rejected by any other route is invalid_token, except a 403 with hint.bind_subcode consent_revoked: see consent_revoked.

Common causes - Authorization codes are single-use and expire after 10 minutes; reusing or waiting too long fails. - Refresh tokens expire after 30 days or when the user revokes consent. - On an API call, hint.bind_subcode consent_revoked: the user disconnected your application.

How to fix - For expired refresh tokens: send the user back through /oauth2/authorize.


invalid_token

The access token is missing, expired, revoked or not a bind.co token. HTTP 401. RFC 6750 §3.1.

Common causes - No Authorization: Bearer <token> header. The response's WWW-Authenticate: Bearer realm="bind.co" names the scheme to use, with no error code (RFC 6750 §3.1). - An Authorization header with another scheme, for example Basic. The description is Authorization must use the Bearer scheme. and WWW-Authenticate carries error="invalid_token". - The access token expired (1 hour lifetime) or was revoked. - The bearer is not a bind.co access token — for example a Google or Microsoft token. Only tokens from POST /oauth2/token are accepted.

How to fix - Send the access token as Authorization: Bearer <token>. - For an expired access token: call POST /oauth2/token with grant_type=refresh_token, then retry with the new one. - For a revoked one: send the user back through /oauth2/authorize. - The WWW-Authenticate header carries the same error as the body: Bearer realm="bind.co", error="invalid_token", error_description="…".

Note on security: when a well-formed token can no longer be used (its user or application is gone, for example), the answer is this same code with the description Could not validate credentials. It never says which check failed, because that would help someone guess valid tokens.


unauthorized_client

Your application may not use the grant it sent to POST /oauth2/token. RFC 6749 §5.2.

Common causes - None today. Every application may use both grants bind.co supports, authorization_code and refresh_token; any other grant_type is unsupported_grant_type.

How to fix - Send grant_type=authorization_code or grant_type=refresh_token. - A route your application may not call answers access_denied, not this code.


unsupported_grant_type

grant_type value is not recognized. RFC 6749 §5.2.

Common causes - Sent something other than authorization_code or refresh_token on POST /oauth2/token.

How to fix - Read hint.received (what you sent) vs hint.expected (allowed values). - bind.co supports authorization_code and refresh_token only.


unsupported_response_type

response_type value is not recognized. RFC 6749 §4.1.2.1.

Common causes - Sent something other than code on GET /oauth2/authorize.

How to fix - Send response_type=code. bind.co supports the authorization code grant only (no implicit, no token).


invalid_scope

A requested scope is invalid, unknown, or exceeds what the user granted. RFC 6749 §5.2.

Common causes - A protected endpoint requires a scope the user did not consent to. - A required scope is missing from the access token. - A route that changes records needs the data area's :write scope, and the token holds only :read. - A scope is not registered for your application. hint.bind_subcode is scope_not_registered.

How to fix - Read hint.required_scope and hint.granted_scopes. A :write scope also satisfies its :read scope, so a hint.required_scope ending in :read is met by either. - If the scope is missing, send the user back through /oauth2/authorize with the additional scope in the scope parameter so they can grant it. - See also: scope_not_registered for when a scope isn't even registered for your client.


access_denied

The user, the resource owner, or the authorization server denied the request. RFC 6749 §4.1.2.1.

Common causes - The user never consented to your application. - The user chose Decline on the consent screen. GET /oauth2/authorize then sends them back to your redirect_uri with error=access_denied. - The route is not open to your application. hint.bind_subcode is endpoint_client_not_allowed. - Your application is not approved, or was suspended or delisted. hint.bind_subcode is client_not_approved.

How to fix - Read hint.reason for the cause. - If the user has not consented, send them through GET /oauth2/authorize. - A route that is not open to your application cannot be opened to it; use the routes in the API reference.


server_error

The server encountered an unexpected condition. RFC 6749 §5.2.

Common causes - An internal error in bind.co.

How to fix - Retry the request after a short delay. - If it persists, write to support@bind.co with the request_id from the error response. - This is fault=server_problem — it's us, not you.


temporarily_unavailable

The server is currently unable to handle the request due to temporary overloading or maintenance. RFC 6749 §5.2.

Common causes - bind.co is under maintenance or overloaded.

How to fix - Retry with exponential backoff. - Honor Retry-After if present.


client_not_approved

Your application is not approved, or it was suspended or delisted. bind.co extension. In a JSON answer, error is access_denied and hint.bind_subcode is client_not_approved.

Common causes - A new application starts as a DRAFT. Only an APPROVED or IMMUTABLE application can send users to /oauth2/authorize or call the API for them. - bind.co suspended or delisted an application that was approved.

How to fix - At /oauth2/authorize, hint.current_status is your application's status. When the request is checked before sign-in, hint.valid_statuses also lists the statuses that work. - On an API call, the same two are hint.client_status and hint.active_statuses. - A new application: submit it for review in the developer portal. - A suspended or delisted one: contact bind.co.


The user disconnected your application. HTTP 403. bind.co extension. In a JSON answer, error is invalid_grant and hint.bind_subcode is consent_revoked.

Common causes - The user disconnected your application at bind.co.

What happens - The consent ends at once, and every token your application holds for that user is revoked with it, access and refresh tokens alike. - So the next API call usually answers invalid_token. This code shows only when a token stayed live after the consent ended. - A refresh answers invalid_grant. - Your webhook endpoints receive consent.revoked, and no more events are sent for that user.

How to fix - Stop calling the API for that user. - To work for them again, send them through /oauth2/authorize to consent again. - This is fault=user_problem: your code is fine, the user opted out.


redirect_uri_mismatch

The redirect_uri you sent is not one registered for your application. bind.co extension; see also RFC 6749 §3.1.2 and §4.1.2.1. In a JSON answer, error is invalid_request and hint.bind_subcode is redirect_uri_mismatch.

Common causes - A typo, a trailing slash, or http instead of https. - A development URI used in production, or the other way round, without registering both.

How to fix - Compare hint.received (what you sent) with hint.registered (what is on file). They must match exactly, byte for byte. - Fix the URI in your code, or register the one you need in the developer portal.


scope_not_registered

A scope you asked for is not registered for your application. bind.co extension. In a JSON answer, error is invalid_scope and hint.bind_subcode is scope_not_registered; the authorize redirect sends error=scope_not_registered.

Common causes - The scope name is mistyped. - Your application was registered with other scopes than the ones your code asks for.

How to fix - Compare hint.requested (what you sent), hint.registered_for_client (what your application can use) and hint.not_registered (the scopes at fault). - Remove the unregistered scope from your authorize URL, or add it to your application in the developer portal. Scopes can change only while the application is a DRAFT.


not_found

The record does not exist, or it is not yours to see. bind.co extension.

Common causes - A mistyped path, such as /v1/oath2/... for /v1/oauth2/.... - An id that does not exist, or that belongs to another user or another application.

How to fix - Check the path against the API reference. - Read hint for the id you sent, and check it against the list route that returns it.


conflict

The requested change conflicts with current state. bind.co extension.

Common causes - Creating a resource that already exists. - Registering a webhook endpoint when your application already has the most active endpoints allowed (hint.max_active_endpoints). - Redelivering a webhook delivery or event that cannot be redelivered in its current status (hint.current_status).

How to fix - Read hint for the specific conflicting state. - Many 409s are idempotent if you treat them as success: when hint shows the record is already in the state you asked for, the desired end state is achieved.


rate_limited

You sent more requests than the route allows in its window. HTTP 429. bind.co extension.

Common causes - Polling a route, such as an authentication route, faster than its per-minute limit. - Spending a webhook endpoint's test-delivery budget or an application's redelivery budget.

How to fix - Wait the number of seconds in the Retry-After header (also hint.retry_after_seconds), then retry. - hint.limit (or hint.limit_per_minute) tells you the window; every limit is listed at Rate limits. - Back off exponentially if you hit it repeatedly; do not tighten the loop.


already_sent

The draft was already sent, so no second email went out. HTTP 409. bind.co extension.

Common causes - Retrying a send after a timeout, when the first attempt had in fact succeeded. - Two parts of your app sending the same draft at the same time.

How to fix - Treat it as success: hint.draft_email_message_id is the id of the email that was sent.


email_send_failed

The mailbox provider did not confirm the email was sent, so the draft was not marked sent. HTTP 502. bind.co extension.

Common causes - The user's mailbox connection has expired or was revoked. - The mailbox provider is temporarily unavailable, or stopped answering after accepting the email.

How to fix - hint.may_have_sent is false when the mailbox could not be used at all: ask the user to reconnect, then retry. - When it is true, check the user's Sent folder first: a provider can fail after it accepted the email. A retry with the same Idempotency-Key gets this answer back and sends nothing; send a new key only if the email is not in Sent. - If it keeps failing, ask the user to reconnect their mailbox.


idempotency_key_reused

The Idempotency-Key was already used on this route with a different request. HTTP 422. bind.co extension.

Common causes - Reusing one key for two different requests.

How to fix - Send a new key for every new request; reuse a key only to retry the exact same request.


idempotency_in_progress

A request with the same Idempotency-Key is still running. HTTP 409. bind.co extension.

Common causes - Retrying before the first attempt has finished.

How to fix - Wait a few seconds and retry with the same key: once the first attempt finishes, you get its answer.


precondition_failed

The record changed since you read it. HTTP 412. RFC 9110 §13.1.1.

Common causes - You sent If-Match with the ETag of an earlier read, and the record was changed since: by another request, a person, or a newer reading of the inbox.

How to fix - Read the record again, reapply your change to what you read, and send it with the new ETag. - hint.current_etag is the record's tag now. Omit If-Match to change the record whatever its state.