Consumption limits
Per-endpoint request limits for the Tidepay API, in sandbox and in production.
Every endpoint of the Tidepay API has a consumption limit. Limits apply per merchant, per endpoint, per environment — your test key and your live key never draw from the same allowance, so a runaway integration test can't consume the quota your production traffic depends on.
Two windows are enforced on every request:
- Per day — the consumption quota you plan against. The window is a fixed UTC day: it resets at 00:00 UTC, not 24 hours after your first call.
- Per minute — a burst guard. It stops a runaway loop from saturating the API before your daily quota would ever notice.
Each (endpoint, method) pair is its own bucket. Exhausting POST /v1/offers does not affect GET /v1/offers, and fetching one customer by id draws from the same bucket as fetching any other.
Sandbox vs. production
Sandbox limits are deliberately lower than production. Test traffic is integration work — bursty, but low in total volume — while live traffic carries real business. A sandbox key hitting its ceiling is usually a signal that a test loop is misbehaving.
Since both environments share one base URL, the environment is decided by which key you send (see Authentication). A tp_test_... key draws from the sandbox column below; a tp_live_... key draws from the production column.
Exceeding a limit
When either window is exhausted the API responds 429 Too Many Requests:
{ "error": "Rate limit exceeded" }with these headers:
| Header | Meaning |
|---|---|
Retry-After | Seconds until the exhausted window rolls over. |
X-RateLimit-Limit | The limit that was hit. |
X-RateLimit-Remaining | Always 0 on a 429. |
X-RateLimit-Reset | Unix timestamp (seconds) at which that window resets. |
X-RateLimit-Scope | Present and set to day when it was the daily quota that was exhausted. Absent when it was the per-minute burst guard. |
Check X-RateLimit-Scope before retrying. A missing scope means you hit the burst guard and Retry-After will be under a minute — a short backoff is enough. A day scope means your quota is spent until 00:00 UTC, and retrying in a loop will not recover it.
Blocked requests still count
A request rejected with 429 still increments the counter. Retrying
immediately does not restore your allowance — honor Retry-After.
Nothing is unblocked manually: both windows clear on their own schedule, so a blocked endpoint recovers without any action on your side.
Limits per endpoint
Currencies
| Endpoint | Production / day | Production / min | Sandbox / day | Sandbox / min |
|---|---|---|---|---|
GET /v1/currencies | 60,000 | 240 | 20,000 | 120 |
POST /v1/offers | 15,000 | 60 | 5,000 | 30 |
GET /v1/offers | 30,000 | 120 | 10,000 | 60 |
GET /v1/offers/{offerId} | 60,000 | 240 | 20,000 | 120 |
PATCH /v1/offers/{offerId} | 15,000 | 60 | 5,000 | 30 |
DELETE /v1/offers/{offerId} | 15,000 | 60 | 5,000 | 30 |
POST /v1/wallets | 15,000 | 60 | 5,000 | 30 |
GET /v1/wallets | 30,000 | 120 | 10,000 | 60 |
DELETE /v1/wallets/{id} | 15,000 | 60 | 5,000 | 30 |
POST /v1/customers | 15,000 | 60 | 5,000 | 30 |
GET /v1/customers | 30,000 | 120 | 10,000 | 60 |
GET /v1/customers/{customerId} | 60,000 | 240 | 20,000 | 120 |
PATCH /v1/customers/{customerId} | 15,000 | 60 | 5,000 | 30 |
DELETE /v1/customers/{customerId} | 15,000 | 60 | 5,000 | 30 |
POST /v1/subscriptions | 15,000 | 60 | 5,000 | 30 |
GET /v1/subscriptions | 30,000 | 120 | 10,000 | 60 |
GET /v1/subscriptions/{subscriptionId} | 60,000 | 240 | 20,000 | 120 |
PATCH /v1/subscriptions/{subscriptionId} | 15,000 | 60 | 5,000 | 30 |
DELETE /v1/subscriptions/{subscriptionId} | 15,000 | 60 | 5,000 | 30 |
POST /v1/subscriptions/{subscriptionId}/retry | 15,000 | 60 | 5,000 | 30 |
POST /v1/payments | 15,000 | 60 | 5,000 | 30 |
GET /v1/payments | 30,000 | 120 | 10,000 | 60 |
GET /v1/payments/{paymentId} | 60,000 | 240 | 20,000 | 120 |
POST /v1/payments/{paymentId}/cancel | 15,000 | 60 | 5,000 | 30 |
POST /v1/subscriptions/{subscriptionId}/migrate | 15,000 | 60 | 5,000 | 30 |
POST /v1/subscriptions/{subscriptionId}/resume | 15,000 | 60 | 5,000 | 30 |
GET /v1/webhooks/secret | 5,000 | 60 | 2,000 | 30 |
POST /v1/webhooks/secret/rotate | 200 | 10 | 200 | 10 |
POST /v1/sandbox/charge | 0 | 0 | 2,000 | 30 |
POST /v1/sandbox/advance | 0 | 0 | 2,000 | 30 |
All endpoints
| Endpoint | Production / day | Production / min | Sandbox / day | Sandbox / min |
|---|---|---|---|---|
GET /v1/currencies | 60,000 | 240 | 20,000 | 120 |
POST /v1/offers | 15,000 | 60 | 5,000 | 30 |
GET /v1/offers | 30,000 | 120 | 10,000 | 60 |
GET /v1/offers/{offerId} | 60,000 | 240 | 20,000 | 120 |
PATCH /v1/offers/{offerId} | 15,000 | 60 | 5,000 | 30 |
DELETE /v1/offers/{offerId} | 15,000 | 60 | 5,000 | 30 |
PATCH /v1/offers/batch | 5,000 | 20 | 2,000 | 10 |
DELETE /v1/offers/batch | 5,000 | 20 | 2,000 | 10 |
POST /v1/wallets | 15,000 | 60 | 5,000 | 30 |
GET /v1/wallets | 30,000 | 120 | 10,000 | 60 |
DELETE /v1/wallets/{id} | 15,000 | 60 | 5,000 | 30 |
POST /v1/customers | 15,000 | 60 | 5,000 | 30 |
GET /v1/customers | 30,000 | 120 | 10,000 | 60 |
GET /v1/customers/{customerId} | 60,000 | 240 | 20,000 | 120 |
PATCH /v1/customers/{customerId} | 15,000 | 60 | 5,000 | 30 |
DELETE /v1/customers/{customerId} | 15,000 | 60 | 5,000 | 30 |
POST /v1/subscriptions | 15,000 | 60 | 5,000 | 30 |
GET /v1/subscriptions | 30,000 | 120 | 10,000 | 60 |
GET /v1/subscriptions/{subscriptionId} | 60,000 | 240 | 20,000 | 120 |
PATCH /v1/subscriptions/{subscriptionId} | 15,000 | 60 | 5,000 | 30 |
DELETE /v1/subscriptions/{subscriptionId} | 15,000 | 60 | 5,000 | 30 |
POST /v1/subscriptions/{subscriptionId}/retry | 15,000 | 60 | 5,000 | 30 |
POST /v1/payments | 15,000 | 60 | 5,000 | 30 |
GET /v1/payments | 30,000 | 120 | 10,000 | 60 |
GET /v1/payments/{paymentId} | 60,000 | 240 | 20,000 | 120 |
POST /v1/payments/{paymentId}/cancel | 15,000 | 60 | 5,000 | 30 |
POST /v1/subscriptions/{subscriptionId}/migrate | 15,000 | 60 | 5,000 | 30 |
POST /v1/subscriptions/{subscriptionId}/resume | 15,000 | 60 | 5,000 | 30 |
GET /v1/webhooks/secret | 5,000 | 60 | 2,000 | 30 |
POST /v1/webhooks/secret/rotate | 200 | 10 | 200 | 10 |
POST /v1/sandbox/charge | 0 | 0 | 2,000 | 30 |
POST /v1/sandbox/advance | 0 | 0 | 2,000 | 30 |
POST /v1/sandbox/charge and POST /v1/sandbox/advance only accept test keys — a live key is rejected with 403 before any limit is consulted. See Test mode.
Endpoints without limits
These are outside the metered /v1 surface and are not counted against any quota:
GET /api/openapi.json— the public OpenAPI document.GET /api/health— the health probe.- The subscriber-facing checkout endpoints used by the hosted checkout page and the embed widget. Those carry no API key and are rate limited per IP address instead, since they're called by your payers' browsers rather than by your server.
Pagination
Every list endpoint (GET /v1/offers, /customers, /payments, /subscriptions) is offset-paginated: limit (max 100, default 20) and start (default 0). See Pagination for the full response shape, filters per endpoint, and how to detect the last page.
Idempotent creates
POST /v1/offers, /customers, and /subscriptions all accept an optional idempotencyKey in the request body (POST /v1/payments already requires one). Include a key you generate per logical operation — a UUID, or your own internal order id — and a retried request with the same key returns the original resource with 200 instead of creating a duplicate. This is what makes it safe to retry a POST after a timeout without knowing whether the first attempt actually landed.
The key is scoped to your merchant and to live/test mode, so a sandbox integration test can never collide with a live key. Omit idempotencyKey entirely and every call creates a new resource, exactly as before this field existed.
Need a higher limit?
The numbers above are defaults sized for typical integrations. If a legitimate workload needs more, get in touch before building around the limit — batching, caching offer and customer reads, or reacting to webhooks instead of polling usually removes the pressure entirely.
Prefer webhooks over polling
Polling GET /v1/subscriptions or GET /v1/payments on a timer is the most
common way to burn a daily quota. Tidepay pushes subscription.*,
payment.*, and wallet.* events to your endpoint as they happen — see
Webhooks.