Skip to main content

API Rate Limits

Comprehensive guide to Trading Card API rate limits, response headers, and best practices for efficient API usage.

📊 Quick Overview

Your rate limit depends on which credential you authenticate with. Subscribers authenticate with the API key issued with their plan, and that key carries their plan's daily allowance:

CredentialAllowanceWindow
None — unauthenticated request100 requests60 minutes
OAuth 2.0 access token or Personal Access Token2,000 requests60 minutes
Subscriber API key — trial10,000 requests24 hours
Subscriber API key — Starter1,000 requests24 hours
Subscriber API key — Pro10,000 requests24 hours
Enterprise / customBy agreementBy agreement

Plan pricing is on our pricing page.

🚦 Rate Limiting Details

Which credential am I using?

Three kinds of credential can call the API, and they are limited differently.

Subscriber API key. Every subscription — including the 14-day trial — issues an opaque API key prefixed tc_live_ (or tc_test_ against staging). The plaintext key is shown once, when it is issued or regenerated — store it in a secret manager at that moment. It cannot be retrieved afterwards: GET /v1/user/api-key returns masked metadata only, and your account portal shows the same masked view. If you lose the key, regenerate it. This is the credential your plan's allowance is attached to: when you start a trial, subscribe, change plan, or regenerate the key, the new allowance is written onto the key and applies to your very next request.

OAuth 2.0 access tokens and Personal Access Tokens. Both are covered by the authentication guide, and both are limited separately from your API key, at the standard authenticated allowance of 2,000 requests per hour. Use them for the integration patterns that guide describes — they are not a way to serve plan-level traffic, and spreading one workload across credentials to exceed the volume your plan provides falls outside our fair use guidelines.

No credential. Unauthenticated requests are limited to 100 per hour, counted per client IP address.

Request Rate Limits

Each credential gets a single allowance covering all general API traffic:

  • Unauthenticated: 100 requests per 60 minutes, counted per IP address
  • OAuth token or Personal Access Token: 2,000 requests per 60 minutes, counted per token
  • Subscriber API key: your plan's allowance per 24 hours, counted per key
  • Enterprise / custom limits: set directly on your key or OAuth client (see Need Higher Limits? below)

There is no separate per-minute burst limit and no monthly quota — the allowance above is the only budget that applies to general API endpoints. You are free to spend it as quickly or as slowly as you like.

Authentication endpoints have their own limits

OAuth token endpoints and account signup carry their own, tighter limits that are tracked separately from your general API allowance. Token generation allows 10 requests per minute, token refresh 30 per 5 minutes, authorization 50 per 10 minutes, and client management 5 per hour. A 429 on /oauth/token therefore does not mean you have exhausted your /v1/cards budget, and vice versa. Retry logic should treat the two independently. These endpoints report their own usage in X-OAuth-RateLimit-* headers.

Rate Limit Enforcement

Rate limits are enforced using a fixed window:

  • The window opens on your first counted request and runs for its full length — 60 minutes or 24 hours, depending on your credential
  • Every request inside that window shares one reset boundary; the counter resets when the window expires, not continuously
  • The 24-hour plan window is not a calendar-day quota. It does not reset at midnight, in any timezone — it resets 24 hours after the request that opened it

📋 Rate Limit Headers

Every successful API response reports your current usage in two headers:

HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847

Header Descriptions

HeaderDescriptionExample
X-RateLimit-LimitMaximum requests allowed in the current window1000
X-RateLimit-RemainingRequests remaining in the current window847
Do not build retry logic on Retry-After

The API does not currently send Retry-After or X-RateLimit-Reset, on any response. A 429 carries no rate-limit headers at all, so there is no server-supplied value telling you when your window reopens.

Track X-RateLimit-Remaining from your successful responses to see a 429 coming, and back off with capped exponential backoff when you hit one. Code that reads Retry-After will silently fall through to whatever default it was given.

⚠️ Rate Limit Responses

When you exceed your allowance, the API returns a 429 Too Many Requests:

