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

# Get a job

> GET /v1/jobs/{job_id} — poll a queued resolve until it reaches a terminal status.

<Note>
  **`GET https://api.linkskipper.app/v1/jobs/{job_id}`** · Any valid API key.
</Note>

When [`POST /v1/resolve`](/docs/resolve) returns `202`, it gives you a `job_id`. Poll this
endpoint to track the resolve until it finishes. The job belongs to the API key that
created it — fetching another key's job returns `not_found` (`404`).

## Request

### Path parameters

<ParamField path="job_id" type="string" required>
  The job UUID returned by `POST /v1/resolve`. Must be a valid UUID; a malformed id returns
  `not_found` (`404`).
</ParamField>

### Headers

<ParamField header="Authorization" type="string" required>
  `Bearer sk_live_…`. See [Authentication](/docs/authentication).
</ParamField>

## Response

<ResponseField name="job_id" type="string">
  The job UUID.
</ResponseField>

<ResponseField name="status" type="string">
  One of `queued`, `running`, `done`, `failed`, `invalid`. See [Lifecycle](#job-lifecycle).
</ResponseField>

<ResponseField name="target_url" type="string">
  Present only when `status` is `done`. The resolved destination URL.
</ResponseField>

<ResponseField name="provider" type="string">
  Present only when `status` is `done`. The provider that owned the link (e.g. `ouo`).
</ResponseField>

<ResponseField name="tier" type="string">
  Present only when `status` is `done`. `"standard"` or `"premium"`.
</ResponseField>

<ResponseField name="error" type="string">
  Present only when `status` is `failed` or `invalid`. `resolve_failed` for a failed job,
  `unsupported_link` for an invalid one.
</ResponseField>

<ResponseField name="credits_charged" type="number">
  Credits spent on this job. The successful cost when `done`; `0` when `failed` / `invalid`.
</ResponseField>

<ResponseField name="balance" type="number | null">
  Your current credit balance.
</ResponseField>

## Examples

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.linkskipper.app/v1/jobs/9b1d7c0e-2f3a-4b5c-8d6e-1a2b3c4d5e6f \
    -H "Authorization: Bearer sk_live_XXXXXXXXXXXXXXXXXXXXXXXX"
  ```

  ```ts JavaScript theme={null}
  const job = await client.getJob("9b1d7c0e-2f3a-4b5c-8d6e-1a2b3c4d5e6f");

  switch (job.status) {
    case "done":
      console.log(job.targetUrl);
      break;
    case "queued":
    case "running":
      // keep polling
      break;
    default:
      console.error("failed:", job.error);
  }
  ```

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

  use LinkSkipper\Enum\JobStatus;

  $job = $client->getJob("9b1d7c0e-2f3a-4b5c-8d6e-1a2b3c4d5e6f");

  if ($job->status === JobStatus::Done) {
      echo $job->targetUrl, PHP_EOL;
  } elseif ($job->status->isTerminal()) {
      fwrite(STDERR, "failed: " . $job->error . PHP_EOL);
  }
  ```
</CodeGroup>

### Responses by status

<Tabs>
  <Tab title="done">
    ```json theme={null}
    {
      "job_id": "9b1d7c0e-2f3a-4b5c-8d6e-1a2b3c4d5e6f",
      "status": "done",
      "target_url": "https://example.com/final",
      "provider": "ouo",
      "tier": "standard",
      "credits_charged": 1,
      "balance": 248
    }
    ```
  </Tab>

  <Tab title="queued / running">
    ```json theme={null}
    {
      "job_id": "9b1d7c0e-2f3a-4b5c-8d6e-1a2b3c4d5e6f",
      "status": "running",
      "balance": 249
    }
    ```
  </Tab>

  <Tab title="failed">
    ```json theme={null}
    {
      "job_id": "9b1d7c0e-2f3a-4b5c-8d6e-1a2b3c4d5e6f",
      "status": "failed",
      "error": "resolve_failed",
      "credits_charged": 0,
      "balance": 249
    }
    ```
  </Tab>

  <Tab title="invalid">
    ```json theme={null}
    {
      "job_id": "9b1d7c0e-2f3a-4b5c-8d6e-1a2b3c4d5e6f",
      "status": "invalid",
      "error": "unsupported_link",
      "credits_charged": 0,
      "balance": 249
    }
    ```
  </Tab>
</Tabs>

## Job lifecycle

A job moves through these states. `queued`, `running` are non-terminal — keep polling.
`done`, `failed`, `invalid` are terminal — stop polling.

| Status    | Terminal | Meaning                                                                       |
| --------- | -------- | ----------------------------------------------------------------------------- |
| `queued`  | no       | Accepted and waiting for a worker.                                            |
| `running` | no       | A worker is resolving the link right now.                                     |
| `done`    | yes      | Resolved. `target_url`, `provider`, `tier`, `credits_charged` set.            |
| `failed`  | yes      | The resolver couldn't resolve it. `error` is `resolve_failed`.                |
| `invalid` | yes      | The link wasn't a supported/resolvable target. `error` is `unsupported_link`. |

```mermaid theme={null}
flowchart LR
  queued --> running
  running --> done
  running --> failed
  running --> invalid
```

## Polling guidance

* Poll on a short, fixed interval — **1.5–2 seconds** is a good default. Standard links
  typically resolve in seconds; premium (`exe.io`) links are best-effort and slower.
* Stop as soon as `status` is `done`, `failed`, or `invalid`.
* Set an overall deadline (e.g. 60–120s) so a stuck job doesn't poll forever.
* Reads count against your per-minute [rate limit](/docs/rate-limits) like any request — don't
  poll tighter than \~1s.
* Prefer [webhooks](/docs/webhooks) for high volume so you don't poll at all.

<Info>
  The SDKs implement this loop for you. `resolveAndWait` (JS / PHP) submits the resolve,
  polls until terminal, and returns the resolved link — throwing `JobFailedError` /
  `JobFailedException` on `failed` / `invalid` and `TimeoutError` / `TimeoutException` if the
  deadline passes. See [JavaScript](/docs/sdks/javascript) and [PHP](/docs/sdks/php).
</Info>
