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
Webhooks are preferred over polling whenever they’re available. See Notifications for the push-based path. The task endpoint exists as the AdCP-compatible polling fallback for callers that can’t receive webhooks.
All endpoints below are mounted under https://api.agentic.scope3.com/api/buyer/.
Prerequisites
- A Scope3 API key (see Authentication)
- A response from an endpoint that returned a task ID (e.g. an audience sync)
export SCOPE3_API_KEY=scope3_<your_api_key>
export BASE=https://api.agentic.scope3.com/api/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:
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": [/* ... */] }
]
}'
{
"data": {
"success": true,
"accountId": "42",
"operationId": "op_5f2a...",
"taskId": "550e8400-e29b-41d4-a716-446655440000"
}
}
The taskId is a UUID. Capture it and poll the task endpoint until the operation is done.
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.
Step 2: Poll for status
curl "$BASE/tasks/550e8400-e29b-41d4-a716-446655440000" \
-H "Authorization: Bearer $SCOPE3_API_KEY"
{
"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 |
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.
Step 3: Handle outcomes
completed
Read resourceId to find the entity the task created or updated, and response for the full downstream payload.
{
"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:
{
"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.
# 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
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 for Retry-After handling.
Best practices
- Prefer webhooks — register a
pushNotificationConfig on the originating call where the endpoint supports it (e.g. the audience sync endpoint accepts one). 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 customer —
GET /tasks/:taskId returns 404 if the task doesn’t belong to the authenticated customer. 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.
- Notifications — webhook-based delivery of the same task lifecycle events; preferred over polling
- Rate limits —
Retry-After semantics on the originating call
- Errors — error codes that appear in the task
error object