{
"errors": [
{
"status": "429",
"code": "TOO-MANY-REQUESTS",
"title": "Too many requests.",
"detail": "You have sent too many requests in a given amount of time. Please retry after a short delay."
}
],
"meta": {
"request_id": "01234567-89ab-cdef-0123-456789abcdef"
}
}

Include the meta.request_id value when contacting support about a specific response.

💳 Payment Required Responses

A 429 is not the only way access can stop. If your subscription is not in good standing, the data endpoints return 402 Payment Required instead:

{
"errors": [
{
"status": "402",
"code": "SUBSCRIPTION_REQUIRED",
"title": "Payment Required",
"detail": "An active subscription is required to access this endpoint. Subscribe or update your billing to restore access; your API key is preserved and access resumes automatically once your subscription is active."
}
]
}

When you get a 402. Your subscription is anything other than active or trialing — most commonly an expired trial, but also a failed payment (past_due), a cancellation, or a paused subscription. This is a distinct condition from rate limiting: waiting will not clear it, and retrying will return 402 every time until the billing problem is resolved.

Your API key survives. Nothing is revoked and nothing needs reissuing. Once the subscription is active again, the same key resumes working on the next request.

What stays reachable. The 402 applies to the /v1 and /v2 data endpoints. Your billing, subscription, account, and API-key endpoints are deliberately left reachable so you can fix the problem with the credentials you already have.

Handle 402 and 429 differently

Treat 429 as retryable with backoff, and 402 as terminal until a human acts — surface it to your operators or your user rather than retrying it. Code that lumps all 4xx responses into one retry path will spin against a 402 indefinitely.

💻 Code Examples

JavaScript (Node.js)

const axios = require('axios');

class TradingCardAPI {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseURL = 'https://api.tradingcardapi.com';
}

async makeRequest(endpoint, options = {}, attempt = 0) {
try {
const response = await axios({
url: `${this.baseURL}${endpoint}`,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Accept': 'application/vnd.api+json',
...options.headers
},
...options
});

// Log rate limit status
this.logRateLimit(response.headers);

return response.data;
} catch (error) {
// 402 is terminal — the subscription needs attention, so retrying
// will never clear it. Surface it instead of backing off.
if (error.response?.status === 402) {
throw new Error(
`Subscription inactive: ${error.response.data?.errors?.[0]?.detail ?? 'payment required'}`
);
}
if (error.response?.status === 429) {
return this.handleRateLimit(endpoint, options, attempt);
}
throw error;
}
}

logRateLimit(headers) {
// Only 2xx responses carry these; a 429 has no rate limit headers.
const limit = headers['x-ratelimit-limit'];
const remaining = headers['x-ratelimit-remaining'];

console.log(`Rate Limit: ${remaining}/${limit} requests remaining`);
}

async handleRateLimit(endpoint, options, attempt, maxAttempts = 5) {
if (attempt >= maxAttempts) {
throw new Error(`Rate limited: giving up after ${maxAttempts} attempts`);
}

// The API sends no Retry-After, so back off exponentially with jitter.
const base = Math.min(1000 * 2 ** attempt, 60000);
const delay = base + Math.random() * 1000;

console.warn(`Rate limited. Retrying in ${Math.round(delay / 1000)}s...`);
await new Promise(resolve => setTimeout(resolve, delay));

return this.makeRequest(endpoint, options, attempt + 1);
}
}

// Usage
const api = new TradingCardAPI('your-api-key');

// Get cards with automatic rate limit handling
const cards = await api.makeRequest('/cards', {
params: { 'page[limit]': 25 }
});

Python

import random
import requests
import time

class SubscriptionInactive(Exception):
"""Raised on a 402 — terminal until the billing problem is fixed."""

class TradingCardAPI:
def __init__(self, api_key):
self.api_key = api_key
self.base_url = 'https://api.tradingcardapi.com'
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {api_key}',
'Accept': 'application/vnd.api+json'
})

def make_request(self, endpoint, attempt=0, **kwargs):
response = self.session.get(f'{self.base_url}{endpoint}', **kwargs)

# Log rate limit status
self._log_rate_limit(response.headers)

