CentraPoint

Getting started

Errors & rate limits

Error response format, error codes, validation issues, rate limits and retry guidance.

On this page

CentraPoint uses conventional HTTP status codes. 2xx means success, 4xx means the request needs to change, and 5xx means something went wrong on our side.

Error format#

Every API error response has a JSON body with a stable, machine-readable code and a human-readable message:

Error body
{
  "error": "unauthorized",
  "message": "Invalid API key"
}
  • Branch on error (and the HTTP status). It is one of the codes below.
  • Show or log message, but do not parse it: wording can change.
  • Validation failures add an issues array.

Error codes#

Error codes
HTTPerrorMeaningWhat to do
400invalid_requestThe body is not valid JSON (message: Body must be JSON), failed validation (issues included), or broke a business rule, e.g. Customer not found or Invoice is paid. The message is always safe to show.Fix the request. Do not retry unchanged.
401unauthorizedAPI key missing, malformed, unknown or revoked.Check the Authorization header and key.
403plan_restrictedYour plan does not include the API, or the feature (e.g. payment links).Upgrade the plan in Settings → Billing.
403plan_limitA plan quota was reached, e.g. the customer limit when a new customer would be created.Upgrade, or reuse existing customers.
403account_restrictedThe account is read-only (suspended, cancelled or trial ended). Write requests only.Resolve billing in the dashboard.
404not_foundThe resource does not exist in your organisation.Check the ID or reference.
409idempotency_conflictThe Idempotency-Key was already used with a different request body.Use a new key for a new operation; resend the identical body when retrying.
409invalid_stateThe action isn't allowed from the resource's current status, e.g. resuming a cancelled subscription.Fetch the resource and check its status.
409unsupportedThe operation isn't supported for this resource, e.g. cancelling a subscription on a gateway that can't cancel through CentraPoint.Do it in the gateway's portal.
413invalid_requestRequest body larger than 100 000 characters.Send less data.
429rate_limitedMore than 120 requests in a minute with this API key (or 600 from one IP address).Wait for Retry-After seconds, then retry.
500internal_errorUnexpected server error. The message is generic; details are logged by CentraPoint.Retry with backoff (see below). Contact support if it persists.
502gateway_errorA payment gateway rejected or failed an operation CentraPoint forwarded (subscription cancel). Nothing changed.Retry later.
502–504–Proxy or network error before the request reached the API. The body may not be JSON.Retry with backoff.

Validation errors#

When the request body fails validation, the response is 400 with error: "invalid_request", message: "Invalid body", and one entry in issues per problem. Each issue has a code, a path to the field and a message; some codes include extra detail such as minimum or format.

400 Bad Request
{
  "error": "invalid_request",
  "message": "Invalid body",
  "issues": [
    {
      "origin": "number",
      "code": "too_small",
      "minimum": 0,
      "inclusive": false,
      "path": ["amount"],
      "message": "Too small: expected number to be >0"
    },
    {
      "origin": "string",
      "code": "invalid_format",
      "format": "email",
      "path": ["customerEmail"],
      "message": "Invalid email address"
    }
  ]
}

Common mistakes#

  • Sending amount as a string ("499.00"). It must be a JSON number (499.00).
  • Sending expiresAt without seconds or timezone (2026-10-31T23:59). Include both, e.g. 2026-10-31T23:59:59+02:00. It must also be in the future and at most 1 year ahead.
  • Using a currency none of your enabled gateways accepts. Enable a suitable gateway first, or use a supported currency.
  • Sending invalid JSON. You get 400 with the message Body must be JSON (no issues). An empty body is treated as {}, so required fields are reported as missing.

Rate limits#

Each API key may make 120 requests per minute, counted in fixed 60-second windows that start on the minute. The count is shared across all CentraPoint servers, so it is the same whichever instance serves your request.

Separately, a coarse guard allows 600 requests per minute per client IP address before the API key is even checked. It only protects against floods (for example many requests with bad keys); normal integrations never reach it. The client IP is taken from the entry our own proxy appends to X-Forwarded-For, so it cannot be spoofed by setting that header yourself.

Rate-limit headers#

Every response to an authenticated request (success or error) includes the state of your key's current window:

Rate-limit headers
HeaderMeaning
X-RateLimit-LimitRequests allowed per window (120).
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix time (seconds) when the current window ends and the count resets.
Retry-After429 only: seconds to wait before retrying.

401 unauthorized responses carry no rate-limit headers, because the key is not known yet.

429 Too Many Requests
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1790323260
Retry-After: 17

{"error":"rate_limited","message":"Rate limit of 120 requests per minute exceeded for this API key"}

If the per-IP guard is hit instead, the message is Too many requests from this network and the headers describe the IP window (limit 600).

Retries and timeouts#

  • 429: wait for the number of seconds in Retry-After (or until X-RateLimit-Reset), then retry.
  • 500 internal_error, 502–504 and network errors: retry reads (GET) with exponential backoff and jitter, e.g. 1s, 2s, 4s, up to about 5 attempts.
  • POST requests: send an Idempotency-Key and reuse it on retries, so a timeout or 500 never creates a second link, invoice or payment. See Idempotency, metadata & references for how to handle this safely.
  • 4xx other than 429: do not retry without changing the request.
  • Use a client timeout of around 30 seconds.

Example: GET with retries#

async function getWithRetry(url, attempts = 5) {
  for (let i = 0; i < attempts; i++) {
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.CENTRAPOINT_API_KEY}` },
      signal: AbortSignal.timeout(30_000),
    }).catch(() => null); // network error or timeout

    if (res && res.status !== 429 && res.status < 500) return res; // success or non-retryable 4xx

    let waitMs = Math.min(30_000, 1000 * 2 ** i) + Math.random() * 250; // backoff + jitter
    if (res?.status === 429) {
      const retryAfter = Number(res.headers.get("Retry-After"));
      const reset = Number(res.headers.get("X-RateLimit-Reset"));
      waitMs = retryAfter > 0 ? retryAfter * 1000 : reset > 0 ? reset * 1000 - Date.now() : waitMs;
    }
    await new Promise((r) => setTimeout(r, Math.max(0, waitMs)));
  }
  throw new Error("CentraPoint API unavailable after retries");
}