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

# Bulk track events

> POST /api/v1/events/bulk &mdash; backfill up to 1,000 events in one request.

The backfill counterpart to [single-event track](/api-reference/events/track). Send up to **1,000 event records** in one POST. Replaces the older [`/api/v1/events/batch`](/api-reference/events/batch) endpoint for new integrations — the bulk endpoint reports per-record errors instead of failing the whole batch.

<Info>
  **SDK is the recommended path.** [`vf.events.bulk()`](/sdk/resources/events#bulk-records) wraps this endpoint with strong typing and pairs with [`chunk()`](/sdk/types#chunk-input-size) for backfills larger than 1,000 records.
</Info>

<Snippet file="auth-headers.mdx" />

## Request

```http theme={null}
POST /api/v1/events/bulk HTTP/1.1
Host: api.vibefollow.com
Authorization: Bearer sk_live_••••
Content-Type: application/json
```

### Body

```json theme={null}
{
  "events": [
    {
      "external_user_id": "usr_42",
      "name": "user_signed_up",
      "timestamp": "2024-01-15T10:00:00Z",
      "properties": { "plan": "trial" }
    },
    {
      "external_user_id": "usr_42",
      "name": "trial_started",
      "timestamp": "2024-01-15T10:00:05Z",
      "properties": { "trialDays": 14 }
    }
  ]
}
```

<ParamField body="events" type="Event[]" required>
  Array of event records. Each entry has the same shape as the [single-event body](/api-reference/events/track#body). **Minimum 1, maximum 1,000 records.**
</ParamField>

## Response

```http theme={null}
HTTP/1.1 200 OK
Content-Type: application/json
```

```json theme={null}
{
  "data": {
    "accepted": 998,
    "rejected": 2,
    "errors": [
      { "index": 17, "code": "validation", "message": "name must be at least 1 character", "field": "name" },
      { "index": 423, "code": "validation", "message": "Invalid timestamp" }
    ]
  },
  "meta": { "received": 1000 }
}
```

<ResponseField name="data.accepted" type="number">
  Records that passed validation and were enqueued.
</ResponseField>

<ResponseField name="data.rejected" type="number">
  Records that failed validation. Each has a corresponding entry in `data.errors`.
</ResponseField>

<ResponseField name="data.errors" type="object[]">
  Per-record validation failures with the original array `index`.
</ResponseField>

<ResponseField name="meta.received" type="number">
  Total records in the request. `accepted + rejected === meta.received`.
</ResponseField>

<Note>
  **Idempotency is automatic.** The server-side `events_dedupe_unique` partial index on `(project_id, tracked_user_id, name, occurred_at)` means re-sending the same event is a no-op. Retry any chunk that returns `429` or `5xx` — duplicates are silently dropped.
</Note>

## Examples

<CodeGroup>
  ```ts Node (SDK) theme={null}
  import { VibeFollow, chunk } from '@vibefollow/sdk';

  const vf = new VibeFollow({ apiKey: process.env.VIBEFOLLOW_API_KEY! });

  for (const slice of chunk(historicalEvents, 1000)) {
    await vf.events.bulk(slice);
  }
  ```

  ```bash cURL theme={null}
  curl https://api.vibefollow.com/api/v1/events/bulk \
    -X POST \
    -H "Authorization: Bearer $VIBEFOLLOW_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "events": [
        {
          "external_user_id": "usr_42",
          "name": "user_signed_up",
          "timestamp": "2024-01-15T10:00:00Z"
        },
        {
          "external_user_id": "usr_42",
          "name": "trial_started",
          "timestamp": "2024-01-15T10:00:05Z"
        }
      ]
    }'
  ```
</CodeGroup>

## Common errors

<AccordionGroup>
  <Accordion title="401 auth_required" icon="key">
    Missing or malformed `Authorization` header.
  </Accordion>

  <Accordion title="400 validation_failed" icon="circle-exclamation">
    Envelope failed validation: `events` missing, not an array, empty, or > 1,000 records. Per-record validation failures surface in `data.errors`.
  </Accordion>

  <Accordion title="429 rate_limited" icon="gauge-high">
    Standard per-project rate limit; honor `Retry-After`.
  </Accordion>

  <Accordion title="429 backfill_queue_full" icon="layer-group">
    The project's ingest queue is saturated (50,000+ jobs waiting or delayed). Back off, drain, then resume in smaller chunks.
  </Accordion>

  <Accordion title="5xx server_error" icon="server">
    Retry the same chunk — per-event dedup makes duplicate ingestion a no-op.
  </Accordion>
</AccordionGroup>

## Sizing

<CardGroup cols={3}>
  <Card title="Max records per request" icon="layer-group">
    1,000
  </Card>

  <Card title="Recommended chunk size" icon="ruler-horizontal">
    1,000
  </Card>

  <Card title="Backfill queue cap" icon="gauge">
    50,000 waiting jobs / project
  </Card>
</CardGroup>
