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

# Working with async results

> The Synthpop API is asynchronous. Choose polling or an opt-in callback to learn when a Task finishes.

## Why results are asynchronous

Processing a Task — reading documents, extracting structured records, and
validating them — takes longer than a single HTTP request should wait. So the API
is **asynchronous**: `POST /task/create` returns immediately with the Task's
initial details, `status: "pending"`, and an empty `verdicts` list. The result
does not exist yet.

You learn the outcome one of two ways:

* **Option A — poll `GET /task/get`** until the Task reaches a terminal status.
* **Option B — supply a `callback_template`** at create time so Synthpop calls
  you when the Task finishes (opt-in, org-gated).

Both observe the same thing: the Task's `status` moving toward a terminal value.

## Task statuses

A Task's `status` is one of the values below. Three are **terminal** — once a Task
reaches one, it will not move again on its own.

| Status       | Terminal? | Meaning                                                                   |
| ------------ | --------- | ------------------------------------------------------------------------- |
| `pending`    | No        | Accepted, waiting to be processed.                                        |
| `processing` | No        | Currently being processed.                                                |
| `waiting`    | No        | Paused — needs more input from you (see below).                           |
| `completed`  | Yes       | Finished processing.                                                      |
| `failed`     | Yes       | An error occurred during processing.                                      |
| `invalid`    | Yes       | The uploaded data provides no information relevant to the requested task. |

<Warning>
  A Task can stop at `waiting` instead of reaching a terminal status. `waiting`
  means Synthpop needs more documents or data before it can continue — it is not
  a failure and it is not terminal. A polling loop that only checks for terminal
  statuses will spin forever on a `waiting` Task. Handle `waiting` explicitly; see
  [Responding to waiting Tasks](/guides/responding-to-waiting-tasks).
</Warning>

## Option A: poll `GET /task/get`

Retrieve a Task by **either** its Synthpop `uuid` **or** your own `external_id` —
exactly one, never both:

* Supplying **neither or both** returns `400`.
* A well-formed identifier that matches **no** Task returns `404`.

Poll on an interval (with backoff), and stop when `status` is terminal — or branch
to your resume logic when it is `waiting`.

<CodeGroup>
  ```bash cURL theme={null}
  # By Synthpop UUID
  curl -s "https://app.synthpop.ai/public/v1/task/get?uuid=$TASK_UUID" \
    -H "Authorization: Bearer $SYNTHPOP_JWT_TOKEN"

  # ...or by your own correlation key
  curl -s "https://app.synthpop.ai/public/v1/task/get?external_id=order-4821" \
    -H "Authorization: Bearer $SYNTHPOP_JWT_TOKEN"
  ```

  ```python Python theme={null}
  import time
  import requests

  BASE_URL = "https://app.synthpop.ai/public/v1"
  TERMINAL = {"completed", "failed", "invalid"}


  def wait_for_task(task_uuid: str, jwt_token: str) -> dict:
      headers = {"Authorization": f"Bearer {jwt_token}"}
      delay = 2.0
      while True:
          resp = requests.get(
              f"{BASE_URL}/task/get",
              params={"uuid": task_uuid},
              headers=headers,
          )
          resp.raise_for_status()
          task = resp.json()
          status = task["status"]

          if status in TERMINAL:
              return task
          if status == "waiting":
              # The Task needs more input before it can continue; hand it
              # back and let the caller resume it.
              return task

          time.sleep(delay)
          delay = min(delay * 1.5, 30.0)  # back off, cap at 30s
  ```
</CodeGroup>

<Tip>
  Read [`GET /task/get`](/api-reference/introduction) results only after the Task
  reaches a terminal status. On a `completed` validation Task you will find
  per-code `verdicts`, plain-English `verdict_summaries`, and any `issues`. See
  [Results](/concepts/results) for the response shape.
</Tip>

## Option B: callback on completion

Instead of polling, pass a `callback_template` when you create the Task. It is a
URL pattern that Synthpop issues a **GET** request against when the Task finishes
processing. Two placeholders are expanded in the URL:

| Placeholder     | Expands to                                                                                |
| --------------- | ----------------------------------------------------------------------------------------- |
| `{uuid}`        | The Task's Synthpop UUID.                                                                 |
| `{external_id}` | The percent-encoded `external_id`, or the literal string `"null"` if you did not set one. |

```bash cURL theme={null}
curl -X POST "https://app.synthpop.ai/public/v1/task/create" \
  -H "Authorization: Bearer $SYNTHPOP_JWT_TOKEN" \
  -F "task_type=task" \
  -F "external_id=order-4821" \
  -F "callback_template=https://example.org/synthpop/hooks?task={uuid}&order={external_id}" \
  -F "uploads=@referral-packet.pdf;type=application/pdf"
```

When the Task above finishes, Synthpop issues:

```
GET https://example.org/synthpop/hooks?task=<the-task-uuid>&order=order-4821
```

If you had omitted `external_id`, the `order` parameter would be the string
`null`:

```
GET https://example.org/synthpop/hooks?task=<the-task-uuid>&order=null
```

The callback signals *that* the Task finished. Your handler should then call
`GET /task/get` to read the actual result — treat the callback as a nudge to
fetch, not as the payload itself.

<Warning>
  **A callback is an unauthenticated notification.** It is a plain GET to the URL
  you supplied, carrying no secret, signature, or proof that it came from
  Synthpop. Treat it as an untrusted nudge: on receipt, call `GET /task/get`
  (with your bearer token) to fetch the authoritative Task state rather than
  trusting anything in the callback request. Callback timing and retry behavior
  are not guaranteed, so polling remains the reliable path — a callback can only
  ever save you a poll, never replace one.
</Warning>

<Warning>
  **Callbacks are opt-in and org-gated.** A `callback_template` fires only if your
  organization has external callbacks enabled. You can confirm this from the
  `org_settings.external_callback_enabled` flag returned by
  [`POST /auth/get_token`](/authentication). If callbacks are not enabled for your
  org, the template is ignored and you must poll. Contact your Synthpop
  representative to enable them.
</Warning>

## Load backpressure

When your organization submits work faster than its provisioned capacity, a Task
whose `task_type` is deferrable is parked server-side rather than running right
away — but it **reports as `pending`** to you the whole time. It is promoted
automatically once capacity frees up; no action is needed on your side. Keep
polling (or wait for the callback) as usual.

There is a **per-organization cap** on how many Tasks can be queued this way. When
the deferred queue is full, `POST /task/create` is rejected with `429 Too Many
Requests`. Back off and retry the create later.

<Note>
  The `429` response is part of the API contract but is **not yet present in the
  committed OpenAPI snapshot** that powers the [API reference](/api-reference/introduction).
  Handle `429` on `POST /task/create` anyway; the committed reference will include
  it once the schema-regeneration job lands.
</Note>

## Which should I use?

<CardGroup cols={2}>
  <Card title="Poll GET /task/get" icon="rotate">
    Works for every organization, no configuration. Best for scripts, batch jobs,
    and integrations that already have a work loop. Use backoff and handle
    `waiting`.
  </Card>

  <Card title="Callback template" icon="bell">
    No polling load; near-real-time notification. Requires
    `external_callback_enabled` and a reachable public HTTPS endpoint. Still fetch
    the result with `GET /task/get`.
  </Card>
</CardGroup>

## Related

<CardGroup cols={2}>
  <Card title="Responding to waiting Tasks" icon="reply" href="/guides/responding-to-waiting-tasks">
    Resume a Task that paused at `waiting` by adding files or data.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/guides/errors">
    The full status-code and error-body reference, including `429`.
  </Card>
</CardGroup>
