Skip to main content
POST
/
api
/
v1
/
users
/
bulk
Bulk upsert users
curl --request POST \
  --url https://api.example.com/api/v1/users/bulk \
  --header 'Content-Type: application/json' \
  --data '
{
  "users": [
    {}
  ]
}
'
{
  "data.accepted": 123,
  "data.rejected": 123,
  "data.errors": [
    {}
  ],
  "meta.received": 123
}

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.

The backfill counterpart to single-record upsert. Send up to 1,000 user records in one POST; per-record validation errors are surfaced in the response, the rest are queued.
SDK is the recommended path. vf.users.identifyBulk() wraps this endpoint with strong typing and pairs cleanly with the exported chunk() helper for backfills larger than 1,000 records.

Request

POST /api/v1/users/bulk HTTP/1.1
Host: api.vibefollow.com
Authorization: Bearer sk_live_••••
Content-Type: application/json

Body

{
  "users": [
    {
      "external_user_id": "usr_42",
      "email": "jane@acme.io",
      "traits": { "name": "Jane Doe", "plan": "pro" }
    },
    {
      "external_user_id": "usr_43",
      "email": "noel@acme.io",
      "traits": { "name": "Noel Carter", "plan": "trial" }
    }
  ]
}
users
User[]
required
Array of user records. Each entry has the same shape as the single-record body. Minimum 1, maximum 1,000 records.

Response

HTTP/1.1 200 OK
Content-Type: application/json
{
  "data": {
    "accepted": 487,
    "rejected": 13,
    "errors": [
      { "index": 12, "code": "validation", "message": "Invalid email", "field": "email" },
      { "index": 47, "code": "validation", "message": "external_user_id must be at least 1 character" }
    ]
  },
  "meta": { "received": 500 }
}
data.accepted
number
Records that passed validation and were enqueued.
data.rejected
number
Records that failed validation. Each has a corresponding entry in data.errors.
data.errors
object[]
Per-record validation failures. index correlates back to the position in the input users array — the record at that position was not enqueued.
meta.received
number
Total records in the request payload. accepted + rejected === meta.received is an invariant — every record is accounted for.
Idempotency is automatic. The (project_id, external_user_id) uniqueness constraint on tracked_users means re-sending the same record with the same external_user_id is treated as an update, not a duplicate. Any chunk that returns 429 or 5xx is safe to retry.

Examples

import { VibeFollow, chunk } from '@vibefollow/sdk';

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

// Backfill any size by pairing with chunk()
for (const slice of chunk(allUsers, 1000)) {
  const result = await vf.users.identifyBulk(slice);
  if (result.data.errors.length > 0) {
    console.warn('skipped', result.data.errors);
  }
}

Common errors

Missing or malformed Authorization header.
The envelope failed validation — users is missing, not an array, empty, or contains more than 1,000 records. Per-record validation failures do not trigger 400; they surface in data.errors.
Standard per-project rate limit; honor Retry-After.
The project’s ingest queue is saturated (50,000+ jobs waiting or delayed). Back off, drain, then resume in smaller chunks.
Retry the same chunk — the upsert is idempotent via the unique (project_id, external_user_id) constraint.

Sizing

Max records per request

1,000

Recommended chunk size

1,000

Backfill queue cap

50,000 waiting jobs / project