# Errors (Developers)

URL: https://qrdx.dev/docs/developers/errors
Source: https://github.com/bucharitesh/qrdx/blob/main/apps/docs/content/docs/developers/errors.mdx

Every error the QRdx API returns, with its HTTP status, what it means and how to fix it.

{/* Generated by pnpm openapi:generate from public\/openapi.json. Edit lib\/openapi\/errors.ts, then run it again. */}

Every REST API error is JSON with the same shape, whichever endpoint returns it:

```json
{
  "code": "INSUFFICIENT_SCOPE",
  "message": "This token needs the 'codes:write' scope. Add it to your app and ask for it when the user authorizes.",
  "status": 403,
  "docs": "https://qrdx.dev/docs/developers/rest-api#endpoints"
}
```

| Field | What it is |
| --- | --- |
| `code` | A stable, machine-readable code from the table below. Branch on this. |
| `message` | What went wrong and how to fix it, for people. The wording may change. |
| `status` | The HTTP status, repeated. |
| `docs` | A page that explains the fix, when there is one. |
| `data` | Extra detail, on some errors. `VALIDATION_ERROR` lists each problem in `data.details`. |

A validation error names every field that failed:

```json
{
  "code": "VALIDATION_ERROR",
  "message": "Invalid request body",
  "status": 400,
  "data": {
    "details": [
      { "path": "destinationUrl", "message": "Enter a full URL, starting with https://" }
    ]
  }
}
```

The image API reports query parameters as `{ "param", "message" }` instead of `{ "path", "message" }`.

## Error codes [#error-codes]

