Skip to main content
When a request fails, the API returns a standard RFC 7807 problem document with the Content-Type: application/problem+json and a non-2xx HTTP status. The body always includes a machine-readable code so you can branch on the failure without parsing prose.

Error envelope

type
string
A URI identifying the error type. Points at the docs section for the code.
title
string
A short, human-readable summary of the error type (e.g. Out of credits).
status
number
The HTTP status code, repeated in the body.
code
string
The stable, machine-readable error code. Branch on this. See the table below.
detail
string
A human-readable explanation specific to this occurrence. For invalid_request it carries the validation message(s).
balance
number | null
Present on out_of_credits — your current balance, so you can tell the user how short they are.
Example: out_of_credits
{
  "type": "https://linkskipper.app/developers/docs#out_of_credits",
  "title": "Out of credits",
  "status": 402,
  "code": "out_of_credits",
  "detail": "Account balance is empty. Buy credits to continue.",
  "balance": 0
}

Error codes

CodeHTTPTitleRetry?Meaning
invalid_request400Invalid requestNo — fix the requestMissing/unknown field, or a value over its limit.
invalid_key401Invalid API keyNo — fix the keyMissing, malformed, or revoked API key.
out_of_credits402Out of creditsNo — top upBalance below the link’s cost. Includes balance.
forbidden_scope403Forbidden scopeNo — grant the scopeThe key lacks the resolve scope.
not_found404Not foundNoUnknown job id, or a job that belongs to another key.
link_removed410Link removedNoThe destination was removed by the shortener.
unsupported_link422Unsupported linkNo — use a supported providerThe URL isn’t a supported shortener (or had no URL).
rate_limited429Rate limitedYes — after Retry-AfterPer-minute limit hit. Includes Retry-After.
quota_exceeded429Daily quota exceededYes — after Retry-AfterDaily quota reached. Resets at UTC 00:00. Includes Retry-After.
resolve_failed502Resolve failedMaybe — transientThe resolver couldn’t resolve the link.
provider_down503Provider unavailableYes — transientThe provider is temporarily unavailable.
rate_limited and quota_exceeded both use HTTP 429 and both send a Retry-After header. Distinguish them by the code field, not the status. See Rate limits.

Handling guidance

invalid_request, invalid_key, forbidden_scope, not_found, link_removed, and unsupported_link won’t succeed on retry. Surface them to the caller / logs and fix the input, key, scope, or URL. For unsupported_link, validate against /v1/providers before resolving.
Stop resolving and prompt a top-up. The balance field tells you how much is left. Top up in the dashboard. Nothing was charged.
Honor the Retry-After header (seconds) before retrying. For quota_exceeded, the window resets at UTC midnight — back off until then rather than hammering. See Rate limits.
provider_down (503) and many resolve_failed (502) cases are temporary. Retry with exponential backoff and a cap on attempts. If a queued job ends failed, the resolver genuinely couldn’t get through — don’t retry indefinitely.

Charging on errors

A request is only charged when it produces a resolved destination. Errors — including out_of_credits, unsupported_link, link_removed, provider_down, and resolve_failed — do not spend credits. Cached resolves also cost 0. See How it works.

Mapping in the SDKs

Both SDKs raise a typed exception per code, all subclasses of the base API error, so you can catch precisely or broadly.
import {
  ApiError,
  OutOfCreditsError,
  RateLimitedError,
  UnsupportedLinkError,
} from "@linkskipper/sdk";

try {
  await client.resolve("https://ouo.io/abc123");
} catch (error) {
  if (error instanceof OutOfCreditsError) {
    console.error("balance:", error.balance);
  } else if (error instanceof RateLimitedError) {
    console.error("retry after:", error.retryAfter);
  } else if (error instanceof UnsupportedLinkError) {
    console.error("not a supported shortener");
  } else if (error instanceof ApiError) {
    console.error(error.code, error.status, error.detail);
  } else {
    throw error;
  }
}
See the SDK error classes in JavaScript and PHP.