> ## 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.

# Batch track events

> POST /api/v1/events/batch &mdash; emit multiple events in one POST.

<Warning>
  **Endpoint behaviour is stable; the SDK batching wrapper is the recommended path today.** Use [`vf.events.batch()`](/sdk/resources/events#batch) for buffered emission — it auto-flushes by size and age and handles retries.
</Warning>

Emit multiple events in a single POST. Useful for backfills, data-warehouse syncs, and any path where per-event HTTP overhead dominates.

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

## Request

```http theme={null}
POST /api/v1/events/batch HTTP/1.1
Host: api.vibefollow.com
Authorization: Bearer sk_live_••••
Content-Type: application/json
Idempotency-Key: 7b6c7e9e-2c2d-4a8d-9d3a-4f3d2c1b0a99
```

### Body

```json theme={null}
{
  "events": [
    {
      "external_user_id": "usr_42",
      "name": "user_signed_up",
      "properties": { "plan": "trial" }
    },
    {
      "external_user_id": "usr_42",
      "name": "trial_started",
      "properties": { "trialDays": 14 }
    }
  ],
  "suppressTriggers": false
}
```

<ParamField body="events" type="Event[]" required>
  Array of events. Each event has the same shape as the [single-event endpoint](/api-reference/events/track) body (`external_user_id`, `name`, `properties`). Maximum 100 events per batch.
</ParamField>

<ParamField body="suppressTriggers" type="boolean" default="false">
  When `true`, every tracked user touched by this batch is marked as **historical** — trigger rules (welcome, onboarding stall, etc.) will **not** fire for those users retroactively, even if the events would otherwise match. Use this when seeding your existing user base into Vibefollow so customers don't get a "welcome" email weeks or months after they actually signed up.

  Mechanics: every `TrackedUser` created or first-seen by a suppressed batch gets `suppressTriggersUntil` set to a far-future date. The trigger-rules cron skips users with a non-null `suppressTriggersUntil`. The watermark dissolves automatically the first time the user emits a normal (non-suppressed) event, so going live after a backfill is a one-flip switch.

  Defaults to `false` — live signups DO fire welcomes, the normal case.
</ParamField>

## Response

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

```json theme={null}
{ "data": { "accepted": true } }
```

<ResponseField name="data.accepted" type="boolean">
  Always `true` on success. The response confirms the **batch** was enqueued. Per-event validation happens server-side.
</ResponseField>

<Note>
  If one event in the batch fails validation, the whole batch is rejected with `422` and `errors[0].field` indicates the offending event index (e.g. `events[3].external_user_id`).
</Note>

## Examples

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

  const vf = new VibeFollow({ apiKey: process.env.VIBEFOLLOW_API_KEY! });
  const batch = vf.events.batch({ maxSize: 100, maxAgeMs: 5_000 });

  for (const row of historicalEvents) {
    batch.track(row.name, row.userId, row.properties);
  }

  await batch.flush();
  ```

  ```ts Seeding historical users (SDK) theme={null}
  import { VibeFollow } from '@vibefollow/sdk';

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

  // `suppressTriggers: true` marks every user in this backfill as
  // historical — trigger rules (welcome, onboarding_stall, etc.) won't
  // fire retroactively. Once you're done seeding, flip back to the
  // default and live signups resume firing welcomes as normal.
  const backfill = vf.events.batch({
    maxSize: 100,
    maxAgeMs: 5_000,
    suppressTriggers: true,
  });

  for (const row of existingUsers) {
    backfill.track('user_signed_up', row.id, { signedUpAt: row.createdAt });
  }

  await backfill.flush();
  ```

  ```bash cURL theme={null}
  curl https://api.vibefollow.com/api/v1/events/batch \
    -X POST \
    -H "Authorization: Bearer $VIBEFOLLOW_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" \
    -d '{
      "events": [
        {
          "external_user_id": "usr_42",
          "name": "user_signed_up",
          "properties": { "plan": "trial" }
        },
        {
          "external_user_id": "usr_43",
          "name": "user_signed_up",
          "properties": { "plan": "trial" }
        }
      ],
      "suppressTriggers": false
    }'
  ```

  ```bash cURL — seeding historical users theme={null}
  # `suppressTriggers: true` marks every user in this backfill as
  # historical so trigger rules don't fire retroactively.
  curl https://api.vibefollow.com/api/v1/events/batch \
    -X POST \
    -H "Authorization: Bearer $VIBEFOLLOW_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" \
    -d '{
      "events": [
        { "external_user_id": "usr_old_1", "name": "user_signed_up" },
        { "external_user_id": "usr_old_2", "name": "user_signed_up" }
      ],
      "suppressTriggers": true
    }'
  ```
</CodeGroup>

## Common errors

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

  <Accordion title="422 validation_failed" icon="circle-exclamation">
    Any event in the batch failed validation; `errors[0].field` indicates the offending index.
  </Accordion>

  <Accordion title="429 rate_limited" icon="gauge-high">
    Wait `Retry-After` seconds.
  </Accordion>

  <Accordion title="5xx server_error" icon="server">
    Retry — the `Idempotency-Key` prevents duplicates.
  </Accordion>
</AccordionGroup>

## Sizing

<CardGroup cols={3}>
  <Card title="Max events per batch" icon="layer-group">
    100
  </Card>

  <Card title="Max event name length" icon="font">
    64 characters
  </Card>

  <Card title="Max body size" icon="weight-scale">
    5 MB
  </Card>
</CardGroup>

<Info>
  If you hit these limits, split the batch on your side. The SDK's `events.batch()` defaults to `maxSize: 100`, which matches the server-side ceiling exactly.
</Info>