| Status | Code | Meaning | Fix |
| --- | --- | --- | --- |
| 400 | `VALIDATION_ERROR` | The body, query or path failed validation. `data.details` lists each problem as `{ path, message }` (`{ param, message }` on the image API). | Correct the fields listed in `data.details` and send the request again. |
| 400 | `INVALID_SLUG` | The custom back-half (`slug`) breaks a rule: a reserved word, or the same back-half twice in one request. | Choose another back-half. The message names the rule. |
| 400 | `TOO_MANY_CODES` | More than 100 codes in one request. The body schema normally rejects this first, as `VALIDATION_ERROR`. | Send at most 100 codes per request. |
| 400 | `DOMAIN_NOT_VERIFIED` | `domainId` points to a custom domain that isn't verified yet. | Finish verifying the domain on the Domains page, or leave `domainId` out. |
| 400 | `SID_REQUIRED` | A conversion without the `qrdx_sid` of the scan (from the browser), or without `sid` or `code` (from a server). | Send the `qrdx_sid` the short link added to your page URL, or, with a token, the code id as `code`. |
| 401 | `UNAUTHORIZED` | No credentials: the request has no `Authorization: Bearer` header (and no QRdx browser session). | Send an OAuth access token as `Authorization: Bearer <token>`. |
| 401 | `INVALID_TOKEN` | The access token is unknown, expired (tokens last one hour) or revoked. | Get a new access token with your refresh token, or send the user through authorization again. |
| 402 | `PLAN_REQUIRED` | The feature isn't on the workspace's plan: bulk creation, custom back-halves and custom domains need Pro; conversions and hooks need Business. | Upgrade the workspace, or leave the feature out of the request. |
| 402 | `LIMIT_REACHED` | The workspace has as many active dynamic codes as its plan includes (10 on Free). | Archive a code (archived codes don't count) or upgrade to Pro for unlimited codes. |
| 403 | `INSUFFICIENT_SCOPE` | The token is valid but lacks the scope this endpoint needs. | Add the scope named in the message to your app and ask for it when the user authorizes. |
| 403 | `FORBIDDEN` | The caller may not do this: a team member deleting a code someone else created, a team member managing hooks, or a browser-session write from another site. | Ask a team owner or admin, or call the API with an access token from your server. |
| 403 | `CONVERSIONS_OFF` | A browser conversion for a code that doesn't have conversion tracking turned on. | Turn on conversion tracking for the code, or report the conversion from your server with a token. |
| 403 | `LIMIT_REACHED` | The workspace already has 20 webhook endpoints and hooks. | Delete an endpoint or hook you no longer use. |
| 404 | `NOT_FOUND` | No such endpoint, or the code, hook, theme or custom domain doesn't exist in the token's workspace. | Check the path and the id. Tokens act on the token owner's personal workspace, not on teams. |
| 404 | `INVALID_SID` | The `qrdx_sid` is unknown or older than 30 days, the attribution window. | Send the `qrdx_sid` of a recent scan, or report the conversion by code id from your server. |
| 409 | `SLUG_TAKEN` | Another code already uses that back-half on that domain. | Choose another back-half. |
| 413 | `PAYLOAD_TOO_LARGE` | The body is over the endpoint's limit: 4 MB for codes, 16 KB for hooks, 8 KB for conversions. | Send a smaller body. For codes, a large inline logo is the usual cause. |
| 415 | `UNSUPPORTED_MEDIA_TYPE` | A write without `Content-Type: application/json`. | Send a JSON body with `Content-Type: application/json`. |
| 422 | `UNSAFE_DESTINATION` | Google Web Risk flags the destination URL (malware, phishing or unwanted software). | Use another destination. If the flag is wrong, ask Google to review the site. |
| 429 | `RATE_LIMITED` | Too many requests: a per-minute limit (only `Retry-After` is set) or the workspace's daily quota (`Retry-After` plus `X-RateLimit-*`). | Wait `Retry-After` seconds. For the daily quota, wait for 00:00 UTC or upgrade the plan. |
| 500 | `INTERNAL_ERROR` | Something failed on QRdx's side. It has been logged. | Try again in a moment. If it keeps happening, report it with the time of the request. |
| 503 | `SLUG_UNAVAILABLE` | No free short-link slug could be reserved in time. | Send the same request again. |
| 503 | `UNAVAILABLE` | Custom domains aren't set up on this deployment (a self-hosted instance without them). | Leave `domainId` out, or configure custom domains on your instance. |

## Retrying [#retrying]

- **`429`:** wait the number of seconds in `Retry-After`, then send the same request. When the daily quota runs out, the `X-RateLimit-*` headers say how many requests the plan allows and when the count resets (00:00 UTC). See [rate limits](https://qrdx.dev/docs/developers/rest-api#rate-limits).
- **`500` and `503`:** retry with exponential backoff, starting at about a second.
- **Other `4xx`:** the same request fails the same way. Fix it first.

Creating codes isn't idempotent: if a `POST /v1/codes` times out, list your codes before sending it again, or you may create the code twice. Conversions are: send your own `id` and a repeat returns `200` with `duplicate: true`.

## `/v1/me` and `/v1/themes` [#v1me-and-v1themes]

These two endpoints send the same body plus the OAuth fields they have always sent, `error` and `error_description` (the same text as `message`), so older clients keep working:

| `error` | `code` |
| --- | --- |
| `invalid_token` | UNAUTHORIZED or INVALID_TOKEN |
| `insufficient_scope` | INSUFFICIENT_SCOPE |
| `rate_limited` | RATE_LIMITED (daily quota) |
| `not_found` | NOT_FOUND (themes) |

## OAuth endpoints [#oauth-endpoints]

The OAuth endpoints under `https://qrdx.dev/api/oauth` (authorize, token, revoke, userinfo) follow the OAuth 2.0 standard instead: `{ "error", "error_description" }`, with `error` one of `invalid_request`, `invalid_client`, `invalid_grant`, `unsupported_grant_type`, `invalid_token` or `access_denied`. Getting `invalid_grant` on a refresh means the refresh token was already used (each refresh returns a new one), expired after 30 days, or the user revoked your app: send the user through authorization again. See [REST API and OAuth](https://qrdx.dev/docs/developers/rest-api#get-an-access-token).

## Unknown endpoints [#unknown-endpoints]

A path under `/v1` that doesn't exist returns `404 NOT_FOUND` as JSON, never an HTML page. The [API reference](https://qrdx.dev/docs/developers/api) lists every endpoint.
