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

# Async jobs

> How long-running lookups work: wait inline, poll, or get a callback.

Some Croma lookups take longer to return, anywhere from a few seconds to a
couple of minutes. These run as **async jobs**: the same request can resolve
three different ways, and you choose which one fits your app.

<Note>
  **You don't have to do anything special.** By default these endpoints behave
  like any other: you `POST` and get `{ "data": … }` back. The options below
  are opt-in, for when you'd rather not hold a connection open.
</Note>

## Which endpoints are async?

These lookups run as async jobs:

**Colombia**

* [Consejo de Estado](/guides/colombia/consejo-estado), jurisprudence search and one providencia
* [Policía Nacional](/guides/colombia/policia), criminal records
* [ADRES](/guides/colombia/adres), health affiliation status
* [SICAAC](/guides/colombia/sicaac), insolvency cases
* [Superfinanciera](/guides/colombia/superfinanciera), complaints
* [RUNT](/guides/colombia/runt), vehicle by plate and vehicle history by plate
* [SIMIT](/guides/colombia/simit), account status
* [Contaduría](/guides/colombia/contaduria), state delinquent debtors
* [DIAN](/guides/colombia/dian), electronic document validation

**Peru**

* [SUNAT](/guides/peru/sunat), all lookups (RUC, document, name, taxpayers)
* [RREE](/guides/peru/rree), foreigner cards
* [SAT Lima](/guides/peru/sat-lima), account status and [capturas](/guides/peru/sat-lima-capturas)
* [Callao](/guides/peru/callao-papeletas), papeletas
* [SUTRAN](/guides/peru/sutran-infracciones), infracciones
* [APESEG](/guides/peru/apeseg-soat) and [SBS](/guides/peru/sbs-soat), SOAT

**Mexico**

* [SIEM](/guides/mexico/siem), business establishments

Every other endpoint answers synchronously, with no job involved.

## The three ways to get a result

| Mode                      | You send                           | You get back                                      |
| ------------------------- | ---------------------------------- | ------------------------------------------------- |
| **Wait inline** (default) | nothing extra, or `Prefer: wait=N` | `200 { data }` if it finishes in time, else `202` |
| **Poll**                  | `Prefer: wait=0`                   | `202` now, then `GET /jobs/:id` until done        |
| **Callback**              | `callback_url` in the body         | `202` now, then a `POST` to your URL when done    |

All three are backed by the **same job**, so pick per request.

<Note>
  If Croma already has a fresh result for the same query, any mode returns
  `200 { data }` right away (`X-Cache: HIT` header) and no job is created; in
  callback mode no `POST` follows. Always handle a direct `200`.
</Note>

***

## Mode 1: Wait inline (default)

Just call the endpoint. The request holds open until the job finishes (up to
**55 seconds**) and returns the result in the usual `{ data }` shape, identical
to a synchronous endpoint.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl https://api.croma.run/co/policia/criminal-records/v1 \
    -H "Authorization: Bearer $CROMA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "document_number": "1234567890" }'
  ```

  ```ts TypeScript theme={"dark"}
  const res = await fetch(
    "https://api.croma.run/co/policia/criminal-records/v1",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.CROMA_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ document_number: "1234567890" }),
    },
  );

  if (res.status === 200) {
    const { data } = await res.json(); // finished, use it
  } else if (res.status === 202) {
    // didn't finish in time; poll the status URL (see Mode 2)
  }
  ```
</CodeGroup>

Control how long to wait with the standard [`Prefer: wait=N`](https://www.rfc-editor.org/rfc/rfc7240#section-4.3)
header (seconds, clamped to **55**):

```bash theme={"dark"}
curl https://api.croma.run/co/policia/criminal-records/v1 \
  -H "Authorization: Bearer $CROMA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Prefer: wait=30" \
  -d '{ "document_number": "1234567890" }'
