API Reference (Max only)
Rate Limits

Rate Limits

Every endpoint in the Seller API is rate-limited to protect both your server and ours. Limits are enforced per API key, not per IP address.

How it works

Each endpoint is assigned a tier (A, B, C, D) based on how expensive the underlying operation is. Each tier has its own token bucket:

TierPer minuteBurstTypical use
A6030Cheap single-resource reads (GET /v1/guild, GET /v1/products/{id})
B3010List reads with filters (GET /v1/products, GET /v1/customers)
C103Heavy reads (stats, invoices listing)
D205Mutations (create subscription, extend, cancel, patch discount)

The buckets are independent — heavy use of GET /v1/invoices (tier C) does not consume tier A budget.

In addition, every key has a global cap:

  • Soft cap: 1,000 requests per 5-minute window. Exceeding returns 429 rate_limited with retryAfterSeconds: 30.
  • Hard cap: 5,000 requests per 5-minute window. Exceeding returns 429 rate_limited with retryAfterSeconds: 60.

The global cap protects against runaway clients regardless of which endpoints they're hitting.

Response headers

Every successful response includes:

HeaderMeaning
X-RateLimit-LimitSteady-state requests per minute for this endpoint's tier.
X-RateLimit-RemainingTokens remaining in your bucket right now.
X-RateLimit-ResetSeconds until your bucket is fully refilled.

When you receive 429 rate_limited, the response also includes:

Retry-After: 7

This is the number of seconds to wait before retrying. The same value is repeated in error.details.retryAfterSeconds.

Tier per endpoint

EndpointMethodTier
/v1/guildGETA
/v1/productsGETB
/v1/products/{productId}GETA
/v1/customersGETB
/v1/customers/{discordUserId}GETA
/v1/subscriptionsGETB
/v1/subscriptionsPOSTD
/v1/subscriptions/{id}GETA
/v1/subscriptions/{id}/extendPOSTD
/v1/subscriptions/{id}/cancelPOSTD
/v1/invoicesGETC
/v1/invoices/{id}GETA
/v1/discountsGETB
/v1/discounts/{id}GETA
/v1/discounts/{id}PATCHD
/v1/stats/overviewGETC
/v1/stats/timeseriesGETC

Best practices

  • Cache aggressively. Most data changes infrequently. Cache product, customer, and discount details for at least a few minutes.
  • Honor Retry-After. Don't poll faster than the header says.
  • Use exponential backoff for 5xx. Combine with Retry-After for 429.
  • Batch your work. Prefer pagination over parallel requests when reading large datasets.
  • Stagger background jobs. If you sync hourly, randomize the start minute so retries don't pile up at the top of the hour.

Example: respecting Retry-After

const apiFetch = async (url, init = {}, attempt = 0) => {
  const res = await fetch(url, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.SUBSCORD_API_KEY}`,
      ...init.headers,
    },
  });
 
  if (res.status === 429 && attempt < 3) {
    const retryAfter = Number(res.headers.get("Retry-After") ?? 1);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return apiFetch(url, init, attempt + 1);
  }
 
  return res;
};