DocsErrors and rate limits
  1. Docs
  2. Guides
  3. Reference

Errors and rate limits

Every error has the same shape, a stable code to branch on, and a Request-Id. Here is what to retry, what to fix and what to show the payer.

De documentatie voor ontwikkelaars is beschikbaar in het Engels.

The error shape

HTTP
HTTP/1.1 400 Bad Request
Request-Id: req_Qm3…

{
  "error": {
    "type": "invalid_request",
    "code": "parameter_invalid",
    "param": "amount",
    "message": "amount must be a positive integer",
    "request_id": "req_Qm3…"
  }
}
  • Branch on type and code. They are stable; message is for humans and may change.
  • param names the field at fault (e.g. payer.email, crypto.asset), or is null.
  • Log request_id (also in the Request-Id header of every response, success or not). Support can find any call by it.
  • Don't show message to payers. Map the cases you expect to your own wording.

Types and what to do

HTTPtypeWhat to do
400invalid_requestFix the request. param names the field. Retrying unchanged gives the same answer.
401authentication_errorMissing, malformed or revoked key. Don't retry; check your configuration.
403permission_errorThe key can't do this: a publishable key on a server call, a store-limited key on workspace data, or live mode not open yet.
404not_foundNo such object in this workspace and mode. A test id with a live key (or the other way round) also gives 404.
409order_stateThe object is in the wrong state (e.g. refunding an unpaid order), or an idempotency conflict. Fetch the object and decide.
422method_unavailableNo allowed payment method can take this order. Change methods or amount, or offer another way to pay.
429rate_limitedToo many requests. Wait Retry-After seconds, then retry.
5xxapi_errorOur side. Retry with backoff and the same Idempotency-Key.

The full list of codes is in the API reference. The ones you are most likely to meet when creating orders:

codeTypical causeHandling
parameter_invalid, parameter_missing, parameter_unknown, amount_mismatchA bug in your request.Fix and redeploy; alert yourself.
amount_too_smallThe amount is outside the limits of every allowed method (e.g. 2 USD with only card).Allow more methods or tell the payer the minimum.
currency_unsupportedA currency we don't accept.Convert to USD or EUR before creating the order.
reference_takenAnother order in this mode already has this reference, often a retry without an Idempotency-Key.Look the order up with GET /orders?reference=… and reuse it, or add a suffix.
crypto_unsupportedThe preset network or coin isn't offered (or not running in live mode).Fall back to creating the order without crypto.
no_method_enabledNone of the requested methods is switched on for this account or store.Check payment method settings; in live mode they need to be granted.
payer_blockedThe payer's email can't be accepted.Show a neutral "payment not possible, contact us".
account_inactiveA live key before your account is approved.Use test keys until live mode opens.

Rate limits

Each key may make 100 requests per second, with short bursts allowed. Every response carries:

HeaderMeaning
RateLimit-LimitRequests allowed per window.
RateLimit-RemainingRequests left in the current window.
RateLimit-ResetSeconds until the window resets.
Retry-AfterOn 429 only: seconds to wait before retrying.

If you hit the limit regularly, you are probably polling. Use webhooks instead of polling order state, and list endpoints with limit=100 (filter GET /orders by reference, state or created[gte] to fetch just the orders you need).

Retrying safely

  • GET requests can always be retried.
  • POST requests: send an Idempotency-Key (up to 255 characters, e.g. your order number) and retry with the same key and body. For 48 hours we return the first answer instead of doing the work twice; a replayed answer has Idempotent-Replay: 1.
  • A 5xx answer is not stored: retrying with the same key runs the request again. A 4xx answer is stored and replayed.
  • 409 idempotency_mismatch: you reused a key with a different body. Use a new key for a new request.
  • 409 idempotency_in_progress: the first request with this key is still running. Wait a moment and retry.
  • Timeouts: if you don't know whether a POST went through, retry with the same key. You get the original order, not a second one.
JavaScript
async function velpay(path, { method = "GET", body, idempotencyKey } = {}) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`https://velpay.pro/api/v1${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${process.env.VELPAY_SECRET_KEY}`,
        ...(body && { "Content-Type": "application/json" }),
        ...(idempotencyKey && { "Idempotency-Key": idempotencyKey }),
      },
      body: body && JSON.stringify(body),
    });
    if (res.ok) return res.json();

    const retryable = res.status === 429 || res.status >= 500;
    if (!retryable || attempt === 4) {
      const { error } = await res.json().catch(() => ({ error: { message: res.statusText } }));
      throw Object.assign(new Error(error.message), { status: res.status, ...error });
    }
    // 429: the server says how long. 5xx: exponential backoff with jitter.
    const retryAfter = Number(res.headers.get("Retry-After"));
    const wait = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 250 + Math.random() * 250;
    await new Promise((r) => setTimeout(r, wait));
  }
}

// POSTs are only retried safely with an Idempotency-Key:
await velpay("/orders", { method: "POST", body: { amount: 4900, currency: "usd" }, idempotencyKey: "order-A-2041" });

Maintenance

During rare maintenance windows POST /orders may answer 503 with code paused. Treat it like any other 5xx: retry with backoff, or offer the payer to try again in a few minutes. Reads keep working.