> ## Documentation Index
> Fetch the complete documentation index at: https://developers.bloobank.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate limiting

> Quotas, response headers, and the recommended backoff strategy when limits are exceeded.

<Note>
  **Proposed defaults — pending product confirmation.** The exact rate-limit values and header names below are aligned with industry conventions (AWS, GitHub, Stripe). Concrete numbers for the BlooBank Transactions Engine will be finalized before this page exits "Proposed" status.
</Note>

The BlooBank API applies rate limits per credential to protect platform stability. This page documents how clients should interpret rate-limit responses and back off gracefully.

## How limits are applied

| Scope                       | What it counts                                                                                               |
| --------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Per credential (Access Key) | All requests authenticated with the same `X-Access-Key`.                                                     |
| Per endpoint                | Each endpoint has its own bucket. Hitting a limit on `POST /paymentOrders` does not throttle `GET /wallets`. |
| Sliding window              | Limits are evaluated continuously, not in fixed wall-clock buckets.                                          |

Limits are applied **after** authentication. An unauthenticated burst will fail with `SIGNATURE_INVALID` long before it consumes quota.

## When you exceed a limit

The API returns `HTTP 429` with `error.status: "RESOURCE_EXHAUSTED"`:

```json theme={"dark"}
{
  "error": {
    "code": 429,
    "status": "RESOURCE_EXHAUSTED",
    "message": "Rate limit exceeded for this credential.",
    "details": [
      {
        "reason": "RATE_LIMIT_EXCEEDED",
        "description": "You have exceeded the request quota for this endpoint.",
        "metadata": {
          "limit": 100,
          "window_seconds": 60,
          "retry_after_seconds": 12
        }
      }
    ]
  }
}
```

The response also includes a `Retry-After` HTTP header carrying the same hint as `metadata.retry_after_seconds`.

## Recommended response

<Steps>
  <Step title="Honor Retry-After when present">
    Use either the `Retry-After` header or `details[0].metadata.retry_after_seconds`. Wait at least that long before retrying.
  </Step>

  <Step title="Apply exponential backoff with jitter">
    For repeated `RESOURCE_EXHAUSTED` (or in absence of `Retry-After`), back off exponentially: `min(2^attempt × base_delay, max_delay)` and add full jitter.
  </Step>

  <Step title="Bound your retries">
    Three attempts is a reasonable default. After that, surface the failure to the caller — escalating without backoff would compound the throttling.
  </Step>

  <Step title="Keep `idempotencyKey` stable across retries">
    Throttled creates should retry with the **same** `idempotencyKey` so a partially-succeeded request is not duplicated. See [Idempotency](/get-started/concepts/idempotency).
  </Step>
</Steps>

## Backoff reference

A canonical exponential backoff with full jitter (AWS guidance):

```ts theme={"dark"}
function delayMs(attempt: number): number {
  const base = 500;       // 500ms
  const cap  = 30_000;    // 30s
  const exp  = Math.min(cap, base * 2 ** attempt);
  return Math.random() * exp; // full jitter: random in [0, exp)
}

let attempt = 0;
while (attempt < 3) {
  try {
    return await api.call(req);
  } catch (err) {
    if (err.status === 'RESOURCE_EXHAUSTED') {
      const hint = err.details?.[0]?.metadata?.retry_after_seconds;
      const wait = hint ? hint * 1000 : delayMs(attempt);
      await sleep(wait);
      attempt++;
      continue;
    }
    throw err;
  }
}
```

## What does *not* count against quota

* Failed authentication (`SIGNATURE_INVALID`, `TIMESTAMP_SKEW_EXCEEDED`, `REPLAY_DETECTED`). These are rejected at the edge and do not consume per-endpoint quota — but they do consume an authentication-attempt quota with its own (more permissive) limit.
* Webhook acknowledgements (your `2xx` response to a delivery). Webhooks are server→you; no quota applies on the return path.

## Best practices

| Pattern                   | Why                                                                                                                                                            |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Concurrent worker cap** | Hold concurrency at a level below the steady-state rate limit. Bursts above that level should queue, not fire.                                                 |
| **Per-tenant fan-out**    | If you run multi-tenant infrastructure, spread your call cadence so one tenant's burst does not block another's normal traffic.                                |
| **Cache wallet metadata** | `getWallet` results change rarely. Cache them for minutes, not seconds.                                                                                        |
| **Avoid hot loops**       | Polling `getPaymentOrder` in a tight loop is the most common reason integrators hit limits — use [webhooks](/get-started/webhooks/overview) for state changes. |

## Increasing your limit

Per-credential limits are configurable for production workloads. To request an increase, contact your BlooBank account team with:

1. Your `X-Access-Key` (the credential needing the bump).
2. The endpoint(s) and target rate.
3. A brief description of the workload (steady vs. bursty, peak hours, etc.).

## Next

<CardGroup cols={2}>
  <Card title="Retry strategy" icon="rotate-right" href="/get-started/errors/retry-strategy">
    Backoff for `UNAVAILABLE` and `RESOURCE_EXHAUSTED` in detail.
  </Card>

  <Card title="Webhooks" icon="webhook" href="/get-started/webhooks/overview">
    Reduce polling — receive state changes by push.
  </Card>
</CardGroup>
