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

CodeHTTPMeaningWhere
VALIDATION_FAILED400The 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).
UNAUTHORIZED401No valid session or API key.Any endpoint requiring authentication
ORIGIN_NOT_ALLOWED403A 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_FOUND404Unknown route, or a resource that does not exist or is not yours.Any endpoint, including unmatched /api/* paths
INTERNAL_ERROR500Unhandled server error. Safe to retry.Any endpoint

Authentication & API keys

CodeHTTPMeaningWhere
INVALID_EMAIL_OR_PASSWORD401No account has this email, or the password is not the account's current password.POST /api/auth/login
PASSWORD_LOGIN_UNAVAILABLE401The 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_REGISTERED409An account with this email already exists.POST /api/auth/register
EMAIL_VERIFICATION_TOKEN_INVALID400No 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_EXPIRED400The 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_INVALID400No 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_EXPIRED400The password reset token has expired. Request a new one with POST /api/auth/forgot-password.POST /api/auth/reset-password
GOOGLE_SIGN_IN_UNAVAILABLE404Google 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_KEY401Missing, empty, or unrecognized Bearer token.Bearer-protected routes (e.g. quota, links, API keys).
API_KEY_REVOKED401The API key was revoked.Bearer-protected routes.
API_KEY_REQUIRES_PAID403API keys are only available on paid plans.Quota, links, API key routes when using Bearer auth.
API_KEY_LIMIT_EXCEEDED403Maximum number of active keys for the plan is reached.POST /api/api-keys
EMAIL_NOT_VERIFIED403Account email must be verified for this action.Session or Bearer routes that require a verified email, including POST /api/links/claim (session only).
ACCOUNT_MISMATCH409The session belongs to a different account than the request's expectedUserId names. Nothing was changed.POST /api/links/claim (session only)

Quota & rate limits

CodeHTTPMeaningWhere
QUOTA_EXCEEDED429 or 200The 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_LIMITED429Too 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

CodeHTTPMeaningWhere
INVALID_URL400 or 200Target 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_BLOCKED400 or 200Destination 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_PAID403A custom short code was requested by a free or anonymous caller.POST /api/links
CUSTOM_SLUG_UNSUPPORTED400Custom short codes are not implemented yet; omit slug to get a generated one.POST /api/links (paid plans)
CUSTOM_EXPIRY_NOT_ALLOWED400 or 200Custom expiry is not allowed for this tier.POST /api/links; batch-create failed[]
EXPIRES_IN_DAYS_OUT_OF_RANGE400 or 200expiresInDays is outside the allowed range; minDays/maxDays may be present.POST /api/links; batch-create failed[]
SLUG_ALLOCATION_FAILED500 or 200Could not allocate a unique short code after retries.POST /api/links (500); POST /api/links/batch-create failed[] (200)
NOT_FOUND200 or 404Slug 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_AMBIGUOUS200 or 409More 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_SLUG200Slug is reserved (e.g. infrastructure paths).Batch delete/patch failed[]
EDIT_DESTINATION_REQUIRES_PAID403Changing destination URL requires Plus, Pro, or Enterprise.PATCH /api/links/:slug
AB_REQUIRES_PAID400 or 403Weighted split destinations require Plus, Pro, or Enterprise (signed-in).POST /api/links (abVariants), PATCH /api/links/:slug (abEnabled, abVariants, clearAb, etc.)
AB_INVALID_WEIGHTS400Variant weights must sum to exactly 100.POST /api/links and PATCH /api/links/:slug with abVariants
AB_INVALID_VARIANTS400abVariants must have 2–12 unique keys (letters, numbers, _ or -).POST /api/links and PATCH /api/links/:slug with abVariants
AB_REQUIRES_TWO_VARIANTS400Cannot enable A/B until two variants exist; send abVariants with keys a and b.PATCH /api/links/:slug with abEnabled: true
AB_WINNER_NOT_FOUND400Declared winner variant is missing; configure variants first.POST /api/links and PATCH /api/links/:slug with abWinnerVariant
CUSTOM_DOMAIN_REQUIRES_AUTH403customDomainId requires a signed-in account.POST /api/links
CUSTOM_DOMAIN_REQUIRES_ACCOUNT403Custom domains require a free account or higher.POST /api/links, POST /api/links/batch-create failed[]
DOMAIN_NOT_OWNED403customDomainId does not belong to this account.POST /api/links, batch-create
DOMAIN_NOT_VERIFIED400Domain must be verified (DNS TXT) before creating links on it.POST /api/links, batch-create

Custom domains

CodeHTTPMeaningWhere
CUSTOM_DOMAINS_REQUIRES_ACCOUNT403Custom domains require a free account or higher.GET/POST/DELETE /api/custom-domains, POST …/verify
DOMAIN_LIMIT_REACHED403Account has reached the max verified domains for the plan; max may be present.POST /api/custom-domains
INVALID_HOSTNAME400hostname 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_RESERVED400That hostname is the platform short-link host.POST /api/custom-domains
HOSTNAME_TAKEN409Another account already registered this hostname.POST /api/custom-domains
DOMAIN_NOT_FOUND404Domain id is missing or not owned by this account.GET/DELETE /api/custom-domains/:id, POST …/verify
DOMAIN_IN_USE409Short links still reference this domain; delete or move them first.DELETE /api/custom-domains/:id
DOMAIN_VERIFICATION_FAILED400TXT record at _linkonda-challenge.<hostname> did not match the token.POST /api/custom-domains/:id/verify
DOMAIN_DISABLED400The 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

CodeHTTPMeaningWhere
WEBHOOK_REQUIRES_PAID403Webhooks require a paid plan (Plus, Pro, or Enterprise).GET /api/webhooks/settings, PATCH /api/webhooks/settings
WEBHOOK_INVALID_URL400Webhook 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_BLOCKED400Returned 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)

CodeHTTPMeaningWhere
BILLING_NOT_CONFIGURED503Checkout or billing is not configured on the server.POST /api/billing/checkout, reactivate, portal
UNAUTHORIZED401Sign-in required.GET /api/billing/invoices; POST /api/billing/checkout, reactivate, portal (see also “Any endpoint”)
ENTERPRISE_MANUAL400Enterprise plan changes are handled out of band.POST /api/billing/checkout
USE_PORTAL409An active subscription exists; use the billing portal to change plans.POST /api/billing/checkout
BILLING_PRICE_RESOLVE500Could not resolve a subscription price for the product.POST /api/billing/checkout
BILLING_CHECKOUT_FAILED500The payment provider returned a checkout session without a payment page. Safe to retry.POST /api/billing/checkout
NO_SUBSCRIPTION400Nothing to reactivate.POST /api/billing/reactivate
BILLING_REACTIVATE_FAILED400Provider rejected reactivation.POST /api/billing/reactivate
NO_CUSTOMER400No billing customer yet; subscribe first.POST /api/billing/portal