# 402 is terminal — retrying will never clear it.
if response.status_code == 402:
detail = response.json()['errors'][0]['detail']
raise SubscriptionInactive(detail)

if response.status_code == 429:
return self._handle_rate_limit(endpoint, attempt, **kwargs)

response.raise_for_status()
return response.json()

def _log_rate_limit(self, headers):
# Only 2xx responses carry these; a 429 has no rate limit headers.
limit = headers.get('X-RateLimit-Limit')
remaining = headers.get('X-RateLimit-Remaining')

if limit and remaining:
print(f"Rate Limit: {remaining}/{limit} requests remaining")

def _handle_rate_limit(self, endpoint, attempt, max_attempts=5, **kwargs):
if attempt >= max_attempts:
raise RuntimeError(f"Rate limited: giving up after {max_attempts} attempts")

# The API sends no Retry-After, so back off exponentially with jitter.
delay = min(2 ** attempt, 60) + random.random()

print(f"Rate limited. Retrying in {delay:.1f}s...")
time.sleep(delay)

return self.make_request(endpoint, attempt=attempt + 1, **kwargs)

# Usage
api = TradingCardAPI('your-api-key')

# Get cards with automatic rate limit handling
cards = api.make_request('/cards', params={'page[limit]': 25})

PHP

<?php

class TradingCardAPI
{
private $apiKey;
private $baseUrl = 'https://api.tradingcardapi.com';

public function __construct($apiKey)
{
$this->apiKey = $apiKey;
}

public function makeRequest($endpoint, $params = [], $attempt = 0)
{
$url = $this->baseUrl . $endpoint;
if (!empty($params)) {
$url .= '?' . http_build_query($params);
}

$context = stream_context_create([
'http' => [
'method' => 'GET',
'header' => [
'Authorization: Bearer ' . $this->apiKey,
'Accept: application/vnd.api+json'
],
// Return the body on 4xx instead of false, so the error
// payload is readable.
'ignore_errors' => true
]
]);

$response = file_get_contents($url, false, $context);

if ($response === false) {
throw new Exception('API request failed');
}

$status = $this->statusCode($http_response_header);

// 402 is terminal — retrying will never clear it.
if ($status === 402) {
$error = json_decode($response, true)['errors'][0]['detail'] ?? 'payment required';
throw new Exception("Subscription inactive: {$error}");
}

if ($status === 429) {
return $this->handleRateLimit($endpoint, $params, $attempt);
}

$this->logRateLimit($http_response_header);

return json_decode($response, true);
}

private function statusCode($headers)
{
preg_match('{HTTP/\S+ (\d{3})}', $headers[0], $match);

return (int) ($match[1] ?? 0);
}

private function logRateLimit($headers)
{
// Only 2xx responses carry these; a 429 has no rate limit headers.
$rateHeaders = [];
foreach ($headers as $header) {
if (strpos($header, 'X-RateLimit-') === 0) {
list($key, $value) = explode(': ', $header, 2);
$rateHeaders[$key] = $value;
}
}

if (isset($rateHeaders['X-RateLimit-Remaining'], $rateHeaders['X-RateLimit-Limit'])) {
$remaining = $rateHeaders['X-RateLimit-Remaining'];
$limit = $rateHeaders['X-RateLimit-Limit'];
echo "Rate Limit: {$remaining}/{$limit} requests remaining\n";
}
}

private function handleRateLimit($endpoint, $params, $attempt, $maxAttempts = 5)
{
if ($attempt >= $maxAttempts) {
throw new Exception("Rate limited: giving up after {$maxAttempts} attempts");
}

// The API sends no Retry-After, so back off exponentially with jitter.
$delay = min(2 ** $attempt, 60) + (mt_rand(0, 1000) / 1000);

printf("Rate limited. Retrying in %.1fs...\n", $delay);
usleep((int) ($delay * 1_000_000));

return $this->makeRequest($endpoint, $params, $attempt + 1);
}
}

// Usage
$api = new TradingCardAPI('your-api-key');

