Errors & Rate Limits
Error codes, response format, rate limits, and retry strategies.
Error format
All API errors return a consistent JSON body:
{
"error": "error_code",
"message": "Human-readable description",
"status": 400
}Some errors include additional fields:
| Field | Type | When present |
|---|---|---|
error_id | string (UUID) | Server errors (500+) — useful for support tickets |
details | object | Validation errors (400) — field-level error descriptions |
Example with details (validation error)
{
"error": "bad_request",
"message": "Validation failed",
"status": 400,
"details": {
"destination_url": "Invalid URL",
"custom_slug": "Slug must be 3–50 characters"
}
}Example with error_id (server error)
{
"error": "internal_error",
"message": "An unexpected error occurred",
"status": 500,
"error_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
}Status codes
Client errors
| Status | Code | Description |
|---|---|---|
400 | bad_request | Missing required field, malformed JSON, invalid URL |
401 | unauthorized | Missing or invalid API key |
403 | forbidden | API key lacks required scope |
404 | not_found | Invalid slug or deleted resource |
409 | conflict | Duplicate custom slug |
429 | rate_limited | Too many requests — see rate limits below |
Server errors
| Status | Code | Description |
|---|---|---|
500 | internal_error | Unexpected server error |
503 | service_unavailable | Temporary maintenance |
Rate limits
Link creation is rate-limited to 300 creations per hour per user. This limit applies across all of your API keys and dashboard combined — not per key. Both single (POST /api/v1/links) and bulk (POST /api/v1/links/bulk) endpoints share the same counter.
All other operations (read, revoke) are not rate-limited.
Bulk creation limits
The bulk endpoint accepts up to 500 links per request. Each link in the batch counts as one creation toward your hourly limit. If the batch would exceed the remaining quota, the entire request is rejected with a 429 and no links are created.
Rate limit headers
Link creation responses include rate limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per hour |
X-RateLimit-Used | Requests used in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds until window resets (only on 429 responses) |
X-RateLimit-Limit: 300
X-RateLimit-Used: 53
X-RateLimit-Reset: 1708099200Throttle your requests when X-RateLimit-Used approaches the limit.
Need a higher rate limit? Submit a support ticket describing your use case and we'll bump it up for you — no charge.
Retry strategy
For 429 and 5xx errors, use exponential backoff with jitter. When you receive a 429, the response includes a Retry-After header indicating how many seconds to wait before retrying.
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.ok) return res;
if ((res.status === 429 || res.status >= 500) && attempt < maxRetries) {
const retryAfter = res.headers.get("Retry-After");
const baseDelay = retryAfter ? parseInt(retryAfter, 10) * 1000 : 2 ** attempt * 1000;
const jitter = Math.random() * 500;
await new Promise((r) => setTimeout(r, baseDelay + jitter));
continue;
}
throw new Error(`Request failed: ${res.status}`);
}
throw new Error("Max retries exceeded");
}