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

# Rate Limits

> How rate limiting works, per-endpoint limits, and how to handle 429 responses.

Every API key is rate-limited using a sliding window. Limits are flat: the same for every organization and every key. There are no API plan tiers.

A separate, lower layer also throttles by IP address, before any API key is checked at all - see [Pre-authentication IP throttle](#pre-authentication-ip-throttle) below. That layer, not this one, is what catches unauthenticated and credential-guessing traffic.

## Response headers

Every response that reaches the rate limiter includes rate-limit headers, success or error. A request reaches it only after the key, the plan and account state, and the scope have passed. The headers are therefore absent on `401` and `403` responses and on pre-authentication `429`s (see [Pre-authentication IP throttle](#pre-authentication-ip-throttle)). Pre-authentication `429`s carry only `Retry-After`.

| Header | Meaning |
| - | - |
| `X-RateLimit-Limit` | Your rate limit for this endpoint (requests per minute) |
| `X-RateLimit-Remaining` | Requests remaining in the current window |
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the oldest request in the window expires. An epoch timestamp, not a duration |
| `RateLimit-Policy` | The static policy for the bucket, as `"<bucket>";q=<quota>;w=<window seconds>` |
| `RateLimit` | Live state, as `"<bucket>";r=<remaining>;t=<seconds until reset>`. `t` is a duration in seconds, not an epoch timestamp |
| `Retry-After` | Seconds to wait before retrying. Present on `429`, and on `409 idempotency_key_in_use` |

`POST /v1/notifications` also advertises its daily budget with `X-RateLimit-Daily-Limit`, `X-RateLimit-Daily-Remaining` and `X-RateLimit-Daily-Reset`.

## How the sliding window works

Each bucket above tracks requests in a rolling window, not a fixed per-minute clock that resets on the dot. Every request you send is timestamped; a request counts against your limit until it "ages out" of the trailing window, and `X-RateLimit-Remaining` reflects exactly how much budget is left in that window at the moment you asked. When you run out, the API returns `429` with a `Retry-After` telling you precisely how long until enough old requests age out to free up capacity - that's the number to sleep for, not a guess.

**`Retry-After` gets your next request through, not your next burst.** It counts down to the moment the oldest request in your window ages out, which is when a single retry will be accepted. It is not the moment your whole budget returns. The rest comes back gradually, one slot at a time, as each earlier request ages out in turn. So after a `429`, size your next burst from `X-RateLimit-Remaining` on the response you just received, rather than assuming a fresh window. A client that sleeps `Retry-After` and then fires the full limit will be throttled again immediately.

Measured on the default bucket, after a burst that exhausted it:

| Moment | `X-RateLimit-Remaining` |
| - | - |
| The `429` itself | 0 |
| `Retry-After` elapsed | 15 |
| About 35 seconds after that | 58 |

How much budget is back at `Retry-After` depends on how your earlier requests were spaced, so treat the middle row as an illustration, not a number to hard-code. The header is the only reliable answer.

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E5F6FF','primaryBorderColor':'#36C6FA','primaryTextColor':'#0B3A4A','lineColor':'#2BA8D9','secondaryColor':'#F4FCFF','tertiaryColor':'#FFFFFF','fontFamily':'Manrope, ui-sans-serif, system-ui, sans-serif'}}}%%
sequenceDiagram
    participant Client
    participant API as VEXiON API

    Note over Client,API: Sliding window example - 60 req/min per key

    Client->>API: Request
    API-->>Client: 200 OK<br/>X-RateLimit-Limit: 60<br/>X-RateLimit-Remaining: 59<br/>X-RateLimit-Reset: 1723130460

    Client->>API: Request (repeated)
    API-->>Client: 200 OK<br/>X-RateLimit-Remaining: 1

    Client->>API: One request too many
    API-->>Client: 429 Too Many Requests<br/>X-RateLimit-Remaining: 0<br/>Retry-After: 12

    Note over Client: Wait exactly Retry-After seconds -<br/>no custom exponential backoff

    Client->>API: Retry the same request
    API-->>Client: 200 OK<br/>X-RateLimit-Remaining: 59
```

## Default limits

Limits are flat: the same fixed budget applies to every organization. Every bucket below is keyed per API key **except** the customer lookup row, which is keyed per organization instead:

| Endpoint | Limit |
| - | - |
| Default (most endpoints) | 60 requests / minute per API key |
| `POST /v1/transactions`, `POST /v1/transactions/{id}/reverse` | 120 requests / minute per API key |
| `POST /v1/notifications` (broadcasts) | 1 request / minute and 50 requests / day per API key |
| `GET /v1/customers` with an `email`, `phone`, or `external_id` lookup filter | 5 requests / **second**, per **organization** |
| `POST /v1/webhook_endpoints` | 10 requests / hour per API key |
| `POST /v1/webhook_endpoints/{id}/test` | 10 requests / minute per endpoint |
| `POST /v1/webhook_endpoints/{id}/deliveries/{id}/retry` | 10 requests / minute per endpoint |
| `POST /v1/team_members/{id}/resend_invitation` | 1 request / minute per member |

A customer lookup counts against two buckets: the default bucket (60 requests per minute per key) and the stricter lookup bucket (5 requests per second per organization). Whichever runs out first returns the `429`. On a lookup that succeeds, the `X-RateLimit-*` headers describe the lookup bucket. Because the lookup bucket is org-scoped rather than key-scoped, every API key belonging to the same organization shares this one budget; minting a second key does not multiply it (unlike every other row above, where a second key gets its own independent budget).

> [!NOTE]
>
> Contact [support@vexioncards.one](mailto:support@vexioncards.one) if you need higher limits.

## Pre-authentication IP throttle

Everything above is keyed on `api_key_id` and only runs once a request has been authenticated. A separate, independent layer runs on every `/v1` request **before** the API key is even looked up, keyed on the caller's IP address instead. It exists to protect the authentication check itself, since an unauthenticated or key-guessing request is cheap to send but not cheap to reject.

| Ceiling | Window | Code |
| - | - | - |
| 600 requests | per minute, per IP | `429 rate_limit_error` / `rate_limited` |
| 20 failed authentications | per 15 minutes, per IP | `429 rate_limit_error` / `too_many_failed_attempts` |

Only responses that come back `401` (missing, invalid, or revoked key) count toward the failed-authentication ceiling. A `403` from a valid key that simply lacks a required scope does not count - that is a configuration problem, not a guessing attempt.

Both ceilings send `Retry-After` (seconds), same as every other 429 on this page. A caller can be rejected here without an API key ever having been evaluated, so a 429 with `code: too_many_failed_attempts` or `rate_limited` at unusually high volume does not necessarily mean any of your keys are over budget - check which ceiling the error names.

> [!NOTE]
>
> These ceilings are enforced across all servers, not per server. The counters are shared, so a restart or redeploy does not reset them.

## What triggers 429

You get a `429 Too Many Requests` response in one of two cases:

* The number of requests we have seen from your key (or, for the customer lookup filter, your organization) in the trailing window exceeds the relevant `X-RateLimit-Limit` above.
* Your IP address has exceeded one of the two pre-authentication ceilings described in [Pre-authentication IP throttle](#pre-authentication-ip-throttle) - this can happen even before your API key is checked.

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E5F6FF','primaryBorderColor':'#36C6FA','primaryTextColor':'#0B3A4A','lineColor':'#2BA8D9','secondaryColor':'#F4FCFF','tertiaryColor':'#FFFFFF','fontFamily':'Manrope, ui-sans-serif, system-ui, sans-serif'}}}%%
flowchart TD
    A[Request arrives] --> B{Key authenticated<br/>yet?}
    B -- No --> C{IP over 600/min, or<br/>20 failed auths/15min?}
    C -- Yes --> D["429 - rate_limited or<br/>too_many_failed_attempts"]
    C -- No --> E[Authentication proceeds]
    B -- Yes --> F{Key or org over its<br/>endpoint bucket?}
    F -- Yes --> G["429 - rate_limit_error"]
    F -- No --> H[Request processed normally]

    classDef success fill:#36C6FA,stroke:#2BA8D9,color:#ffffff,font-weight:600;
    classDef muted fill:#94A3B8,stroke:#94A3B8,color:#ffffff;
    class H success;
    class D,G muted;
```

Both paths return the same `429` status and a `Retry-After` header, so the response body's `code` is what tells you which ceiling you hit - see the table in [Pre-authentication IP throttle](#pre-authentication-ip-throttle) and the [Errors](https://vexioncards.mintlify.site/api-reference/errors.md) reference for the full code list.

## Handling 429 correctly

**1. Read the response headers**

Check `Retry-After` and `X-RateLimit-Remaining` on the `429` - they tell you exactly how long to wait and confirm which budget you ran out of.

**2. Wait the exact Retry-After duration, then re-read your budget**

Sleep for that many seconds before your next attempt. Don't apply your own exponential backoff on top of it - `Retry-After` is when your next single request will be accepted. It is not when your full budget returns, so read `X-RateLimit-Remaining` on that retry before sending anything more.

**3. Retry the same request**

Resend with the same idempotency key if the endpoint supports one, so a delayed retry can't create a duplicate.

**4. Escalate if it keeps happening**

Repeated `429`s at low volume usually mean too many parallel workers sharing one key. If you've ruled that out and still need more headroom, contact [support@vexioncards.one](mailto:support@vexioncards.one).

The snippets below implement steps 1-3:

```javascript Node.js theme={null}
async function requestWithRetry(url, options, attempt = 1) {
  const res = await fetch(url, options);

  if (res.status === 429) {
    const retryAfter = parseInt(res.headers.get('Retry-After') || '5', 10);
    if (attempt < 5) {
      await new Promise(r => setTimeout(r, retryAfter * 1000));
      return requestWithRetry(url, options, attempt + 1);
    }
  }

  return res;
}
```

```python Python theme={null}
import time
import requests

def request_with_retry(method, url, max_attempts=5, **kwargs):
    for attempt in range(1, max_attempts + 1):
        r = requests.request(method, url, **kwargs)
        if r.status_code != 429:
            return r
        retry_after = int(r.headers.get("Retry-After", "5"))
        time.sleep(retry_after)
    return r
```

> [!WARNING]
>
> **Do not** implement your own exponential backoff. Respect the `Retry-After` header: it reflects the exact time our system needs before it will accept another request.

## MCP server

The [MCP server](https://vexioncards.mintlify.site/ai/mcp-server.md) follows the same plan and limits as the REST API. When you send too much, the assistant receives "Too many requests. Slow down." with a wait time, and should pause. The MCP server adds its own limits on top:

| Limit | Detail |
| - | - |
| **Per connection or API key** | 120 requests per minute. |
| **Per IP address** | 1,200 requests per minute, shared by everyone behind the same address. |
| **Many failing API keys** | After about 100 failing API keys from one IP address within 15 minutes, further API key requests from that address get 429 with a `Retry-After` header until the 15 minutes end. OAuth sign-ins are not counted. |
| **Notifications** | 1 notification per minute and 50 per day for your business. The limit is shared by every connected AI app (per API key when a key is used). |
| **Reports** | A full report can use up to 30 of your 60 reads per minute. Wait a minute between large reports. See [Reports and large lists](https://vexioncards.mintlify.site/ai/mcp-tools.md#reports-and-large-lists). |

## Bulk operations

For large data imports or one-time syncs, prefer:

* Idempotency keys, which make retries free
* Off-peak hours (early morning UTC)
* Contact us for a temporary rate-limit increase

## Best practices

* **Cache** GET responses on your side. Most resources change less than once a day.
* **Poll intelligently.** For "has anything changed?" checks, use webhooks. See [Webhooks](https://vexioncards.mintlify.site/api-reference/webhooks.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.