// Get cards with automatic rate limit handling
$cards = $api->makeRequest('/cards', ['page[limit]' => 25]);
?>

🎯 Best Practices

1. Monitor Rate Limit Headers

These headers arrive on your successful responses, which makes them an early warning rather than an after-the-fact one. Watch them and slow down before you are throttled:

function checkRateLimit(response) {
const remaining = parseInt(response.headers['x-ratelimit-remaining']);
const limit = parseInt(response.headers['x-ratelimit-limit']);

if (remaining < limit * 0.1) { // Less than 10% remaining
console.warn('Approaching rate limit. Consider slowing down requests.');
}
}

2. Implement Exponential Backoff

Because a 429 carries no Retry-After, exponential backoff is the only correct retry strategy. Add jitter so multiple workers do not retry in lockstep:

async function exponentialBackoff(attempt, maxAttempts = 5) {
if (attempt >= maxAttempts) {
throw new Error('Max retry attempts exceeded');
}

const base = Math.min(1000 * Math.pow(2, attempt), 60000); // Max 60 seconds
const delay = base + Math.random() * 1000; // Jitter
await new Promise(resolve => setTimeout(resolve, delay));
}

Bear in mind how long a window is when you size maxAttempts. On a 24-hour plan window, exhausting your allowance means backoff will not recover you within any reasonable retry budget — fail the job and resume later rather than sleeping against a window that reopens tomorrow.

3. Cache Responses Intelligently

Reduce API calls by caching responses when appropriate:

class APICache {
constructor(ttl = 300000) { // 5 minutes default
this.cache = new Map();
this.ttl = ttl;
}

get(key) {
const item = this.cache.get(key);
if (!item) return null;

if (Date.now() > item.expires) {
this.cache.delete(key);
return null;
}

return item.data;
}

set(key, data) {
this.cache.set(key, {
data,
expires: Date.now() + this.ttl
});
}
}

4. Batch Requests When Possible

Use includes to fetch related data in single requests:

// ❌ Multiple requests
const card = await api.get('/cards/123');
const set = await api.get(`/sets/${card.relationships.set.data.id}`);
const player = await api.get(`/players/${card.relationships.oncard.data[0].id}`);

// ✅ Single request with includes
const cardWithRelations = await api.get('/cards/123?include=set,oncard');

5. Use Appropriate Page Sizes

Balance between fewer requests and manageable response sizes:

// ❌ Too many small requests
for (let page = 1; page <= 100; page++) {
await api.get(`/cards?page[number]=${page}&page[limit]=10`);
}

// ✅ Fewer larger requests
for (let page = 1; page <= 10; page++) {
await api.get(`/cards?page[number]=${page}&page[limit]=100`);
}

📜 Usage Policies

Fair Use Guidelines

These guidelines apply to every caller, on every plan.

Acceptable Use

  • Building trading card applications and tools
  • Personal collection management
  • Educational and research purposes
  • Commercial applications operating within their allowance

Prohibited Uses

  • Excessive automated scraping without proper rate limiting
  • Reselling raw API data without value addition
  • Bypassing rate limits through multiple accounts
  • Using the API for spam or malicious purposes

Commercial Use

Commercial and production applications are welcome on any plan. If your application needs a higher sustained volume, an SLA, or dedicated support, see our pricing page for available plans, or request a custom limit.

🔧 Troubleshooting

Common Rate Limiting Issues

Issue: Constant 429 Errors

// Problem: retrying on a fixed delay, or on a header that is never sent
const retryAfter = response.headers['retry-after']; // ❌ always undefined
setTimeout(() => retry(), 1000); // ❌ hammers the window

// Solution: exponential backoff with jitter, and a retry ceiling
const base = Math.min(1000 * Math.pow(2, attempt), 60000);
setTimeout(() => retry(attempt + 1), base + Math.random() * 1000); // ✅

Issue: Exhausting Your Allowance Too Quickly

// Problem: Sending requests too quickly
Promise.all(urls.map(url => api.get(url))); // ❌ All at once

