> ## Documentation Index
> Fetch the complete documentation index at: https://docs.memory.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: 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 cURL 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"]
  }'
```

```python Python theme={null}
hook = client.create_webhook(
    space_id="customer-support-bot",
    url="https://example.com/hooks/anona",
    event_types=["memory.created", "memory.consolidated"],
)
```

```typescript TypeScript theme={null}
const hook = await anona.createWebhook({
  spaceId: "customer-support-bot",
  url: "https://example.com/hooks/anona",
  eventTypes: ["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.

## Step 2: 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 cURL 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" }
    ]
  }'
```

```python Python theme={null}
job = client.record_batch(
    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",
        },
    ],
)
```

```typescript TypeScript theme={null}
const job = await anona.recordBatch({
  spaceId: "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](https://docs.memory.anonalabs.com/guides/temporal-search.md).

> [!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.

## 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 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}
```

```typescript TypeScript theme={null}
import crypto from "node:crypto";

function verify(rawBody: Buffer, signatureHeader: string, secret: string) {
  const expected =
    "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader || "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// express.raw() keeps the body bytes intact for signing
app.post("/hooks/anona", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.get("X-Anona-Signature") ?? "", WEBHOOK_SECRET)) {
    return res.status(401).send("bad signature");
  }
  const event = JSON.parse(req.body);
  // event.event === "memory.created" once the item is indexed and searchable
  res.json({ 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

> **[Memories API](https://docs.memory.anonalabs.com/api-reference/memories.md#bulk-ingest)**
>
> Full `record` and `record/batch` reference.

> **[Webhooks API](https://docs.memory.anonalabs.com/api-reference/webhooks.md)**
>
> Events, retries, delivery debugging.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.