# Async requests, polling & webhooks

> Most Nyne.ai data endpoints are asynchronous: a POST returns 202 with a request_id, then you poll the same path until it returns a terminal response - or hand over a callback_url and let a webhook deliver the result. This guide covers the request lifecycle, correct polling, terminal errors, and callback webhooks.

HTML version: https://api.nyne.ai/documentation/async

The common success lifecycle is: `POST /person/enrichment` -> `202` with a `request_id` -> `GET ?request_id=` (poll) -> `200` with `status: "completed"`. Some endpoints use `queued`, others use `processing`, and a terminal failure may be returned as a non-2xx error instead of a `200 failed` envelope.

## Submit, 202, poll

A `POST` to a data endpoint validates your input, checks account access and available credits when applicable, and enqueues background work. It returns `202 Accepted` with a `request_id` - the durable identifier for this one job (for example `a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4_1717000000_4271`). The result is NOT in that response; the acknowledgement carries a non-terminal status such as `queued` or `processing`.

```bash
curl -X POST https://api.nyne.ai/person/enrichment \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "email": "jane.doe@acme.com" }'
```

The `202` body hands you the `request_id` to poll with next:

```json
{
  "success": true,
  "data": {
    "request_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4_1717000000_4271",
    "status": "queued",
    "message": "Enrichment request queued. Use GET /person/enrichment?request_id=... to check status."
  },
  "timestamp": "2026-05-28T18:04:11"
}
```

## The lifecycle statuses

The submit/queue reply contains a non-terminal status such as `queued` or `processing`. From then on, the status endpoint commonly returns `pending`, `processing`, `completed`, or `failed`. A representative progression is `queued -> processing -> completed`.

- Most endpoints use `completed` for terminal success (never `"done"`). Some endpoint pages document additional successful terminal values. For example, People Search can return `exhausted` or `stale` with `completed: true`.
- A successful poll may use HTTP `200` or `202` while work is still in flight. When a successful payload includes `completed`, `false` means keep polling and `true` means stop; otherwise use the endpoint's status table. HTTP `200` alone does not prove completion. A terminal error envelope or non-retriable non-2xx processing/billing response also stops that request; transient transport/status-infrastructure responses may be retried using the endpoint's documented backoff policy.

Treat the status values listed here and on each endpoint page as the complete public contract. Do not infer additional intermediate states. See [Interpreting responses](https://api.nyne.ai/documentation/responses.md) for the full status model.

## Poll for the result

Poll by sending a `GET` to the **same path you POSTed to**, with `?request_id=...` and any additional page values required by that endpoint. A successful poll may use HTTP `200` or `202` before completion. When `completed` is present on a successful response, continue while it is false and stop when it is true. Otherwise follow the endpoint's status table. Any terminal error envelope or non-retriable non-2xx job response also stops that request; retry transient transport/status-infrastructure responses only as documented by the endpoint.

```bash
curl "https://api.nyne.ai/person/enrichment?request_id=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4_1717000000_4271" \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET"
```

Enrichment usually finishes in well under a second, so the first poll often already returns the result. Poll roughly once per second with a sensible max-wait; do not hammer the endpoint. Polls count toward your throttles, so see the [Rate limits](https://api.nyne.ai/documentation/rate-limits.md) guide before tightening the loop.

A successful terminal payload carries the merged, sanitized result inside the same envelope, with `status: "completed"` and `completed: true`:

```json
{
  "success": true,
  "data": {
    "request_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4_1717000000_4271",
    "status": "completed",
    "completed": true,
    "result": {
      "displayname": "Jane Doe",
      "headline": "VP of Product at Acme",
      "location": "San Francisco, CA",
      "best_business_email": "jane.doe@acme.com"
    },
    "error": null,
    "created_on": "2026-05-28T18:04:11",
    "completed_on": "2026-05-28T18:04:39"
  },
  "timestamp": "2026-05-28T18:04:45"
}
```

A status lookup itself can fail. The request must belong to the authenticating key, the `request_id` must be known, and the lookup store must be reachable. Treat authentication, authorization, unknown-request, insufficient-credit, and terminal processing errors as settled. Retry a `429` poll only when the poll itself was throttled; a `429` stored as the job's terminal status ends that request. Back off and retry transient service-unavailable responses:

| error.code | HTTP | Meaning |
| --- | --- | --- |
| invalid_credentials | 401 | Authentication failed. Fix credentials before retrying. |
| insufficient_credits | 402 | The job cannot continue until the account has enough credits. |
| access_denied / ip_not_allowed | 403 | The authenticated account or request IP is not allowed to use the endpoint. |
| request_not_found | 404 | No request matches that request_id, including request IDs owned by another API key. |
| rate_limit_exceeded | 429 | If the poll itself was throttled, honor Retry-After and retry it. If this is the job's stored terminal status, stop polling that request_id and submit a new request after the limit resets. |
| search_error / internal_error | 500 | Terminal processing failure for that request_id unless the endpoint says otherwise. |
| service_unavailable | 503 | A transient lookup issue. Back off and retry. |

## Webhooks with callback_url

Instead of polling, supply a `callback_url` on the submit. When the job completes, Nyne.ai POSTs the same result envelope to your URL:

```bash
curl -X POST https://api.nyne.ai/person/enrichment \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "X-API-Secret: YOUR_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
        "email": "jane.doe@acme.com",
        "callback_url": "https://hooks.example.com/nyne"
      }'
```

The callback URL is validated before the request is accepted. It must be `http(s)://`, at most 1024 characters, and carry no userinfo (no `user:pass@` in the URL). It is also checked against an allowed-hosts policy that rejects localhost, private, and reserved addresses, along with `.local`, `.internal`, `.lan`, `.home`, `.corp`, `.test`, `.invalid`, and `.example` hosts. An invalid target is rejected at submit time with `400 invalid_callback_url`.

For endpoints using the common callback envelope, the delivered payload matches what a poll would have returned: `success`, `data` (with the terminal `status` and `result`), and `timestamp`. Verify and process it idempotently, keyed on the `request_id`, so a retried delivery is safe.

Person Search has endpoint-specific pagination behavior. Its callback can report queued session work, but it is not a per-page pagination mechanism: after a successful callback it is not re-emitted for every later page, and an immediately cached page POST does not enqueue one. The exact `GET /person/search` request with that page's `request_id`, `offset`, and `limit` remains the authoritative retrieval path. See [Person Search](https://api.nyne.ai/documentation/person/search.md).

## Which calls are synchronous

Not everything is async. A few calls return `200` directly, with no `request_id` and nothing to poll:

- `POST /person/enrichment/feedback` is a single insert that returns `200` immediately.
- The status-lookup `GET`s themselves are synchronous, and they are not credit-gated - polling never costs credits.

## Idempotency and durability

The `request_id` is the durable handle to one job. Re-polling it with the documented status GET is safe and does NOT re-charge credits. A poll loop, a retried GET poll, or a redelivered webhook does not create another charge. People Search is the pagination exception: multiple page POSTs reuse one `request_id`, and each successfully fulfilled page POST is billable; use GET, not POST, for status checks and stored-page reads.

## What's next

- [Rate limits](https://api.nyne.ai/documentation/rate-limits.md) - how throttles work so your poll loop stays under them.
- [Interpreting responses](https://api.nyne.ai/documentation/responses.md) - the envelope, the status model, and how normalized statuses are derived.
- [Credits](https://api.nyne.ai/documentation/credits.md) - when a request is charged and what counts as a meaningful result.
- [Your first request](https://api.nyne.ai/documentation/first-request.md) - the end-to-end walkthrough this guide builds on.