```

* **`200`**: finished. Body is `{ "data": … }`, same as the synchronous shape.
* **`202`**: not finished within the wait. Body is a [job envelope](#the-job-envelope);
  follow its `status_url` to poll. The `X-Job-Id` header carries the job id.

<Note>
  Always handle both `200` and `202`. A `202` is not an error; it just means
  "still working, come back for it."
</Note>

***

## Mode 2: Poll

Send `Prefer: wait=0` to get a `202` immediately, then `GET` the job's
`status_url` until it reaches a terminal state.

<CodeGroup>
  ```bash cURL theme={"dark"}
  # 1. Start the job (returns 202 immediately)
  curl -i https://api.croma.run/co/policia/criminal-records/v1 \
    -H "Authorization: Bearer $CROMA_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Prefer: wait=0" \
    -d '{ "document_number": "1234567890" }'

  # 2. Poll the status_url from the response (or Location header)
  curl https://api.croma.run/jobs/run_abc123 \
    -H "Authorization: Bearer $CROMA_API_KEY"
  ```

  ```ts TypeScript theme={"dark"}
  // 1. Start the job
  const start = await fetch(
    "https://api.croma.run/co/policia/criminal-records/v1",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.CROMA_API_KEY}`,
        "Content-Type": "application/json",
        Prefer: "wait=0",
      },
      body: JSON.stringify({ document_number: "1234567890" }),
    },
  );
  const { job } = await start.json();

  // 2. Poll until terminal
  async function poll(statusUrl: string) {
    while (true) {
      const res = await fetch(statusUrl, {
        headers: { Authorization: `Bearer ${process.env.CROMA_API_KEY}` },
      });
      const body = await res.json();
      if (body.job.status === "completed") return body.data;
      if (["failed", "canceled", "expired"].includes(body.job.status)) {
        throw new Error(body.error?.message ?? body.job.status);
      }
      // honor Retry-After; defaults to 2s while running
      const wait = Number(res.headers.get("Retry-After") ?? 2) * 1000;
      await new Promise((r) => setTimeout(r, wait));
    }
  }

  const data = await poll(job.status_url);
  ```
</CodeGroup>

`GET /jobs/:id` always returns `200` with the [envelope](#the-job-envelope).
While the job is still running it includes a `Retry-After` header (seconds);
use it to pace your polling. Jobs are scoped to your organization; a job
belonging to another org reads as `404`.

***

## Mode 3: Callback (webhook)

Include a `callback_url` in the request body. You get a `202` right away, and
Croma `POST`s the result to your URL once the job finishes, with no polling.

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl -i https://api.croma.run/co/policia/criminal-records/v1 \
    -H "Authorization: Bearer $CROMA_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
          "document_number": "1234567890",
          "callback_url": "https://your-app.com/webhooks/croma"
        }'
  ```

  ```ts TypeScript theme={"dark"}
  await fetch("https://api.croma.run/co/policia/criminal-records/v1", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.CROMA_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      document_number: "1234567890",
      callback_url: "https://your-app.com/webhooks/croma",
    }),
  });
  // → 202. The result arrives as a POST to your callback_url.
  ```
</CodeGroup>

`callback_url` must be an absolute **HTTPS** URL on a public host (localhost and
private ranges are rejected). When the job finishes, Croma sends a `POST` to it:

* **Body**: the same [job envelope](#the-job-envelope) as the poll endpoint.
* **`x-croma-job-id`**: the job id.
* **`x-croma-signature`**: `sha256=<hmac>`, an HMAC-SHA256 of the raw request
  body, so you can verify the payload's integrity.

```http theme={"dark"}
POST /webhooks/croma HTTP/1.1
content-type: application/json
x-croma-job-id: run_abc123
x-croma-signature: sha256=9f86d081…

{ "job": { "id": "run_abc123", "status": "completed", … }, "data": { … }, "error": null }
```

<Note>
  Signature verification uses a shared secret issued by Croma; reach out if you
  want it enabled for your callbacks. Respond `2xx` quickly; non-`2xx` responses
  are retried.
</Note>

***

## The job envelope

The `202` response, `GET /jobs/:id`, and the callback body all share one shape:

```json theme={"dark"}
{
  "job": {
    "id": "run_abc123",
    "status": "completed",
    "endpoint": "/co/policia/criminal-records/v1",
    "created_at": "2026-06-01T01:04:55.045Z",
    "finished_at": "2026-06-01T01:05:39.809Z",
    "status_url": "https://api.croma.run/jobs/run_abc123"
  },
  "data": { },
  "error": null
}
```

* **`data`** is populated only when `status` is `completed` (and matches the
  endpoint's normal `data` payload). Otherwise it's `null`.
* **`error`** is populated only when the job has `failed`, as
  `{ "type", "code", "message" }`.

### Statuses

| `status`    | Terminal? | Meaning                            |
| ----------- | --------- | ---------------------------------- |
| `queued`    | no        | Accepted, waiting to run.          |
| `running`   | no        | In progress.                       |
| `completed` | yes       | Done. Read `data`.                 |
| `failed`    | yes       | The job errored. Read `error`.     |
| `canceled`  | yes       | The run was canceled.              |
| `expired`   | yes       | The run expired before completing. |

## Headers reference

| Header                       | Where                  | Meaning                                                                                           |
| ---------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------- |
| `Prefer: wait=N`             | request                | Seconds to wait inline before returning `202`. Clamped to 55. `wait=0` returns `202` immediately. |
| `Preference-Applied: wait=N` | `200` response         | Echoes the wait that was applied.                                                                 |
| `Location`                   | `202` response         | The job's `status_url`.                                                                           |
| `Retry-After`                | `202` / running poll   | Suggested seconds before polling again.                                                           |
| `X-Job-Id`                   | `202` / `200` response | The job id.                                                                                       |

<Card title="Errors" icon="triangle-exclamation" href="/errors">
  How failures and the `error` object work.
</Card>
