Rate limits & errors
Every error response, on every route, uses one JSON envelope. Rate limits are per surface: public link resolution, /match, and the key-authenticated routes each carry their own budget.
The error envelope
{
"statusCode": 404,
"error": "Not Found",
"message": "Link not found",
"timestamp": "2026-10-06T12:00:00.000Z",
"path": "/demo1/welcome1"
}
| Field | Contents |
|---|---|
statusCode | HTTP status, repeated in the body |
error | Status name, e.g. Payment Required |
message | Human-readable cause |
details | Present only on validation failures: one entry per bad field (below) |
timestamp | ISO 8601 UTC time of the response |
path | Request path that failed |
requestId | Your X-Request-Id request header, echoed when you send one |
Every response also carries an X-Request-Id header: your value if you sent one, otherwise a server-generated UUID. Send your own header to correlate a request across your logs and ours, and quote the value when contacting support about a failed call.
Validation errors
Requests that fail field validation return 400 with message: "Validation failed" and one entry per bad field:
{
"statusCode": 400,
"error": "Bad Request",
"message": "Validation failed",
"details": [
{ "message": "clipboardToken must be shorter than or equal to 100 characters" }
],
"timestamp": "2026-10-06T12:00:00.000Z",
"path": "/match",
"requestId": "0d9a6f1e-2b3c-4d5e-8f90-1a2b3c4d5e6f"
}
Structured error codes
Plan and workflow errors carry a machine-readable code next to the standard fields, so your code can branch without parsing messages:
| Status | code | Extra fields | Meaning |
|---|---|---|---|
402 | QUOTA_EXCEEDED | quota | A plan quota is exhausted; quota names it (e.g. "links") |
402 | PLAN_NOT_SELECTED | — | The organization has no plan selected yet |
403 | FEATURE_NOT_AVAILABLE | feature | The plan doesn't include the feature (e.g. custom_domains) |
422 | USE_BILLING_FLOW | billingEndpoint | A paid plan must be selected through checkout, not written directly |
428 | APP_CONFIG_REQUIRED | — | Register your app before creating the first link |
A quota error looks like this:
{
"statusCode": 402,
"error": "Payment Required",
"message": "You've reached your links limit.",
"code": "QUOTA_EXCEEDED",
"quota": "links",
"timestamp": "2026-10-06T12:00:00.000Z",
"path": "/links"
}
How to tell quota from auth problems:
401— the credential is missing, invalid, or the organization is suspended.403— the credential is valid but not permitted: the wrong key tier on the route, or a role below the route's minimum.FEATURE_NOT_AVAILABLE(with acode) is a plan limit, not a permission problem.402with acode— the account is fine; the plan is the limit. Limits per plan: Plans, quotas & entitlements.
Status codes
| Status | When you'll see it |
|---|---|
400 | Validation failed, or a malformed path parameter (e.g. an orgKey that isn't 4 alphanumeric characters) |
401 | Missing or invalid credential, or suspended organization |
402 | Plan limit reached (QUOTA_EXCEEDED, PLAN_NOT_SELECTED) |
403 | Wrong key tier, insufficient role, or feature not on the plan |
404 | Unknown resource: link, link ID, or domain |
409 | Conflict, e.g. a short code already in use in your organization |
410 | Link no longer resolves: deactivated, expired, or organization suspended (resolution routes) |
422 | Semantically invalid request (USE_BILLING_FLOW) |
428 | Predecessor step missing (APP_CONFIG_REQUIRED) |
429 | Rate limit exceeded (below) |
500 | Server-side failure; retry with backoff and include the requestId |
Rate limits
Limits are fixed and counted in sliding windows. A request counts against your API key on key-authenticated routes and against the request IP everywhere else.
| Surface | Limit | Counted per |
|---|---|---|
Link resolution: redirect page and /data | 500 requests/second | IP |
POST /match | 30 requests/minute | IP |
GET /links, GET /links/:id, QR endpoints | 1,000 requests/second | API key |
POST / PATCH / DELETE /links | 100 requests/second | API key |
/sdk/session, /sdk/identity, /sdk/identity/clear | 60 requests/minute | API key |
/sdk/events | 600 requests/minute | API key |
Portal routes (/portal/*) | 30 requests/second | IP |
The 429 response
{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded. Retry after 42 second(s).",
"timestamp": "2026-10-06T12:00:00.000Z",
"path": "/match"
}
There are no rate-limit response headers: no Retry-After, no remaining-count headers. The retry delay is only in the message string, so clients that back off automatically should parse the seconds out of Retry after N second(s).
The /match limit is deliberately the tightest: 30 attempts per minute per IP blunts brute-force enumeration of one-time match tokens.