> ## Documentation Index
> Fetch the complete documentation index at: https://linkskipper.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The RFC 7807 problem+json error envelope, every error code, and how to handle them.

When a request fails, the API returns a standard
[RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807) 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

<ResponseField name="type" type="string">
  A URI identifying the error type. Points at the docs section for the code.
</ResponseField>

<ResponseField name="title" type="string">
  A short, human-readable summary of the error type (e.g. `Out of credits`).
</ResponseField>

<ResponseField name="status" type="number">
  The HTTP status code, repeated in the body.
</ResponseField>

<ResponseField name="code" type="string">
  The stable, machine-readable error code. **Branch on this.** See the table below.
</ResponseField>

<ResponseField name="detail" type="string">
  A human-readable explanation specific to this occurrence. For `invalid_request` it carries
  the validation message(s).
</ResponseField>

<ResponseField name="balance" type="number | null">
  Present on `out_of_credits` — your current balance, so you can tell the user how short they
  are.
</ResponseField>

```json Example: out_of_credits theme={null}
{
  "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

| Code               | HTTP | Title                | Retry?                        | Meaning                                                           |
| ------------------ | ---- | -------------------- | ----------------------------- | ----------------------------------------------------------------- |
| `invalid_request`  | 400  | Invalid request      | No — fix the request          | Missing/unknown field, or a value over its limit.                 |
| `invalid_key`      | 401  | Invalid API key      | No — fix the key              | Missing, malformed, or revoked API key.                           |
| `out_of_credits`   | 402  | Out of credits       | No — top up                   | Balance below the link's cost. Includes `balance`.                |
| `forbidden_scope`  | 403  | Forbidden scope      | No — grant the scope          | The key lacks the `resolve` scope.                                |
| `not_found`        | 404  | Not found            | No                            | Unknown job id, or a job that belongs to another key.             |
| `link_removed`     | 410  | Link removed         | No                            | The destination was removed by the shortener.                     |
| `unsupported_link` | 422  | Unsupported link     | No — use a supported provider | The URL isn't a supported shortener (or had no URL).              |
| `rate_limited`     | 429  | Rate limited         | Yes — after `Retry-After`     | Per-minute limit hit. Includes `Retry-After`.                     |
| `quota_exceeded`   | 429  | Daily quota exceeded | Yes — after `Retry-After`     | Daily quota reached. Resets at UTC 00:00. Includes `Retry-After`. |
| `resolve_failed`   | 502  | Resolve failed       | Maybe — transient             | The resolver couldn't resolve the link.                           |
| `provider_down`    | 503  | Provider unavailable | Yes — transient               | The provider is temporarily unavailable.                          |

<Note>
  `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](/docs/rate-limits).
</Note>

## Handling guidance

<AccordionGroup>
  <Accordion title="4xx — your request needs a change" icon="hand">
    `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`](/docs/providers) before resolving.
  </Accordion>

  <Accordion title="402 — out of credits" icon="coins">
    Stop resolving and prompt a top-up. The `balance` field tells you how much is left. Top
    up in the [dashboard](https://linkskipper.app/developers/billing). Nothing was charged.
  </Accordion>

  <Accordion title="429 — slow down" icon="gauge">
    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](/docs/rate-limits).
  </Accordion>

  <Accordion title="5xx — transient, retry with backoff" icon="rotate">
    `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.
  </Accordion>
</AccordionGroup>

## 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](/docs/how-it-works#credits).

## 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.

<CodeGroup>
  ```ts JavaScript theme={null}
  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;
    }
  }
  ```

  ```php PHP theme={null}
  <?php

  use LinkSkipper\Exception\ApiException;
  use LinkSkipper\Exception\OutOfCreditsException;
  use LinkSkipper\Exception\RateLimitedException;
  use LinkSkipper\Exception\UnsupportedLinkException;

  try {
      $client->resolve("https://ouo.io/abc123");
  } catch (OutOfCreditsException $e) {
      fwrite(STDERR, "balance: " . $e->balance() . PHP_EOL);
  } catch (RateLimitedException $e) {
      fwrite(STDERR, "retry after: " . $e->retryAfter() . PHP_EOL);
  } catch (UnsupportedLinkException $e) {
      fwrite(STDERR, "not a supported shortener" . PHP_EOL);
  } catch (ApiException $e) {
      fwrite(STDERR, $e->errorCode()->value . " " . $e->status() . " " . $e->detail() . PHP_EOL);
  }
  ```
</CodeGroup>

See the SDK error classes in [JavaScript](/docs/sdks/javascript#errors) and [PHP](/docs/sdks/php#errors).
