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

# Webhooks and events

> Receive each candidate's result as soon as they finish.

Do not poll for results. When a candidate finishes, Record sends you an **event** with their [result record](/blue-collar-api/guides/results). Nothing is dropped: an event you do not confirm comes again.

| You send | You get | Scopes |
| - | - | - |
| an HTTPS URL (`POST /webhooks/endpoints`) | one `candidate.result` event per finished candidate | `webhooks:manage` (endpoints), `events:read` (feed) |

## Two ways to receive events

| | Webhook (Record pushes) | Event feed (you pull) |
| - | - | - |
| How | Record POSTs each event to your HTTPS URL, signed with your secret. | You call `POST /events/pull` whenever you want. |
| Best for | Getting results the moment they are ready. | No public URL, or catching what a webhook missed. |
| An event is confirmed when | Your server answers **2xx**. | You send its id in `ack` on your next pull. |
| Scope | `webhooks:manage` | `events:read` |

**Use the webhook as the main path and the feed as the safety net.** An event is confirmed once, by whichever way gets there first, and then stops coming from the other: a 2xx removes it from the feed, and a pull that confirms it cancels the webhook that was waiting. If you run both, let your pull wait about a minute before confirming new events, so the webhook gets its turn.

**Which one delivered it?** The delivery log shows `delivered_via`: `webhook` or `feed`.

**Your URL is yours to choose.** Register any HTTPS URL, such as `https://ats.example.com/hooks/record` or `https://example.com/api/record-results`. Record posts to exactly that URL.

### How the pull loop works

Say 5 events are waiting. Your first pull returns all 5. If you never confirm them, **the next pull returns the same 5 again**. Once you pass their ids in `ack`, they are gone and you get only newer ones.

1. Pull with `ack: []`. You get the events, each with an `event_id`.
2. Save them. Skip an `event_id` you already saved.
3. Pull again with those ids in `ack`. They are confirmed and the next events come back.
4. Stop when a pull returns no events.

Confirm only what you really saved: that is what makes a lost response or a crash safe. Confirming is safe to repeat, and ids that are unknown or already confirmed are accepted.

## When an event is sent

Once, when a candidate reaches `completed`, `declined` or `failed` and their result has stayed the same for about 45 seconds (the last questions are answered after the skill result exists, so Record waits instead of sending one event per answer). If a finished result later changes (a re-grade), you get a new event with a higher `version` for the same candidate.

```http theme={null}
POST /hooks/record HTTP/1.1
Content-Type: application/json
User-Agent: Record-Webhooks/1.0
Record-Event-Id: evt_9c2e4a6b8d0f1234567890abcdef1234
Record-Delivery-Id: dlv_7a1b3c5d7e9f0123456789abcdef0123
Record-Attempt: 1
Record-Signature: t=1791204071,v1=5f2b…

{
  "event_id": "evt_9c2e4a6b8d0f1234567890abcdef1234",
  "type": "candidate.result",
  "api_version": "2026-10-01",
  "created_at": "2026-10-05T12:41:11Z",
  "mode": "live",
  "version": 1,
  "sequence": 482913,
  "data": { "...": "the record shown in Results" }
}
```

