Skip to main content
Documentation

Errors

The WarpLink API error response format, a reference of common error codes, and how to troubleshoot failed requests, including auth and rate limit errors.

All API errors return a consistent JSON structure.

Error Format

The details field is optional and appears on validation errors to indicate which fields failed.

Error Codes

HTTP StatusCodeDescription
400VALIDATION_ERRORRequest body or query parameters failed validation
400IMMUTABLE_FIELDAttempted to update a field that cannot be changed
400INVALID_IDThe provided ID is not a valid UUID
400INVALID_APP_IDThe app_id does not belong to your organization
400SLUG_UNCHANGEDThe new slug equals the link's current slug
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENThe key lacks the required scope. Attribution requests also return this when an API key is used instead of an SDK key
403SDK_KEY_ROUTE_FORBIDDENAn SDK key was used on an endpoint that is not an SDK endpoint, or on the MCP server. Use an API key for this request
403KEY_APP_MISMATCHAn SDK key tied to one app was used for a different app. Use the SDK key created for that app
403ATTRIBUTED_KEY_REQUIREDA subdomain rename through an API key that has no creator
403PASSWORD_REQUIREDThe link is password protected. Resolving it returns no destination and no platform URLs
404NOT_FOUNDThe requested resource does not exist
409SLUG_TAKENThe slug is already in use on that domain, or it is the old address of a moved link
409APP_HANDLE_TAKENThe subdomain already belongs to another app
409APP_HANDLE_HELDThe subdomain was just released and is held for 7 days. Only the app that released it can take it back before then
409CONFIRMATION_REQUIREDLinks use the old subdomain. Send confirm: true to rename
409APP_LINK_STATE_CHANGEDThe app's subdomain, default link domain or links changed since you read them. Read the app again and retry
409DEFAULT_DOMAIN_UNAVAILABLEThe app's saved default link domain is a custom domain that is no longer live. Name an allowed domain, or choose a new default
409RENAME_PERIOD_OPENAn earlier rename's old subdomain is still active
409RENAME_PERIOD_NOT_OPENA rename extension was requested with no open period
409RENAME_EXTENSION_LIMITBoth rename extensions are used
409LINK_MOVE_NOT_PENDINGThe link does not use an app's previous subdomain
409DUPLICATE_APPAn app with this bundle ID or package name exists
410GONEThe link has expired
429RATE_LIMIT_EXCEEDEDToo many requests
500INTERNAL_ERRORAn unexpected server error
503SERVICE_UNAVAILABLEA dependency is temporarily unreachable. Retry after the delay in the Retry-After header
422INVALID_DOMAINThe domain is not eligible to serve this link, or it is not allowed for the link, for example aplnk.to for an app created after subdomains, or a slug reserved on an app's own subdomain
409DOMAIN_CLAIMEDThis domain is already connected to another WarpLink account
409DUPLICATE_DOMAINYou have already added this domain
403PLAN_UPGRADE_REQUIREDYour plan does not include this feature or your custom domain trial has ended
403DOMAIN_LIMIT_EXCEEDEDYour plan's custom domain limit has been reached
403APP_REQUIREDThe organization has no registered app. Create an app, then create links
403LINK_LIMIT_EXCEEDEDYour plan's monthly link limit has been reached
403APP_LIMIT_EXCEEDEDYour plan's app limit has been reached
403SEAT_LIMIT_EXCEEDEDYour plan's team member limit has been reached
409ALREADY_MEMBERThis email is already a member of the organization
409DUPLICATE_INVITATIONAn active invitation already exists for this email
400MISSING_CONFIRMATIONOrganization deletion requires the X-Confirmation-Name header
403CONFIRMATION_MISMATCHThe confirmation name does not match the organization name
400PARAM_CONFIG_TOO_LARGEThe link parameter configuration exceeds the allowed size
400INVITATION_ALREADY_ACCEPTEDThis invitation has already been accepted
400INVITATION_ALREADY_REVOKEDThis invitation has already been revoked
400INVITATION_EXPIREDThis invitation has expired

Troubleshooting

"SERVICE_UNAVAILABLE" on requests that normally succeed

  • This is transient, not a problem with your request. Retry after the number of seconds in the Retry-After header (currently 5)
  • Reads are always safe to retry
  • For a write, the change may or may not have been applied before the failure. Read the resource back before retrying a create, or send a custom slug so a duplicate is rejected with SLUG_TAKEN instead of creating a second link

"UNAUTHORIZED" on every request

  • Verify your API key starts with wl_live_
  • Check the header format: Authorization: Bearer wl_live_YOUR_API_KEY
  • Ensure the key hasn't expired or been deleted

"FORBIDDEN" on attribution requests

  • Install attribution requires an SDK key. An API key cannot record installs, whatever scopes it holds
  • Create one at Keys > SDK keys and pass it to WarpLink.configure()
  • Telltale symptom: deep links resolve normally but no installs appear in your dashboard

"SDK_KEY_ROUTE_FORBIDDEN"

  • An SDK key, tied to one app or not, can call only GET /v1/sdk/validate, GET /v1/links/resolve/{slug} and POST /v1/attribution/match. It cannot list links, manage apps or connect to the MCP server
  • Create an API key under Keys > API keys and use it for scripts, CI and AI clients. Keep the SDK key in your mobile app

"KEY_APP_MISMATCH" on attribution requests

  • The SDK key is tied to one app, and the request came from another app of your organization, or from an app the organization does not have. The message is the same in both cases
  • Use the SDK key created for this app, under Keys > SDK keys. A key cannot be moved to another app after it is created, so create a new key for the right app
  • An SDK key works only with the WarpLink SDK. Any other endpoint, and the MCP server, answers it with 403 SDK_KEY_ROUTE_FORBIDDEN
  • The link has a password. GET /v1/links/resolve/{slug} returns no destination and no platform URLs for it, because the password is checked in the browser and never by the caller
  • Send the user to the short URL itself. The password form is served there, and a correct password redirects on to the destination
  • SDK 1.1.0 and later surface this as a named error: passwordRequired on iOS, PasswordRequired on Android, E_PASSWORD_REQUIRED on React Native, WarpLinkPasswordRequiredException on Flutter. Upgrade the SDK to get the named error.
  • A password-protected link resolves on the web page after the password is entered. See password protection
  • Check the domain's status in the dashboard. A custom domain that has stopped serving, such as one whose free trial ended, answers exactly as a missing link does
  • This is deliberate: a distinct code would confirm to any caller that a domain exists, so the API refuses to tell a suspended domain apart from an unknown one
  • Upgrading restores the domain, and the same request starts succeeding again. See custom domains
  • destination_url must include the protocol (https://)
  • slug must be 2-100 characters, lowercase alphanumeric with hyphens
  • expires_at must be an ISO 8601 date in the future
  • tags is limited to 20 items, each up to 50 characters
  • Custom slugs must be unique per domain. The error names the domain, and a slug that is the old address of a moved link counts as taken there
  • On an app's subdomain, a slug that one of the app's links on its old host still has is held for that link until it moves. The error names the link, and details.held_by holds its old domain and slug
  • Try a different slug or omit slug to auto-generate one
  • Reserved slugs (api, admin, dashboard, etc.) cannot be used