API error codes
Batch endpoints that return partial success use HTTP 200 with per-item entries in failed (each entry includes code where applicable). See the developer overview for public API details.
Any endpoint
| Code | HTTP | Meaning | Where |
|---|---|---|---|
VALIDATION_FAILED | 400 | The request body is not valid: it failed schema validation; a POST, PATCH or DELETE under /api/ carried a body whose Content-Type is missing or not application/json; or a batch-delete or batch-patch body named no non-empty slug or link id, or more than 100 different ones. details[] lists the problems as strings. | Any endpoint taking a JSON body. The Content-Type check covers any POST, PATCH or DELETE under /api/ with a body, except POST /api/billing/webhook and requests with Authorization: Bearer on the routes that accept API keys (listed under ORIGIN_NOT_ALLOWED). |
UNAUTHORIZED | 401 | No valid session or API key. | Any endpoint requiring authentication |
ORIGIN_NOT_ALLOWED | 403 | A browser request whose Origin header is not the Linkonda site, or that has no Origin header and Sec-Fetch-Site: cross-site. Requests with neither header, such as curl or server-side clients, are not affected. | Any request under /api/ other than GET, HEAD or OPTIONS (in practice POST, PATCH and DELETE), except POST /api/billing/webhook. Requests with Authorization: Bearer are not checked on the routes that accept API keys: POST /api/links, /api/links/batch-create, /api/links/batch-delete, /api/links/batch-patch, and PATCH and DELETE /api/links/:slug. The browser extension's origin is also accepted on POST /api/links. |
NOT_FOUND | 404 | Unknown route, or a resource that does not exist or is not yours. | Any endpoint, including unmatched /api/* paths |
INTERNAL_ERROR | 500 | Unhandled server error. Safe to retry. | Any endpoint |
Authentication & API keys
| Code | HTTP | Meaning | Where |
|---|---|---|---|
INVALID_EMAIL_OR_PASSWORD | 401 | No account has this email, or the password is not the account's current password. | POST /api/auth/login |
PASSWORD_LOGIN_UNAVAILABLE | 401 | The account for this email has no password: it signs in with Google. Sign in with Google, or set a password with the reset email from POST /api/auth/forgot-password. | POST /api/auth/login |
EMAIL_ALREADY_REGISTERED | 409 | An account with this email already exists. | POST /api/auth/register |
EMAIL_VERIFICATION_TOKEN_INVALID | 400 | No account is waiting for this email verification token: it is wrong, already used, replaced by a newer one, or no longer needed because a Google sign-in verified the address. Signed in, request a new one with POST /api/auth/resend-verification. | POST /api/auth/verify-email |
EMAIL_VERIFICATION_TOKEN_EXPIRED | 400 | The email verification token has expired. Signed in, request a new one with POST /api/auth/resend-verification. | POST /api/auth/verify-email |
PASSWORD_RESET_TOKEN_INVALID | 400 | No account is waiting for this password reset token: it is wrong, already used, or replaced by a newer one. Request a new one with POST /api/auth/forgot-password. | POST /api/auth/reset-password |
PASSWORD_RESET_TOKEN_EXPIRED | 400 | The password reset token has expired. Request a new one with POST /api/auth/forgot-password. | POST /api/auth/reset-password |
GOOGLE_SIGN_IN_UNAVAILABLE | 404 | Google sign-in is not configured on this server; GET /api/auth/providers then answers google: false. | GET /api/auth/google/start, GET /api/auth/google/callback |
INVALID_API_KEY | 401 | Missing, empty, or unrecognized Bearer token. | Bearer-protected routes (e.g. quota, links, API keys). |
API_KEY_REVOKED | 401 | The API key was revoked. | Bearer-protected routes. |
API_KEY_REQUIRES_PAID | 403 | API keys are only available on paid plans. | Quota, links, API key routes when using Bearer auth. |
API_KEY_LIMIT_EXCEEDED | 403 | Maximum number of active keys for the plan is reached. | POST /api/api-keys |
EMAIL_NOT_VERIFIED | 403 | Account email must be verified for this action. | Session or Bearer routes that require a verified email, including POST /api/links/claim (session only). |
ACCOUNT_MISMATCH | 409 | The session belongs to a different account than the request's expectedUserId names. Nothing was changed. | POST /api/links/claim (session only) |
Quota & rate limits
| Code | HTTP | Meaning | Where |
|---|---|---|---|
QUOTA_EXCEEDED | 429 or 200 | The quota (active-link limit) of the account, or of anonymous links from this network, is reached; a 429 carries tier, maxActive and active. In batch-create failed[], the first link over the limit and every link after it have this code. Resuming a paused link is refused with this code when the account has no free slot; in batch-patch failed[], each paused link left without a slot has it. | POST /api/links and PATCH /api/links/:slug with paused: false (429). POST /api/links/batch-create and POST /api/links/batch-patch failed[] (HTTP 200). |
RATE_LIMITED | 429 | Too many requests in the current window, or the caller's daily allowance of destination checks is used up (until 00:00 UTC). Retry-After gives the seconds to wait. | POST /api/links, /api/links/batch-create, /api/links/batch-delete, /api/links/batch-patch and /api/links/claim; PATCH /api/links/:slug with url, abVariants or paused: false; POST /api/auth/login, /register, /verify-email, /resend-verification, /forgot-password and /reset-password; GET /api/auth/google/start and /google/callback. |
Links
| Code | HTTP | Meaning | Where |
|---|---|---|---|
INVALID_URL | 400 or 200 | Target URL failed validation: not an absolute http(s) URL, longer than 2,048 characters, or the host is not a resolvable name. | POST /api/links; PATCH /api/links/:slug; batch-create failed[] |
DESTINATION_BLOCKED | 400 or 200 | Destination refused: reported unsafe by Google Safe Browsing, or a private, loopback, or link-local address. | POST /api/links; PATCH /api/links/:slug; batch-create failed[] |
CUSTOM_SLUG_REQUIRES_PAID | 403 | A custom short code was requested by a free or anonymous caller. | POST /api/links |
CUSTOM_SLUG_UNSUPPORTED | 400 | Custom short codes are not implemented yet; omit slug to get a generated one. | POST /api/links (paid plans) |
CUSTOM_EXPIRY_NOT_ALLOWED | 400 or 200 | Custom expiry is not allowed for this tier. | POST /api/links; batch-create failed[] |
EXPIRES_IN_DAYS_OUT_OF_RANGE | 400 or 200 | expiresInDays is outside the allowed range; minDays/maxDays may be present. | POST /api/links; batch-create failed[] |
SLUG_ALLOCATION_FAILED | 500 or 200 | Could not allocate a unique short code after retries. | POST /api/links (500); POST /api/links/batch-create failed[] (200) |
NOT_FOUND | 200 or 404 | Slug or link id is not an owned, non-deleted link. | batch-delete and batch-patch failed[]; 404 on GET/PATCH/DELETE /api/links/:slug |
SLUG_AMBIGUOUS | 200 or 409 | More than one saved link shares this slug (platform vs custom domain). Disambiguate with linkId query on PATCH/DELETE, or send linkIds in batch bodies. | PATCH/DELETE /api/links/:slug (409); batch-delete and batch-patch failed[] (200) |
RESERVED_SLUG | 200 | Slug is reserved (e.g. infrastructure paths). | Batch delete/patch failed[] |
EDIT_DESTINATION_REQUIRES_PAID | 403 | Changing destination URL requires Plus, Pro, or Enterprise. | PATCH /api/links/:slug |
AB_REQUIRES_PAID | 400 or 403 | Weighted split destinations require Plus, Pro, or Enterprise (signed-in). | POST /api/links (abVariants), PATCH /api/links/:slug (abEnabled, abVariants, clearAb, etc.) |
AB_INVALID_WEIGHTS | 400 | Variant weights must sum to exactly 100. | POST /api/links and PATCH /api/links/:slug with abVariants |
AB_INVALID_VARIANTS | 400 | abVariants must have 2–12 unique keys (letters, numbers, _ or -). | POST /api/links and PATCH /api/links/:slug with abVariants |
AB_REQUIRES_TWO_VARIANTS | 400 | Cannot enable A/B until two variants exist; send abVariants with keys a and b. | PATCH /api/links/:slug with abEnabled: true |
AB_WINNER_NOT_FOUND | 400 | Declared winner variant is missing; configure variants first. | POST /api/links and PATCH /api/links/:slug with abWinnerVariant |
CUSTOM_DOMAIN_REQUIRES_AUTH | 403 | customDomainId requires a signed-in account. | POST /api/links |
CUSTOM_DOMAIN_REQUIRES_ACCOUNT | 403 | Custom domains require a free account or higher. | POST /api/links, POST /api/links/batch-create failed[] |
DOMAIN_NOT_OWNED | 403 | customDomainId does not belong to this account. | POST /api/links, batch-create |
DOMAIN_NOT_VERIFIED | 400 | Domain must be verified (DNS TXT) before creating links on it. | POST /api/links, batch-create |
Custom domains
| Code | HTTP | Meaning | Where |
|---|---|---|---|
CUSTOM_DOMAINS_REQUIRES_ACCOUNT | 403 | Custom domains require a free account or higher. | GET/POST/DELETE /api/custom-domains, POST …/verify |
DOMAIN_LIMIT_REACHED | 403 | Account has reached the max verified domains for the plan; max may be present. | POST /api/custom-domains |
INVALID_HOSTNAME | 400 | hostname is not a full hostname such as links.example.com: it is blank, contains /, :, ? or # (a scheme, port, path, query or fragment), can't be converted to ASCII, is longer than 253 characters, has a single label, or has an empty label or one longer than 63 characters. | POST /api/custom-domains |
HOSTNAME_RESERVED | 400 | That hostname is the platform short-link host. | POST /api/custom-domains |
HOSTNAME_TAKEN | 409 | Another account already registered this hostname. | POST /api/custom-domains |
DOMAIN_NOT_FOUND | 404 | Domain id is missing or not owned by this account. | GET/DELETE /api/custom-domains/:id, POST …/verify |
DOMAIN_IN_USE | 409 | Short links still reference this domain; delete or move them first. | DELETE /api/custom-domains/:id |
DOMAIN_VERIFICATION_FAILED | 400 | TXT record at _linkonda-challenge.<hostname> did not match the token. | POST /api/custom-domains/:id/verify |
DOMAIN_DISABLED | 400 | The domain is disabled, so it can't be verified. To use the hostname again, delete every short link on it, then delete the domain and add it again for a new verification token. Until its links are deleted, deleting the domain answers 409 DOMAIN_IN_USE. | POST /api/custom-domains/:id/verify |
No longer returned: CUSTOM_DOMAIN_REQUIRES_PRO and CUSTOM_DOMAINS_REQUIRES_PRO, from when custom domains needed Pro or Enterprise. The API answers CUSTOM_DOMAIN_REQUIRES_ACCOUNT and CUSTOM_DOMAINS_REQUIRES_ACCOUNT in their place.
Webhooks
| Code | HTTP | Meaning | Where |
|---|---|---|---|
WEBHOOK_REQUIRES_PAID | 403 | Webhooks require a paid plan (Plus, Pro, or Enterprise). | GET /api/webhooks/settings, PATCH /api/webhooks/settings |
WEBHOOK_INVALID_URL | 400 | Webhook URL is not a full https:// URL, contains a user name or password, or is longer than 2,048 characters. | PATCH /api/webhooks/settings |
WEBHOOK_URL_BLOCKED | 400 | Returned when saving, for localhost, a name without a domain or a non-public IP address. A delivery to a name that resolves to such an address is refused and recorded as a failed attempt; no error is returned. | PATCH /api/webhooks/settings |
Billing (signed-in)
| Code | HTTP | Meaning | Where |
|---|---|---|---|
BILLING_NOT_CONFIGURED | 503 | Checkout or billing is not configured on the server. | POST /api/billing/checkout, reactivate, portal |
UNAUTHORIZED | 401 | Sign-in required. | GET /api/billing/invoices; POST /api/billing/checkout, reactivate, portal (see also “Any endpoint”) |
ENTERPRISE_MANUAL | 400 | Enterprise plan changes are handled out of band. | POST /api/billing/checkout |
USE_PORTAL | 409 | An active subscription exists; use the billing portal to change plans. | POST /api/billing/checkout |
BILLING_PRICE_RESOLVE | 500 | Could not resolve a subscription price for the product. | POST /api/billing/checkout |
BILLING_CHECKOUT_FAILED | 500 | The payment provider returned a checkout session without a payment page. Safe to retry. | POST /api/billing/checkout |
NO_SUBSCRIPTION | 400 | Nothing to reactivate. | POST /api/billing/reactivate |
BILLING_REACTIVATE_FAILED | 400 | Provider rejected reactivation. | POST /api/billing/reactivate |
NO_CUSTOMER | 400 | No billing customer yet; subscribe first. | POST /api/billing/portal |