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

# Tasks

> Async operation polling — receive a task ID, poll for status, handle long-running work

## Overview

Some v2 endpoints can't return a final result synchronously — typically because they fan out to a slow upstream (a sales agent that takes minutes to confirm a media buy, an audience platform that hashes and matches CRM data, a creative agent rendering a large asset). For those operations, the API returns a **task ID** immediately with HTTP `202 Accepted`. The task tracks the async work; you poll it (or receive a webhook) until it reaches a terminal state.

A task has:

* A stable UUID `taskId`
* A `taskType` describing the operation kind (`audience_sync`, `media_buy_create`, `creative_sync`)
* A `status` that progresses through AdCP states: `submitted` → `working` → `completed` (or `failed`, or `input-required`)
* A `resourceId` populated on completion that points at the resource the task created or updated
* An `error` object populated on failure
* A `response` payload with the full downstream response
* A `retryAfterSeconds` hint that tells you how often to poll

<Tip>
  Webhooks are preferred over polling whenever they're available. See [Notifications](/v2/guides/notifications) for the push-based path. The task endpoint exists as the AdCP-compatible polling fallback for callers that can't receive webhooks.
</Tip>

<Note>
  All endpoints below are mounted under `https://api.interchange.io/api/v2/buyer/`.
</Note>

## Prerequisites

* A Scope3 API key (see [Authentication](/v2/authentication))
* A response from an endpoint that returned a task ID (e.g. an audience sync)

```bash theme={null}
export SCOPE3_API_KEY=scope3_<your_api_key>
export BASE=https://api.interchange.io/api/v2/buyer
```

## Step 1: Recognize when a task is returned

Async endpoints return HTTP `202 Accepted` with a `taskId` in the response body. For example, syncing an audience:

```bash theme={null}
curl -X POST "$BASE/advertisers/42/audiences/sync" \
  -H "Authorization: Bearer $SCOPE3_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "audiences": [
      { "name": "high_value_buyers", "members": [/* ... */] }
    ]
  }'
```

```json theme={null}
{
  "data": {
    "success": true,
    "accountId": "42",
    "operationId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "taskId": "550e8400-e29b-41d4-a716-446655440000"
  }
}
```

The `taskId` is a UUID. Capture it and poll the task endpoint until the operation is done.

<Tip>
  The current set of operations that go through the task system is `audience_sync`, `media_buy_create`, and `creative_sync`. Synchronous endpoints (most reads, simple updates) do not return a task ID.
</Tip>

## Step 2: Poll for status

```bash theme={null}
curl "$BASE/tasks/550e8400-e29b-41d4-a716-446655440000" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "data": {
    "taskId": "550e8400-e29b-41d4-a716-446655440000",
    "taskType": "audience_sync",
    "status": "working",
    "resourceType": "audience",
    "resourceId": null,
    "error": null,
    "response": null,
    "metadata": {
      "accountId": "42",
      "audienceCount": 3,
      "deleteMissing": false
    },
    "retryAfterSeconds": 30,
    "createdAt": "2026-04-26T14:30:00.000Z",
    "updatedAt": "2026-04-26T14:30:12.481Z"
  }
}
```

### Status values

| Status           | Meaning                                        | What to do                                                                                 |
| ---------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `submitted`      | Task accepted, not yet picked up               | Wait `retryAfterSeconds` and poll again                                                    |
| `working`        | Downstream system is processing                | Wait `retryAfterSeconds` and poll again                                                    |
| `input-required` | The downstream needs additional input from you | Inspect `response` / `error.suggestion` and resubmit the originating call with corrections |
| `completed`      | Done — `resourceId` and `response` populated   | Stop polling; read the result                                                              |
| `failed`         | Permanent failure — `error` populated          | Stop polling; read the error and decide whether to retry                                   |

<Warning>
  `submitted` and `working` are the only non-terminal states. `completed`, `failed`, and `input-required` are terminal from the polling perspective — keep polling only while the status is `submitted` or `working`.
</Warning>

## Step 3: Handle outcomes

### `completed`

Read `resourceId` to find the entity the task created or updated, and `response` for the full downstream payload.

