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

# Bulk-Importing History with Webhooks

> Backfill a space from existing history without polling: batch-ingest, then let a webhook tell you when it's done.

Backfilling a space (from a CRM export, chat transcripts, a support ticket
history) means writing many memories at once, and each one takes a couple of
seconds to extract and index. This guide covers the pattern that avoids blocking on
that: batch ingest plus a webhook, so you never poll.

## Step 1: queue the batch

`record/batch` accepts 1 to 100 items in one call and is always asynchronous. You
get a `job_id` back immediately, the extraction happens in the background.

```bash theme={null}
curl -X POST https://api.anonalabs.com/v1/record/batch \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "space_id": "customer-support-bot",
    "items": [
      { "content": "Alice works at Google." },
      { "content": "Bob prefers async standups.", "context": "team sync" },
      { "content": "The Q3 launch slipped to October.", "timestamp": "2026-06-01T00:00:00Z" }
    ]
  }'
```

```json theme={null}
{
  "job_id": "job_batch_71b3",
  "job_ids": ["job_batch_71b3"],
  "status": "processing",
  "accepted": 3
}
```

<Note>
  Set `timestamp` on every item you can. It is the **event** time (when the thing
  happened), and ranking is recency-aware, so an undated backfill arrives looking
  uniformly fresh and competes with genuinely recent memories. Note that it does
  not change when the memory was *recorded*, so `as_of` still sees the whole import
  as having landed today. See [Searching over time](/guides/temporal-search).
</Note>

<Note>
  More than 100 items to import? Chunk them into multiple `record/batch` calls. There's
  no cost benefit to queuing versus writing synchronously, only a latency one, so
  chunking costs nothing extra.
</Note>

## Step 2: register a webhook once, instead of polling every batch

You *can* poll `GET /v1/spaces/{space_id}/jobs/{job_id}` until `status` is terminal,
but for a bulk import that's a lot of polling loops for not much information. Register
a webhook instead and get told when each item lands:

```bash theme={null}
curl -X POST https://api.anonalabs.com/v1/spaces/customer-support-bot/webhooks \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/anona",
    "event_types": ["memory.created", "memory.consolidated"]
  }'
```

The response includes `secret` **once**. Store it immediately, it's never shown
again and you need it to verify deliveries.

<Tip>
  A new webhook can take up to 15 seconds before it starts receiving events. If you
  register the webhook and then immediately fire the batch, the first item or two may
  be missed. Register it, wait a moment, then queue the import.
</Tip>

## Step 3: verify and handle deliveries

Every delivery is signed. Verify the raw request body, not the parsed JSON, because
reserializing changes byte order and breaks the signature:

```python theme={null}
import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

@app.post("/hooks/anona")
async def receive(request: Request):
    raw = await request.body()
    if not verify(raw, request.headers.get("X-Anona-Signature", ""), WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="bad signature")
    event = json.loads(raw)
    # event["event"] == "memory.created" once the item is indexed and searchable
    return {"ok": True}
```

Acknowledge with any `2xx` **fast**: deliveries time out after 10 seconds, and a
slow handler gets treated as failed and retried, which turns into duplicate
deliveries. Do the real work after you've returned the response, and make it
idempotent on `operation_id` since delivery is at-least-once.

## Track overall progress

For a large import you'll usually want a "is the whole thing done" view, not just
per-item events. Two options, not mutually exclusive:

* Poll the batch's `job_id` (`GET /v1/spaces/{space_id}/jobs/{job_id}`) at a slow
  interval (e.g. every 10–30s) purely for the top-level `completed`/`failed` status.
* Count `memory.created` webhook deliveries against `accepted` from the batch
  response, if you want per-item completion in your own system without polling at
  all.

## Common mistakes

* **Polling the job status quickly and often.** It's a real endpoint hit; for a bulk
  import a webhook is strictly better. Reserve polling for "did the whole batch finish"
  checks, not per-item status.
* **Re-serializing the webhook body before verifying it.** Breaks the HMAC. Verify
  the raw bytes.
* **Holding the webhook connection open while you process.** Acknowledge first, work
  after.
* **Sending more than 100 items in one `record/batch` call.** It's rejected, so chunk it.

## Next steps

<CardGroup cols={2}>
  <Card title="Memories API" icon="database" href="/api-reference/memories#bulk-ingest">
    Full `record` and `record/batch` reference.
  </Card>

  <Card title="Webhooks API" icon="bell" href="/api-reference/webhooks">
    Events, retries, delivery debugging.
  </Card>
</CardGroup>
