BBankConnectorDeveloper Docs

Errors & Retries

Audience: πŸ”Œ Integration Client

Getting error handling right is most of what separates a fragile integration from a reliable one. This chapter gives you a model you can code against directly.

The error envelope

Almost every error is:

{ "error": { "code": "validation_failed", "message": "…", "details": { "issues": [ … ] } } }

⚠️ The envelope is not universal. A few responses are intentionally flat { "error": "<string>" }:

  • 413 body-too-large
  • the idempotency terminal-error body
  • the CSRF 403 (browser only β€” you won't hit it as an API client)

Everywhere else, error is always an object. If you're distinguishing the two shapes in code, check typeof body.error === "string" for the flat variant. The API Reference's ErrorResponse schema now matches this object envelope (it previously described the legacy flat shape β€” fixed).

The 400 vs 422 boundary

This distinction is deliberate and useful:

So: 400 β†’ fix your request construction; 422 β†’ surface the issues to whoever supplied the payment data. Neither is retryable as-is.

What's retryable

StatuscodeRetry?How
429rate_limitedβœ… yeswait for Retry-After / retryAfterSeconds, then retry
503overloadedβœ… yestransient (converter pool busy); honour Retry-After (~2 s)
503service_unavailableβœ… yesa dependency is down; back off
409conflict (in-progress)βœ… yessame idempotency key, retry shortly
409conflict (different-body)❌ noyou reused a key with a changed body β€” fix it
500internal_error⚠️ mayberetry once with backoff; if it persists, capture X-Request-ID and escalate
502sftp_probe_failed etc.⚠️ maybeupstream hiccup; limited retry
400any❌ nofix the request
401unauthorized❌ no*re-authenticate (check the key); not a blind retry
403forbidden / admin_required❌ noscope/role problem; will never succeed as-is
404*_not_found❌ nowrong id / resource
422validation_failed etc.❌ nofix the data

Rule of thumb: retry 429, 503, in-progress 409, and (cautiously) 5xx. Never blind-retry a 4xx other than in-progress 409.

The two faces of 409

Because it's the one that bites people: on POST /journal/payments, 409 means either

Tell them apart by the message text, then branch. (Details in Idempotency.)

Rate limiting

A retry helper

async function withRetry<T>(fn: () => Promise<Response>, parse: (r: Response) => Promise<T>) {
  const MAX = 5;
  for (let attempt = 1; attempt <= MAX; attempt++) {
    const res = await fn();
    const requestId = res.headers.get("x-request-id");

    if (res.ok) return parse(res);

    const retryable =
      res.status === 429 ||
      res.status === 503 ||
      (res.status === 409 && /being processed/i.test(await res.clone().text())) ||
      res.status >= 500;

    if (!retryable || attempt === MAX) {
      throw new Error(`${res.status} not retryable (request ${requestId})`);
    }

    const retryAfter = Number(res.headers.get("retry-after")) || 2 ** attempt;
    await new Promise(r => setTimeout(r, retryAfter * 1000));
  }
  throw new Error("unreachable");
}

Always capture X-Request-ID

Every response carries an X-Request-ID. Log it with every call. When you open a support ticket, quoting it lets the operator look up the exact request in the exception log. It's the single most useful thing you can record.