```json theme={null}
{
  "data": {
    "taskId": "550e8400-e29b-41d4-a716-446655440000",
    "taskType": "audience_sync",
    "status": "completed",
    "resourceType": "audience",
    "resourceId": "aud_12345",
    "response": { "matched": 8421, "unmatched": 312 },
    "error": null,
    "retryAfterSeconds": null,
    "createdAt": "2026-04-26T14:30:00.000Z",
    "updatedAt": "2026-04-26T14:32:47.901Z"
  }
}
```

### `failed`

Inspect the AdCP-compatible `error` object:

```json theme={null}
{
  "data": {
    "taskId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "failed",
    "error": {
      "code": "VALIDATION_ERROR",
      "message": "Invalid budget value",
      "field": "packages[0].targeting",
      "suggestion": "Ensure budget is positive",
      "retryAfter": 60,
      "recovery": "correctable"
    }
  }
}
```

`error.recovery` classifies the failure for agent retry logic:

| Recovery      | Meaning                                                                           |
| ------------- | --------------------------------------------------------------------------------- |
| `transient`   | Retry the original call after a backoff — the upstream is temporarily unavailable |
| `correctable` | Fix the input per `error.field` / `error.suggestion`, then retry                  |
| `terminal`    | No automatic retry will help — surface to a human operator                        |

If `error.retryAfter` is set, wait at least that many seconds before retrying the original operation.

### `working` / `submitted` — backing off

The response carries `retryAfterSeconds` as a hint; treat it as a floor. If the operation doesn't complete within your timeout, surface the task ID to the caller so they can poll later — tasks are durable and outlive the originating request.

```bash theme={null}
# pseudo-loop
while true; do
  resp=$(curl -s "$BASE/tasks/$TASK_ID" -H "Authorization: Bearer $SCOPE3_API_KEY")
  status=$(echo "$resp" | jq -r '.data.status')
  case "$status" in
    completed|failed|input-required) echo "$resp"; break ;;
    *) sleep "$(echo "$resp" | jq -r '.data.retryAfterSeconds // 30')" ;;
  esac
done
```

<Tip>
  If the underlying operation is rate-limited, the API will return a `429` on the originating call (not on the task poll). See [Rate limits](/v2/reference/rate-limits) for `Retry-After` handling.
</Tip>

## Best practices

* **Prefer webhooks** — register a `pushNotificationConfig` on the originating call where the endpoint supports it (e.g. the audience sync endpoint, and the storefront's `create_media_buy` and `update_media_buy` tools when called directly over AdCP). The webhook fires on every status transition; polling is only the fallback.
* **Honour `retryAfterSeconds`** — it's set per task type and reflects how fast the downstream actually changes state. Polling more aggressively wastes quota and won't make the task complete sooner.
* **Cap polling duration** — if a task hasn't reached a terminal state within an order of magnitude of the typical completion time for its `taskType`, escalate to operator review rather than spinning forever.
* **Tasks are scoped to your account** — `GET /tasks/:taskId` returns `404` if the task doesn't belong to the authenticated account. Treat the task ID as opaque and don't share it across tenants.
* **Persist the `taskId`** — store it next to the originating request so a later process (or a human operator) can resolve the outcome even if the originating client died.
* **Idempotency** — the underlying AdCP operations are idempotent on the originating call's idempotency key; if a task `failed` with `recovery: transient`, you can safely retry the original call with the same idempotency key.

## Endpoint reference

| Method | Path             | Purpose                                   |
| ------ | ---------------- | ----------------------------------------- |
| `GET`  | `/tasks/:taskId` | Fetch the current status of an async task |

**Path parameter**: `taskId` (UUID).

**Response**: `{ data: TaskOutput }` where `TaskOutput` includes `taskId`, `taskType`, `status`, `resourceType`, `resourceId`, `error`, `response`, `metadata`, `retryAfterSeconds`, `createdAt`, `updatedAt`.

**Status enum**: `submitted`, `working`, `completed`, `failed`, `input-required`.

**Task type enum**: `audience_sync`, `media_buy_create`, `creative_sync`.

See the OpenAPI spec for the full schema: [API Reference](/v2/buyer-api-reference).

## Related

* [Notifications](/v2/guides/notifications) — webhook-based delivery of the same task lifecycle events; preferred over polling
* [Rate limits](/v2/reference/rate-limits) — `Retry-After` semantics on the originating call
* [Errors](/v2/reference/errors) — error codes that appear in the task `error` object
