Errors

The API uses standard HTTP status codes and a consistent JSON error envelope.

Envelope

{
  "error": {
    "code": "validation_error",
    "message": "Request failed validation.",
    "details": { "issues": [{ "path": ["recipient", "email"], "message": "Invalid email address" }] }
  }
}
  • code — a stable machine-readable string. Switch on this in your integration.
  • message — human-readable explanation. Safe to log; do not switch on this.
  • details — optional structured data (e.g. Zod issues, quota info).

Status / code table

| HTTP | code | Meaning | |---|---|---| | 401 | unauthorized | Missing Authorization header. | | 401 | invalid_api_key | Key not found, malformed, or revoked. | | 402 | quota_exceeded | Monthly certificate quota exhausted. details includes remaining, limit, resetAt. | | 403 | forbidden | The key is valid but the account is not entitled to API access — Business plan or an active partner contract required. Also returned by POST /certificates/:id/send when email delivery is disabled for the plan. | | 403 | insufficient_scope | Key lacks the scope this endpoint requires. | | 404 | not_found | Resource does not exist or is not yours. | | 405 | method_not_allowed | HTTP method not supported on this route. | | 409 | conflict | Resource state blocks the operation. Returned by POST /certificates/:id/send when the certificate is not in the Issued state. | | 422 | validation_error | Request body/query/path failed schema validation. details.issues lists the fields. | | 429 | rate_limited | Too many requests. See rate limits. | | 500 | internal_error | Something went wrong on our side. Safe to retry with backoff. |

Telling the two 403s apart

They mean different things and need different fixes, so always read code rather than the status:

  • insufficient_scope — the key is missing a scope. Issue a new key with the right scopes.
  • forbidden — the key is fine; the account lost API entitlement (plan downgrade, lapsed partner contract) or the specific feature is off for that plan. No amount of key rotation fixes it.

Quota is not uniform across issue modes

POST /certificates treats quota differently by mode, and only one of them produces a 402:

  • Single (recipient) — fails with 402 quota_exceeded when the period quota is exhausted.
  • Batch (recipients) — partial. It issues as many as the remaining quota allows and returns 201 with the rest marked failed in results. Check created and failed; a 201 does not mean every recipient succeeded.

Best practices

  • Log code, not message. Messages may change; codes are contractual.
  • Retry only idempotent failures. 500 and 429 are retry-safe; 402/403/404/409/422 are not.
  • Surface details to your users. For validation errors, the details.issues array maps directly to form fields.