| Field | Description |
| - | - |
| `event_id` | Stable forever. Use it to skip repeats. |
| `type` | `candidate.result` (or `webhook.test` from the test endpoint). |
| `version` | Rises each time the same candidate's result changes. Keep the highest. |
| `sequence` | Order of events in your stream. |
| `mode` | `live` or `test`. |
| `data` | The [result record](/blue-collar-api/guides/results#the-record). |

## What your server must do

1. **Verify the signature** (below), then **answer 2xx within 10 seconds**. That is the acknowledgement. Process the event after answering.
2. **Keep the `event_id`** of what you processed and skip repeats: delivery is at-least-once, so the same event can arrive twice.
3. **Keep the highest `version`** per candidate; ignore an older one that arrives late.
4. Any other answer, a timeout or a connection error means "not received": Record retries.

## Retries and nothing lost

| | |
| - | - |
| Retry waits | 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h, 12 h, 24 h (about 52 hours), then the delivery is kept as `dead`: visible in the delivery log and replayable with `POST /webhooks/deliveries/{delivery_id}/retry`. A `Retry-After` header in your answer is respected. |
| Endpoint down for long | After 20 failures in a row the endpoint is `auto_paused`. Events keep being recorded and wait. Fix it, then `PATCH` its status to `active`: they are sent, oldest first. |
| Event feed | `POST /events/pull` returns every event you have not confirmed, oldest first, and confirms the ids you send in `ack`. Use it as a safety net next to the webhook, or instead of it if you cannot expose a URL. Events are kept 30 days. |
| Past results | Create the endpoint with `backfill_from`, or call `POST /webhooks/endpoints/{endpoint_id}/backfill`, to receive results from the last 30 days at most. |
| Test | `POST /webhooks/endpoints/{endpoint_id}/test` sends a signed `webhook.test` now and shows what your server answered. |

## Verify the signature

Each delivery has `Record-Signature: t=<seconds>,v1=<hex>`. Compute HMAC-SHA256 of `<t>.<raw request body>` with your **API secret key** (`VFN_SK_…`, the same one you sign your own calls with) and compare to `v1`. Reject if `t` is more than 5 minutes old. Nothing else to configure.

<CodeGroup>
  ```javascript Node theme={null}
  // Express: use the RAW body, not a parsed one.
  const crypto = require("crypto");

  app.post("/hooks/record", express.raw({ type: "application/json" }), (req, res) => {
    const header = req.get("Record-Signature") || "";
    const t = (header.match(/t=(\d+)/) || [])[1];
    const sigs = [...header.matchAll(/v1=([a-f0-9]{64})/g)].map((m) => m[1]);
    const expected = crypto
      .createHmac("sha256", process.env.RECORD_API_SECRET)
      .update(`${t}.${req.body.toString("utf8")}`)
      .digest("hex");

    const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
    const valid = sigs.some((s) => crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));
    if (!t || !fresh || !valid) return res.status(400).end();

    res.status(200).end(); // acknowledge first
    const event = JSON.parse(req.body.toString("utf8"));
    // then: skip if event.event_id was already processed; ignore an older version; store event.data
  });
  ```

  ```python Python theme={null}
  import hashlib, hmac, os, re, time
  from flask import Flask, request

  app = Flask(__name__)

  @app.post("/hooks/record")
  def record_hook():
      raw = request.get_data()  # the raw body
      header = request.headers.get("Record-Signature", "")
      m = re.search(r"t=(\d+)", header)
      sigs = re.findall(r"v1=([a-f0-9]{64})", header)
      if not m or abs(time.time() - int(m.group(1))) > 300:
          return "", 400
      expected = hmac.new(os.environ["RECORD_API_SECRET"].encode(),
                          f"{m.group(1)}.".encode() + raw, hashlib.sha256).hexdigest()
      if not any(hmac.compare_digest(s, expected) for s in sigs):
          return "", 400
      # acknowledge now; hand the event to a queue and process it there
      return "", 200
  ```
</CodeGroup>

Your URL must be HTTPS on port 443 and reachable from the internet. The path is yours to choose. Redirects are not followed. You can have up to 5 endpoints.

## Webhook endpoints

### Create an endpoint

`POST /webhooks/endpoints`

Register the HTTPS URL that receives result events. The path is up to you. Deliveries are signed with your API secret key.

**Scope:** `webhooks:manage` · **Idempotency-Key:** optional

| Field | Type | Description |
| - | - | - |
| `url` | `string` | **Required.** HTTPS, port 443, a public host. Any 2xx answer counts as the acknowledgement. |
| `description` | `string` | Up to 200 characters. |
| `event_types` | `array<string>` | Default `candidate.result`. |
| `backfill_from` | `string` | ISO date-time: also send results from then on (at most 30 days back). |

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("POST", "/api/v1/partner/webhooks/endpoints", {
    "url": "https://ats.example.com/hooks/record",
    "description": "Production ATS",
    "event_types": ["candidate.result"]
  });
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("POST", "/api/v1/partner/webhooks/endpoints", {
      "url": "https://ats.example.com/hooks/record",
      "description": "Production ATS",
      "event_types": ["candidate.result"]
  })
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall POST "/api/v1/partner/webhooks/endpoints" '{
    "url": "https://ats.example.com/hooks/record",
    "description": "Production ATS",
    "event_types": ["candidate.result"]
  }'
  ```

  ```json 201 Response theme={null}
  {
    "success": true,
    "data": {
      "endpoint": {
        "endpoint_id": "we_3f9a1c7e5b2d4086a1b3c5d7e9f01234",
        "url": "https://ats.example.com/hooks/record",
        "description": "Production ATS",
        "event_types": ["candidate.result"],
        "status": "active",
        "paused_reason": null,
        "consecutive_failures": 0,
        "last_success_at": "2026-10-05T12:41:12.000Z",
        "last_failure_at": null,
        "created_at": "2026-10-01T08:00:00.000Z"
      },
      "existing": false
    }
  }
  ```
</CodeGroup>

Save `data.endpoint.endpoint_id` for the next step.

| Error | When |
| - | - |
| 400 `VALIDATION_FAILED` | URL not https, a private address, a port other than 443, or an unknown event type. |
| 422 `LIMIT_EXCEEDED` | Already 5 endpoints. |

Can also return idempotency errors (`IDEMPOTENCY_IN_PROGRESS`, `IDEMPOTENCY_KEY_REUSED`) and the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/webhooks/create-an-endpoint)

***

### List endpoints

`GET /webhooks/endpoints`

Each endpoint with its status (`active`, `paused`, `auto_paused`), consecutive failures and last success.

**Scope:** `webhooks:manage`

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("GET", "/api/v1/partner/webhooks/endpoints");
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("GET", "/api/v1/partner/webhooks/endpoints")
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall GET "/api/v1/partner/webhooks/endpoints"
  ```

  ```json 200 Response theme={null}
  {
    "success": true,
    "data": {
      "items": [
        {
          "endpoint_id": "we_3f9a1c7e5b2d4086a1b3c5d7e9f01234",
          "url": "https://ats.example.com/hooks/record",
          "description": "Production ATS",
          "event_types": ["candidate.result"],
          "status": "active",
          "paused_reason": null,
          "consecutive_failures": 0,
          "last_success_at": "2026-10-05T12:41:12.000Z",
          "last_failure_at": null,
          "created_at": "2026-10-01T08:00:00.000Z"
        }
      ]
    }
  }
  ```
