# Error codes

> Stable error envelope and every code you may receive.

All errors share a consistent JSON envelope:

```json
{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "API key is invalid or no longer recognized.",
    "param": null,
    "request_id": "req_01J..."
  }
}
```

`request_id` is also echoed in the response headers; please quote it in support tickets.

## Auth errors

| Code | HTTP | Meaning |
| --- | --- | --- |
| `MISSING_AUTH` | 401 | No `Authorization` header sent. |
| `INVALID_API_KEY` | 401 | Key not found. |
| `REVOKED_API_KEY` | 401 | Key was revoked. |
| `WRONG_AUTH_TYPE` | 401 | Sent an API key to a dashboard endpoint, or a Clerk JWT to a product endpoint. |
| `INVALID_CLERK_TOKEN` | 401 | JWT failed signature, expiry, or issuer checks. |
| `ACCOUNT_NOT_FOUND` | 404 | Clerk user has no Nordva Launch account. Call `POST /v1/auth/provision`. |

## Validation and quota

| Code | HTTP | Meaning |
| --- | --- | --- |
| `VALIDATION_ERROR` | 400 | Body or query fails schema. `param` names the offending field. |
| `INVALID_PLAN` | 400 | Plan name not recognised (must be `indie` or `builder`). |
| `RATE_LIMITED` | 429 | Plan rate limit exceeded. `Retry-After` header included. |
| `PLAN_LIMIT_REACHED` | 403 | Monthly quota exhausted (for notifications: the project's notifications for this calendar month, UTC). Upgrade or wait for the cycle to reset. |
| `PLAN_UPGRADE_REQUIRED` | 403 | Feature gated to a higher plan. |
| `NOT_FOUND` | 404 | Resource missing or owned by another project. |
| `CANNOT_EDIT_PUBLISHED` | 409 | Tried to edit a published changelog entry. |
| `HOSTNAME_ALREADY_REGISTERED` | 409 | Custom hostname already registered to another project. |
| `IDEMPOTENCY_CONFLICT` | 409 | Same `Idempotency-Key` used with a different request body. |

## Billing

| Code | HTTP | Meaning |
| --- | --- | --- |
| `BILLING_NOT_CONFIGURED` | 503 | Server-side Paddle environment is not configured. |
| `NO_BILLING_ACCOUNT` | 404 | Account has no Stripe customer yet: complete a checkout first. |
| `BILLING_PORTAL_UNAVAILABLE` | 502 | Paddle portal-session API call failed. Retry shortly. |

## Custom domains

| Code | HTTP | Meaning |
| --- | --- | --- |
| `CUSTOM_DOMAIN_NOT_CONFIGURED` | 503 | Server-side Cloudflare for SaaS env not configured. |
| `CUSTOM_DOMAIN_LIMIT_REACHED` | 403 | Reached the per-plan custom hostname limit. |

## Publishable keys

| Code | HTTP | Meaning |
| --- | --- | --- |
| `KEY_INSUFFICIENT_PERMISSIONS` | 403 | A publishable key (`nv_pub_*`) was used on a secret-only endpoint. Publishable keys are browser-safe and may only write to the public endpoints listed in [Authentication](/authentication#api-keys-product-endpoints); list, read, delete, and admin operations require a secret key (`nv_live_*` / `nv_test_*`). |
| `API_KEY_LIMIT_REACHED` | 422 | Project already has the per-plan maximum of active keys of the requested type. Secret and publishable keys are counted independently. Revoke an unused key of the same type, or upgrade the plan. |

## Project slug

| Code | Status | Meaning |
|---|---|---|
| `INVALID_SLUG` | 422 | Slug is not 1–40 lowercase letters, numbers and hyphens. |
| `SLUG_TAKEN` | 409 | The slug is in use, reserved, or was released by another project in the last 30 days. |
| `INVALID_WEBHOOK_URL` | 422 | A webhook URL must be a public `https://` URL: no IP address, internal hostname, credentials or custom port. `message` says which rule it broke. |
| `WEBHOOK_LIMIT_REACHED` | 403 | The project already has as many webhook endpoints as the plan includes (Indie 2, Builder 5). |
| `PROJECT_LIMIT_REACHED` | 403 | Dashboard only. The account already has as many projects as its plan includes (Free 2, Indie 4, Builder 10). |
| `LAST_PROJECT` | 409 | Dashboard only. An account keeps at least one project, so its only project cannot be deleted. |
| `PROJECT_NOT_FOUND` | 404 | Dashboard only. The project named in `X-Nordva-Project`, or chosen when approving a CLI sign-in, is not one of this account's. |
| `AFFILIATE_EXISTS` | 409 | Dashboard only. The account has already applied to the affiliate program. |
| `AFFILIATE_NOT_FOUND` | 404 | Dashboard only. The account has not joined the affiliate program. |
| `AFFILIATE_INVITE_INVALID` | 404 | Dashboard only. The affiliate invite link is unknown or was already used. |
| `SLUG_CHANGE_LIMIT_REACHED` | 429 | The slug was already changed twice in the last 30 days. A project's first rename, away from the generated slug, is not counted. `remediation` gives the date it can change again. Old links keep working in the meantime. |

## Revenue, contacts and waitlist emails

| Code | Status | Meaning |
|---|---|---|
| `STRIPE_NOT_CONNECTED` | 409 | No payment account (Stripe or Lemon Squeezy) is connected to this project. Connect one in the dashboard. The name is older than Lemon Squeezy support. |
| `STRIPE_ALREADY_CONNECTED` | 409 | Disconnect the current key before connecting another. |
| `STRIPE_KEY_NOT_RESTRICTED` | 422 | The key is not a restricted key (`rk_live_` / `rk_test_`). If a secret key was pasted, roll it in Stripe. |
| `STRIPE_KEY_INVALID` | 422 | Stripe did not recognise the key. |
| `STRIPE_KEY_MISSING_PERMISSION` | 422 | A required read permission is missing. `remediation` carries Stripe's message naming it. |
| `STRIPE_CONNECTION_LIMIT_REACHED` | 403 | Free and Indie include one Stripe connection per account; Builder one per project. |
| `LEMONSQUEEZY_NOT_CONNECTED` | 409 | No Lemon Squeezy store is connected to this project. |
| `LEMONSQUEEZY_ALREADY_CONNECTED` | 409 | This project already has a Lemon Squeezy store connected. Disconnect it first. |
| `LEMONSQUEEZY_KEY_INVALID` | 422 | Lemon Squeezy did not accept the API key: expired, deleted or copied incompletely. |
| `LEMONSQUEEZY_KEY_MISSING_ACCESS` | 422 | Lemon Squeezy refused one of the reads Launch needs. The message says which. |
| `LEMONSQUEEZY_STORE_NOT_FOUND` | 422 | The store is not one the key can see. A test mode key only sees test data. |
| `LEMONSQUEEZY_CONNECTION_LIMIT_REACHED` | 403 | Free and Indie include one Lemon Squeezy store per account; Builder one per project. |
| `STRIPE_CONNECT_NOT_CONFIGURED` | 503 | Server-side encryption key for Stripe connections is not configured. |
| `CONTACT_NOT_FOUND` | 404 | No contact with that id in this project. |
| `EMAIL_TEMPLATE_INVALID` | 422 | Unknown placeholder, missing required placeholder, or empty/too long subject or body. `param` is `subject` or `body`. |

## Notifications

| Code | HTTP | Meaning |
| --- | --- | --- |
| `USER_HASH_INVALID` | 403 | A browser call to a notification bell endpoint sent no `user_id` or `user_hash`, a hash that does not match the user id, or the project has no signing secret yet. See [Identity verification](/guides/notifications-identity/). |
| `SIGNING_SECRET_ALREADY_SHOWN` | 409 | The project's notifications signing secret already exists and was shown once when it was made. Rotate it to get a new one; the old one keeps working for 24 hours. |

## Analytics

| Code | HTTP | Meaning |
| --- | --- | --- |
| `ANALYTICS_UNAVAILABLE` | 503 | The analytics store could not be read right now. Collection keeps working; try again in a minute. |
| `ANALYTICS_RAW_EVENTS_ONLY` | 400 | The read follows single visitors or filters by a dimension (visitor lists, one visitor, funnels, hourly buckets, filters), which needs raw events, kept 90 days. Ask for `period_days` of 90 or fewer. Totals, time series, breakdowns, goals and signups reach back over your plan's whole history. With `param: "exclude_country"`: a breakdown past 90 days cannot leave countries out; ask for 90 days or fewer, or send `exclude_country=` to count every country. |

## System

| Code | HTTP | Meaning |
| --- | --- | --- |
| `INTERNAL_ERROR` | 500 | Unexpected server error. Include the `request_id` in your report. |
| `TURNSTILE_FAILED` | 400 | Cloudflare Turnstile token verification failed. |

If you hit a code not listed here, write to [support@nordva.dev](mailto:support@nordva.dev) with the `request_id` from the response. Codes are never renamed; new ones are added to this page.

Source: https://docs.nordva.dev/api-reference/errors/
