Skip to main content
Most of the SouthPay API is not rate limited today. The paths below are, and they are the ones a payment integration actually calls, so read this before you build a retry loop or a polling loop.

What is limited

Live mode. Test mode gets a quarter of the sustained rate on the credential-keyed policies, so a load test cannot spend your production budget. Reading payments, creating refunds, listing balances, running payouts, managing subscriptions and invoices, registering webhook endpoints, and the platform account endpoints have no enforced limit today. We are measuring them, and limits will be introduced there. Exact numbers will be published here, and announced in the changelog, at least 60 days before they are enforced. Build the backoff described below once and apply it everywhere so that change is a non-event for you.

The budget belongs to the store

The credential-keyed limits are counted against your store, not against the individual key. Issuing a second API key does not buy a second budget, and rotating a key does not reset the one you have. This is the same model Stripe and Modern Treasury use, and it is what lets us publish a number that means something. Two consequences worth designing around: Platforms are budgeted per sub-merchant. When you send X-Southpay-Account, the request is counted against that sub-merchant, not against your platform credential. One busy sub-merchant cannot exhaust the budget your other sub-merchants depend on. Test and live are separate buckets. They are different stores’ worth of budget in practice, so a load test in test mode never touches live traffic. The dashboard has its own budget. A person signed in to the SouthPay dashboard is counted separately from your API keys, and connected OAuth apps are counted separately again. A batch job that exhausts your API budget cannot lock a colleague out of the dashboard, and a busy dashboard cannot slow your integration down.

Shoppers are counted per checkout, not per address

The checkout endpoints a customer’s browser calls are budgeted per checkout reference, with a much higher per-IP ceiling behind it purely to stop abuse. This matters because shoppers share addresses. Mobile carrier NAT and office networks put hundreds of unrelated people behind one IP, and budgeting a checkout by address would let strangers throttle each other. A 429 on a checkout is the one throttle that costs a merchant money directly, so it is keyed to the thing that identifies a single shopper’s session. The practical effect: your customer’s checkout has its own budget, and nobody else can spend it.

How the limits behave

Each limit is a token bucket. The bucket refills continuously at the sustained rate, and the burst column is how many requests you can spend at once from a full bucket. There are no window boundaries and no clock alignment. A request is admitted the moment the bucket has a token for it, so you never need to guess when a window rolls over. Sustained traffic below the rate in the table never throttles, however it is distributed.
This replaced a fixed one minute window in August 2026. If you built against the old behaviour, the guidance to “wait until the top of the minute” no longer applies. Read Retry-After instead.

Reading your headroom

Every authenticated response carries your current budget, so you can pace yourself without waiting to be throttled.
X-RateLimit-Limit reports the burst bucket, not the per-minute rate, so that it and X-RateLimit-Remaining measure the same thing. The sustained rate lives in RateLimit-Policy as q per w seconds. In the example above you may hold 200 tokens at once, and they refill at 1,200 per minute.
X-RateLimit-Reset is delta-seconds, not a Unix timestamp. The RateLimit and RateLimit-Policy pair follows draft-ietf-httpapi-ratelimit-headers. That draft is not yet an RFC and its syntax has changed between revisions, so treat the X- headers as the stable contract and the draft pair as additive.

What a 429 looks like

Every throttled request returns the same envelope as the rest of the API, and always carries Retry-After.
Response 429
Retry-After is in seconds and is never less than 1. Waiting exactly that long is enough; you do not need to guess. A 429 with code: "too_many_failed_attempts" is different: it means too many requests from your address failed authentication. Fix the credential rather than retrying, then the budget clears on its own.
A 429 with code: "upstream_rate_limited" is not your limit. It means a market data provider throttled SouthPay. Branch on the code, not on the status alone, or your client will back off against a budget it never spent.

Backing off

A throttled request is rejected before it runs, so it changes nothing on our side. It creates no payment intent and consumes no idempotency key. Retrying is always safe.
southpay/retry.ts
Send the same Idempotency-Key on the retry. See Idempotency for what that guarantees.

Staying under the limits

Watch X-RateLimit-Remaining in batch jobs. It is the cheapest way to pace a migration or a nightly run. Slow down as it approaches zero rather than sprinting into a 429. Read onramp payment methods once per checkout and cache the result. It is the tightest limit on the API, and a 429 there interrupts a paying customer. Your server and the shopper’s browser each get their own budget on a checkout, but both count against that checkout’s overall ceiling. Cache your OAuth access token. Tokens are reusable until they expire. Refresh as expiry approaches, not on every call. Do not retry a 429 immediately. Read Retry-After and wait. Polling a payment for its status is not throttled today, so it will not produce a 429. It is still the wrong pattern. Subscribe with webhooks and you learn about the state change sooner.

Raising a limit

The limits are the same for every store. If your integration has a legitimate pattern that does not fit, contact support with the path, the rate you need and what drives it. Raising a limit is free.
Last modified on August 31, 2026