</CodeGroup>

Can also return the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/webhooks/list-endpoints)

***

### Get an endpoint

`GET /webhooks/endpoints/{endpoint_id}`

One endpoint with its current status.

**Scope:** `webhooks:manage`

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("GET", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234");
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("GET", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234")
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall GET "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234"
  ```

  ```json 200 Response theme={null}
  {
    "success": true,
    "data": {
      "endpoint_id": "we_3f9a1c7e5b2d4086a1b3c5d7e9f01234",
      "url": "https://ats.example.com/hooks/record",
      "description": "Production ATS",
      "event_types": ["candidate.result"],
      "status": "active",
      "paused_reason": null,
      "consecutive_failures": 0,
      "last_success_at": "2026-10-05T12:41:12.000Z",
      "last_failure_at": null,
      "created_at": "2026-10-01T08:00:00.000Z"
    }
  }
  ```
</CodeGroup>

| Error | When |
| - | - |
| 404 `NOT_FOUND` | No such endpoint. |

Can also return the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/webhooks/get-an-endpoint)

***

### Update an endpoint

`PATCH /webhooks/endpoints/{endpoint_id}`

Send only what changes. `status: paused` holds deliveries (nothing is lost); `status: active` resumes them, oldest first. Use it to bring back an `auto_paused` endpoint.

**Scope:** `webhooks:manage` · **Idempotency-Key:** optional

| Field | Type | Description |
| - | - | - |
| `url` | `string` | Your HTTPS endpoint (port 443, public host). Record POSTs each event here and counts any 2xx as the acknowledgement. |
| `description` | `string` | At most 200 characters. |
| `event_types` | `array<string>` | Each one of `candidate.result`. 1 to 1 items. |
| `status` | `string` | One of `active`, `paused`. |

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("PATCH", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234", { "status": "paused" });
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("PATCH", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234", {"status": "paused"})
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall PATCH "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234" '{ "status": "paused" }'
  ```

  ```json 200 Response theme={null}
  {
    "success": true,
    "data": {
      "endpoint_id": "we_3f9a1c7e5b2d4086a1b3c5d7e9f01234",
      "url": "https://ats.example.com/hooks/record",
      "description": "Production ATS",
      "event_types": ["candidate.result"],
      "status": "paused",
      "paused_reason": "paused by you",
      "consecutive_failures": 0,
      "last_success_at": "2026-10-05T12:41:12.000Z",
      "last_failure_at": null,
      "created_at": "2026-10-01T08:00:00.000Z"
    }
  }
  ```
</CodeGroup>

| Error | When |
| - | - |
| 404 `NOT_FOUND` | No such endpoint. |
| 400 `VALIDATION_FAILED` | No field sent, or a bad URL. |

Can also return idempotency errors (`IDEMPOTENCY_IN_PROGRESS`, `IDEMPOTENCY_KEY_REUSED`) and the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/webhooks/update-an-endpoint)

***

### Delete an endpoint

`DELETE /webhooks/endpoints/{endpoint_id}`

Deliveries still waiting for it stop. The events stay in the feed for 30 days.

**Scope:** `webhooks:manage` · **Idempotency-Key:** optional

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("DELETE", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234");
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("DELETE", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234")
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall DELETE "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234"
  ```

  ```json 200 Response theme={null}
  {
    "success": true,
    "data": { "deleted": true }
  }
  ```
</CodeGroup>

| Error | When |
| - | - |
| 404 `NOT_FOUND` | No such endpoint. |

Can also return idempotency errors (`IDEMPOTENCY_IN_PROGRESS`, `IDEMPOTENCY_KEY_REUSED`) and the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/webhooks/delete-an-endpoint)

***

### Send a test event

`POST /webhooks/endpoints/{endpoint_id}/test`

Sends a signed `webhook.test` right now and shows what your server answered. Not stored, not retried.

**Scope:** `webhooks:manage`

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("POST", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234/test");
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("POST", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234/test")
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall POST "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234/test"
  ```

  ```json 200 Response theme={null}
  {
    "success": true,
    "data": { "ok": true, "status_code": 200, "latency_ms": 142, "error": null }
  }
  ```
</CodeGroup>

| Error | When |
| - | - |
| 404 `NOT_FOUND` | No such endpoint. |

Can also return the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/webhooks/send-a-test-event)

***

### Backfill past results

`POST /webhooks/endpoints/{endpoint_id}/backfill`

Queues the results since `from` (at most 30 days back) as normal deliveries. Safe to repeat.

**Scope:** `webhooks:manage` · **Idempotency-Key:** optional

| Field | Type | Description |
| - | - | - |
| `from` | `string` | **Required.** ISO 8601 date-time. |

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("POST", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234/backfill", { "from": "2026-10-01T00:00:00Z" });
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("POST", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234/backfill", {"from": "2026-10-01T00:00:00Z"})
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall POST "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234/backfill" '{ "from": "2026-10-01T00:00:00Z" }'
  ```

  ```json 202 Response theme={null}
  {
    "success": true,
    "data": { "accepted": true, "from": "2026-10-01T00:00:00.000Z" }
  }
  ```
</CodeGroup>

| Error | When |
| - | - |
| 404 `NOT_FOUND` | No such endpoint. |

Can also return idempotency errors (`IDEMPOTENCY_IN_PROGRESS`, `IDEMPOTENCY_KEY_REUSED`) and the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/webhooks/backfill-past-results)

***

### List deliveries

`GET /webhooks/endpoints/{endpoint_id}/deliveries`

Every send to this endpoint, newest first: attempts, last HTTP status or error, when the next retry is due.

**Scope:** `webhooks:manage`

| Query | Type | Description |
| - | - | - |
| `status` | `string` | One of `pending`, `in_flight`, `retrying`, `held`, `delivered`, `dead`. |
| `limit` | `integer` | 1 to 100, default 50. |
| `cursor` | `string` | The next\_cursor of the previous page. |

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("GET", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234/deliveries?status=dead&limit=50");
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("GET", "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234/deliveries?status=dead&limit=50")
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall GET "/api/v1/partner/webhooks/endpoints/we_3f9a1c7e5b2d4086a1b3c5d7e9f01234/deliveries?status=dead&limit=50"
  ```

  ```json 200 Response theme={null}
  {
    "success": true,
    "data": {
      "items": [
        {
          "delivery_id": "dlv_7a1b3c5d7e9f0123456789abcdef0123",
          "event_id": "evt_9c2e4a6b8d0f1234567890abcdef1234",
          "endpoint_id": "we_3f9a1c7e5b2d4086a1b3c5d7e9f01234",
          "status": "dead",
          "attempts": 11,
          "next_attempt_at": null,
          "last_status_code": 503,
          "last_error": "HTTP 503",
          "last_latency_ms": 142,
          "last_attempt_at": "2026-10-05T12:41:12.000Z",
          "delivered_at": "2026-10-05T12:41:12.000Z",
          "delivered_via": "webhook",
          "created_at": "2026-10-05T12:41:11.000Z"
        }
      ],
      "count": 1,
      "total": null,
      "next_cursor": null,
      "has_more": false
    }
  }
  ```
</CodeGroup>

| Error | When |
| - | - |
| 404 `NOT_FOUND` | No such endpoint. |

Can also return the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/webhooks/list-deliveries)

***

### Retry a delivery

`POST /webhooks/deliveries/{delivery_id}/retry`

For a dead one after you fixed your endpoint. Its attempts count from zero again.

**Scope:** `webhooks:manage` · **Idempotency-Key:** optional

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("POST", "/api/v1/partner/webhooks/deliveries/dlv_7a1b3c5d7e9f0123456789abcdef0123/retry");
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("POST", "/api/v1/partner/webhooks/deliveries/dlv_7a1b3c5d7e9f0123456789abcdef0123/retry")
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall POST "/api/v1/partner/webhooks/deliveries/dlv_7a1b3c5d7e9f0123456789abcdef0123/retry"
  ```

  ```json 200 Response theme={null}
  {
    "success": true,
    "data": {
      "delivery_id": "dlv_7a1b3c5d7e9f0123456789abcdef0123",
      "event_id": "evt_9c2e4a6b8d0f1234567890abcdef1234",
      "endpoint_id": "we_3f9a1c7e5b2d4086a1b3c5d7e9f01234",
      "status": "pending",
      "attempts": 0,
      "next_attempt_at": "2026-10-05T12:45:00.000Z",
      "last_status_code": 200,
      "last_error": null,
      "last_latency_ms": 142,
      "last_attempt_at": "2026-10-05T12:41:12.000Z",
      "delivered_at": null,
      "delivered_via": null,
      "created_at": "2026-10-05T12:41:11.000Z"
    }
  }
  ```
</CodeGroup>

| Error | When |
| - | - |
| 404 `NOT_FOUND` | No such delivery. |

Can also return idempotency errors (`IDEMPOTENCY_IN_PROGRESS`, `IDEMPOTENCY_KEY_REUSED`) and the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/webhooks/retry-a-delivery)

## Event feed

### Pull and confirm events

`POST /events/pull`

One call does both jobs. Send the `ack` ids of the events you already processed, and get the next events back. An event keeps coming back until you put its id in `ack`.

**Scope:** `events:read`

| Field | Type | Description |
| - | - | - |
| `ack` | `array<string>` | Ids of the events you processed, up to 100. Leave it empty on your first call. |
| `status` | `string` | `unacked` (default) gives only events you have not confirmed. `all` replays history (use it with `from`). |
| `type` | `string` | `candidate.result`. |
| `from` | `string` | Only events created at or after this time (replay). |
| `after` | `integer` | Only events with a higher `sequence` than this. A paging shortcut: rely on `unacked`. |
| `limit` | `integer` | 1 to 100, default 100. |

<CodeGroup>
  ```javascript JavaScript theme={null}
  let ack = [];
  for (;;) {
    const res = await call("POST", "/api/v1/partner/events/pull", { ack, limit: 100 });
    const { items } = (await res.json()).data;
    if (items.length === 0) break;        // the last call confirmed the final page: nothing is left
    for (const e of items) save(e);       // store first; skip an event_id you already stored
    ack = items.map((e) => e.event_id);   // confirm them with the next call
  }
  ```

  ```python Python theme={null}
  ack = []
  while True:
      res = call("POST", "/api/v1/partner/events/pull", {"ack": ack, "limit": 100})
      items = res.json()["data"]["items"]
      if not items:
          break                            # the last call confirmed the final page: nothing is left
      for e in items:
          save(e)                          # store first; skip an event_id you already stored
      ack = [e["event_id"] for e in items] # confirm them with the next call
  ```

  ```bash cURL theme={null}
  pcall POST "/api/v1/partner/events/pull" '{ "ack": [], "limit": 100 }'
  ```

  ```json 200 Response theme={null}
  {
    "success": true,
    "data": {
      "items": [
        {
          "event_id": "evt_9c2e4a6b8d0f1234567890abcdef1234",
          "type": "candidate.result",
          "api_version": "2026-10-01",
          "created_at": "2026-10-05T12:41:11.000Z",
          "mode": "test",
          "version": 1,
          "sequence": 482913,
          "acknowledged": false,
          "data": {
            "candidate_id": "a3f1c9d2-7b44-4e0a-9c1d-5e6f7a8b9c01",
            "external_id": "cand-88231",
            "campaign_id": "asmnt_9f2c41d7a8b3e65f",
            "mandate_id": "JM_261009101612456H3N8W2L5C9V1Z4",
            "job_id": "job-10452",
            "workflow_id": "WF_BG_261009101530123K7M2P9Q4X7R5T8",
            "name": "Ravi Kumar",
            "status": "completed",
            "reason": null,
            "attempts": 3,
            "language": "tamil",
            "location_name": "Coimbatore",
            "metadata": { "ats_application_id": "app_991" },
            "started_at": "2026-10-05T09:30:00.000Z",
            "completed_at": "2026-10-05T12:41:10.000Z",
            "updated_at": "2026-10-05T12:41:10.000Z",
            "output": {
              "enrichment": {
                "qualified": true,
                "answers": {
                  "Do you have a valid driving licence?": "yes",
                  "Why do you want this job?": "I want a stable job near home"
                }
              },
              "skill_verification": {
                "verdict": "Good Fit",
                "total_score": 78,
                "is_partial": false,
                "answered": 3,
                "total": 3,
                "skills": [
                  { "name": "Forklift Basic Operation", "rating": "Strong" }
                ],
                "call_recording_url": null,
                "answers": [
                  {
                    "skill": "Forklift Basic Operation",
                    "question": "What do you check before lifting a load?",
                    "answer": "I check the load weight against the forklift rating and that the forks are level.",
                    "answer_native": null,
                    "rating": "Strong",
                    "summary": "Mentions weight limit and fork position.",
                    "video_url": "https://storage.googleapis.com/record-interviews/sess_91c2/q1.mp4"
                  }
                ]
              },
              "interest_collection": {
                "current_role": "Forklift Operator",
                "other_roles": [
                  { "role": "Store Keeper", "years": "1 to 2 years" }
                ]
              }
            }
          }
        }
      ],
      "count": 1,
      "acknowledged": 0,
      "pending": 1,
      "next_cursor": null,
      "has_more": false
    }
  }
  ```
</CodeGroup>

| Response field | Meaning |
| - | - |
| `items` | The events, oldest first. |
| `acknowledged` | How many of the `ack` ids this call confirmed for the first time. |
| `pending` | How many events you have not confirmed in all. |
| `next_cursor`, `has_more` | More events are waiting. Keep pulling. |

| Error | When |
| - | - |
| 400 `VALIDATION_FAILED` | An `ack` id that is not an event id, more than 100 ids, a bad date, or `limit` above 100. |

Can also return the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/events/pull-and-confirm-events)

***

### Get an event

`GET /events/{event_id}`

Fetch one event by its id.

**Scope:** `events:read`

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("GET", "/api/v1/partner/events/evt_9c2e4a6b8d0f1234567890abcdef1234");
  const { data } = await res.json();
  ```

  ```python Python theme={null}
  res = call("GET", "/api/v1/partner/events/evt_9c2e4a6b8d0f1234567890abcdef1234")
  data = res.json()["data"]
  ```

  ```bash cURL theme={null}
  pcall GET "/api/v1/partner/events/evt_9c2e4a6b8d0f1234567890abcdef1234"
  ```

  ```json 200 Response theme={null}
  {
    "success": true,
    "data": {
      "event_id": "evt_9c2e4a6b8d0f1234567890abcdef1234",
      "type": "candidate.result",
      "api_version": "2026-10-01",
      "created_at": "2026-10-05T12:41:11.000Z",
      "mode": "test",
      "version": 1,
      "sequence": 482913,
      "acknowledged": false,
      "data": {
        "candidate_id": "a3f1c9d2-7b44-4e0a-9c1d-5e6f7a8b9c01",
        "external_id": "cand-88231",
        "campaign_id": "asmnt_9f2c41d7a8b3e65f",
        "mandate_id": "JM_261009101612456H3N8W2L5C9V1Z4",
        "job_id": "job-10452",
        "workflow_id": "WF_BG_261009101530123K7M2P9Q4X7R5T8",
        "name": "Ravi Kumar",
        "status": "completed",
        "reason": null,
        "attempts": 3,
        "language": "tamil",
        "location_name": "Coimbatore",
        "metadata": { "ats_application_id": "app_991" },
        "started_at": "2026-10-05T09:30:00.000Z",
        "completed_at": "2026-10-05T12:41:10.000Z",
        "updated_at": "2026-10-05T12:41:10.000Z",
        "output": {
          "enrichment": {
            "qualified": true,
            "answers": {
              "Do you have a valid driving licence?": "yes",
              "Why do you want this job?": "I want a stable job near home"
            }
          },
          "skill_verification": {
            "verdict": "Good Fit",
            "total_score": 78,
            "is_partial": false,
            "answered": 3,
            "total": 3,
            "skills": [
              { "name": "Forklift Basic Operation", "rating": "Strong" }
            ],
            "call_recording_url": null,
            "answers": [
              {
                "skill": "Forklift Basic Operation",
                "question": "What do you check before lifting a load?",
                "answer": "I check the load weight against the forklift rating and that the forks are level.",
                "answer_native": null,
                "rating": "Strong",
                "summary": "Mentions weight limit and fork position.",
                "video_url": "https://storage.googleapis.com/record-interviews/sess_91c2/q1.mp4"
              }
            ]
          },
          "interest_collection": {
            "current_role": "Forklift Operator",
            "other_roles": [
              { "role": "Store Keeper", "years": "1 to 2 years" }
            ]
          }
        }
      }
    }
  }
  ```
</CodeGroup>

| Error | When |
| - | - |
| 404 `NOT_FOUND` | No such event (or older than 30 days). |

Can also return the [common errors](/blue-collar-api/reference/errors). [Full schema](/api-reference/events/get-an-event)


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