// Solution: Control concurrency
async function limitConcurrency(urls, limit = 5) {
const results = [];
for (let i = 0; i < urls.length; i += limit) {
const batch = urls.slice(i, i + limit);
const batchResults = await Promise.all(batch.map(url => api.get(url)));
results.push(...batchResults);

// Small delay between batches
if (i + limit < urls.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
return results;
}

Issue: Unexpected Rate Limit Resets

  • Rate limits use a fixed window, not a rolling average
  • The window starts on your first counted request, so it rarely aligns with the top of the hour — and a 24-hour plan window never aligns with midnight
  • The API sends no reset timestamp, so track when your own window opened if you need to predict it

Issue: A 402, not a 429

  • Retrying will not help — see Payment Required Responses
  • Most often an expired trial or a failed payment; your API key is still valid

Error Handling Checklist

  • Check for 429 status codes in all API calls
  • Handle 402 separately — surface it, do not retry it
  • Implement exponential backoff with jitter, not a Retry-After read
  • Log rate limit headers from successful responses for monitoring
  • Set up alerts for approaching rate limits
  • Cache responses to reduce API calls
  • Use batch operations when available

📞 Need Higher Limits?

Upgrade your plan

If you are on Starter and hitting 1,000 requests per 24 hours, Pro raises that to 10,000. See the pricing page — a plan change takes effect on your existing key immediately, with no code change on your side.

Requesting a custom limit

Above Pro, we set a custom limit directly on your credentials. If 10,000 requests per 24 hours is not enough:

  1. Contact Support: Email [email protected]
  2. Provide Details:
    • Your API key ID or OAuth client ID
    • Current usage patterns
    • Expected future needs
    • Application description and business case
  3. We Adjust Your Credentials: approved accounts get a higher allowance applied to their existing key or client — again, no code change required

📊 Monitoring Your Usage

Usage Tracking Tips

  1. Log Rate Limit Headers: Track your usage patterns
  2. Set Up Alerts: Warn when approaching limits
  3. Monitor Response Times: Detect performance issues
  4. Track Error Rates: Identify problematic endpoints

Sample Monitoring Code

class UsageMonitor {
constructor() {
this.requests = [];
this.errors = [];
}

recordRequest(endpoint, headers) {
this.requests.push({
endpoint,
timestamp: Date.now(),
remaining: parseInt(headers['x-ratelimit-remaining']),
limit: parseInt(headers['x-ratelimit-limit'])
});
}

recordError(endpoint, status, error) {
this.errors.push({
endpoint,
status,
error: error.message,
timestamp: Date.now()
});
}

getUsageStats() {
const now = Date.now();
const lastHour = now - 3600000;

const recentRequests = this.requests.filter(r => r.timestamp > lastHour);
const recentErrors = this.errors.filter(e => e.timestamp > lastHour);

return {
requestsLastHour: recentRequests.length,
errorsLastHour: recentErrors.length,
errorRate: recentErrors.length / recentRequests.length,
mostRecentLimit: recentRequests[recentRequests.length - 1]?.remaining
};
}
}

🔄 Rate Limit Changes

Notification Process

We'll notify users of rate limit changes through:

  • Email announcements to registered users
  • API changelog documentation updates
  • GitHub releases for SDK updates
  • Blog posts for major changes

Backwards Compatibility

  • Rate limit increases: Applied immediately
  • Rate limit decreases: 30-day advance notice
  • A plan change you make yourself: applied to your existing key immediately
  • Emergency changes: Immediate with explanation

Quick Reference

CredentialAllowanceWindow
None — unauthenticated request100 requests60 minutes
OAuth 2.0 access token or Personal Access Token2,000 requests60 minutes
Subscriber API key — trial10,000 requests24 hours
Subscriber API key — Starter1,000 requests24 hours
Subscriber API key — Pro10,000 requests24 hours
Enterprise / customBy agreementBy agreement

X-RateLimit-Limit and X-RateLimit-Remaining on successful responses only. No Retry-After, no X-RateLimit-Reset. 429 means back off; 402 means check your subscription.

Need help? Contact [email protected] or check our support documentation.