General

Errors

Every failure returns the same shape with a stable string code. Branch on the code; the message is for whoever reads the log.

{
  "error": {
    "code": "insufficient_scope",
    "message": "This key is not granted mail.send.",
    "details": [ { "field": "required_scope", "value": "mail.send" } ],
    "request_id": "req_..."
  }
}
Codes are permanent

A published code is never renamed, never reused for a different condition, and never removed within a major version. New codes may be added, so treat an unrecognised one as its HTTP status.

Authentication

The key was missing, malformed, or is not one of ours.

CodeStatusWhat it means
missing_credential 401 No Authorization header was sent.
wrong_credential_type 401 Something other than a veruapis key was presented, such as a VeruSuite session token or personal API token. Those do not work here.
invalid_token 401 The key does not exist or its secret does not match.
token_expired 401 The key passed its expiry date. Create a new one.
token_revoked 401 The key was revoked.
grant_revoked 401 The authorization behind the key was withdrawn: the person who created it signed out everywhere, changed their password, or was removed. Retrying will not help; a new key is needed.
tenant_inactive 401 The workspace is suspended.

Permissions

The key is valid but is not allowed to do this.

CodeStatusWhat it means
insufficient_scope 403 The key was not granted a permission this endpoint requires. The missing one is named in details.
admin_scope_required 403 The endpoint needs a workspace admin permission, which only an administrator can put on a key.
plan_required 403 The workspace's plan does not include this app. A new key will not help; the plan has to change.
resource_forbidden 403 The person the key acts as has no access to this particular item.

Request problems

CodeStatusWhat it means
malformed_request 400 The body is not valid JSON.
unknown_parameter 400 An unrecognised query parameter was sent. Unknown parameters are rejected rather than ignored, so a typo in a filter cannot silently return the wrong data.
invalid_parameter 400 A parameter was present but unusable.
invalid_id 400 An identifier had the wrong prefix. The expected one is named in details.
invalid_cursor 400 The pagination cursor could not be decoded.
idempotency_key_required 400 A write was sent without an Idempotency-Key header.
validation_error 422 The request was well formed but semantically invalid. Field-level problems are in details.
payload_too_large 413 The body exceeded the documented maximum.

Conflicts

CodeStatusWhat it means
idempotency_key_reuse 409 The same idempotency key was reused with a different body.
idempotency_key_in_flight 409 An earlier request with this key has not finished. Retry shortly.
version_conflict 409 If-Match did not match the current version.
state_conflict 409 The item is in a state that forbids this operation.
already_exists 409 Something with that name or address already exists.
cursor_expired 410 The pagination position is no longer valid. Start the listing again.

Limits

Two different problems with two different fixes: a burst, and a plan allowance.

CodeStatusWhat it means
rate_limited 429 Too many requests too quickly. Back off using the RateLimit headers.
upstream_rate_limited 429 A backend refused. The RateLimit headers do not describe this limit.
quota_exceeded 429 The plan's request allowance is spent. Waiting for the reset or upgrading are the only fixes.

Failures

A 500 is always our fault. Anything else names the service that failed, so you can tell your own users what is happening.

CodeStatusWhat it means
internal_error 500 Something went wrong on our side. The request id identifies it in our logs.
upstream_error 502 A backend returned something unusable.
upstream_unavailable 503 A backend is unreachable. The backend is named in the response.
upstream_timeout 504 A backend took too long.