Rate limits

Request limits and how to handle 429s.

View as Markdown

VINR throttles API traffic to keep the platform fast and fair for every merchant. This page explains the per-environment limits, the headers that let you track your budget in real time, and how to back off cleanly when you hit a 429 Too Many Requests.

How limiting worksAsk

Limits are enforced per secret key using a token-bucket algorithm. Each bucket refills continuously at a fixed rate and holds a burst allowance on top, so short spikes are absorbed while sustained overuse is throttled. Read and write operations share the same key but are metered in separate buckets — a flood of GET /payments calls will not consume your capacity to create payments.

Limits apply to your account as a whole, not to individual IP addresses. Spreading requests across many servers or containers does not raise your ceiling.

Limits by environmentAsk

EnvironmentSustained rateBurstScope
Sandbox25 req/s50Per secret key
Production (standard)100 req/s200Per secret key
Production (scale)500 req/s1000Per secret key

Webhook delivery, hosted checkout sessions, and the dashboard are not counted against your API budget. Bulk endpoints such as list operations return up to 100 objects per page; paginate with the starting_after cursor rather than polling a single page in a tight loop.

The scale tier is enabled per account, not self-served. If a launch or migration needs headroom, request an increase before the event — see Next steps.

Rate-limit headersAsk

Every response includes headers describing your current budget. Read them on success as well as on 429, so you can throttle proactively instead of reacting to errors.

HeaderMeaning
X-RateLimit-LimitMaximum requests allowed in the current window
X-RateLimit-RemainingRequests left before throttling kicks in
X-RateLimit-ResetUnix epoch seconds when the bucket fully refills
Retry-AfterSeconds to wait before retrying (present only on 429)

A throttled response uses status 429 with a structured body:

{
  "error": {
    "type": "rate_limit_error",
    "code": "too_many_requests",
    "message": "Request rate exceeded. Retry after 2 seconds.",
    "retry_after": 2
  }
}

Backoff strategyAsk

When you receive a 429, wait at least the number of seconds in Retry-After, then retry with exponential backoff and jitter. Jitter prevents many clients from retrying in lockstep and re-triggering the limit. The SDK applies this automatically for idempotent operations, but here is the logic if you call the REST API directly.

The SDK retries 429 and 5xx responses transparently, honoring Retry-After. Tune the behavior at construction time:

import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({
  secretKey: process.env.VINR_SECRET_KEY,
  maxRetries: 5,        // attempts after the initial request
  retryBaseDelayMs: 250 // grows exponentially with jitter, capped at Retry-After
});

// No special handling needed — throttled calls are retried for you.
const payment = await vinr.payments.create({
  amount: 1000,
  currency: 'EUR',
  returnUrl: 'https://shop.example.com/return',
});
async function callWithBackoff(path: string, init: RequestInit, attempt = 0): Promise<Response> {
  const res = await fetch(`https://api.vinr.com/v1${path}`, {
    ...init,
    headers: { 'X-Api-Key': process.env.VINR_SECRET_KEY!, ...init.headers },
  });

  if (res.status !== 429 || attempt >= 5) return res;

  const retryAfter = Number(res.headers.get('Retry-After') ?? 1);
  const backoff = Math.min(2 ** attempt * 250, retryAfter * 1000);
  const jitter = Math.random() * backoff;
  await new Promise((r) => setTimeout(r, backoff + jitter));

  return callWithBackoff(path, init, attempt + 1);
}

Never retry in a tight while loop without delay. Hammering a throttled key keeps the bucket empty, extends your Retry-After, and can trip abuse protection.

Staying under the limitAsk

Read the remaining budget

Inspect X-RateLimit-Remaining on responses and slow down before it reaches zero, rather than waiting for the first 429.

Prefer webhooks over polling

Subscribe to events like payment.completed and invoice.paid instead of polling list endpoints. See Webhooks.

Batch and paginate

Use cursor pagination and request the largest page size you need, instead of issuing many small calls.

Use idempotency keys

Send an idempotencyKey on writes so a safe retry never creates a duplicate pay_ or re_ resource.

Next stepsAsk

Was this page helpful?