Skip to content

batch

Two things make a backfill safe: historical timestamps on any write, and one request carrying up to 500 calls.

await cow.batch({
items: [
{ type: "identify", identifiers: { userId: "user_1" }, traits: { plan: "pro" } },
{
type: "track",
identifiers: { userId: "user_1" },
event: "subscription_started",
timestamp: "2026-03-01T00:00:00.000Z",
messageId: "stripe-evt_0001",
},
],
});

One batch carries one source, so it never spans apps.

identify and track both accept timestamp. On a track it becomes the event’s own time (receivedAt still records when Cowliss saw the call). On an identify a first-write profile is backdated; an existing profile keeps its original createdAt.

A timestamp in the future is refused (with 60 seconds of clock-skew tolerance), and so is one older than your retention window, which is 18 months at most. See the settings reference.

{
"data": {
"results": [
{ "ok": true, "type": "identify", "id": "usr_…" },
{ "ok": true, "type": "track", "id": "evt_…", "deduped": false },
{ "ok": false, "error": { "code": "trait_validation_failed", "message": "" } }
]
}
}

One bad row never fails the batch: it reports the error the single call would have, and the rest land. Request-level problems (malformed JSON, over 500 items, a bad envelope) are still 400 or 422 for the whole request.

Batch writes land in storage but skip raw-event journey triggers, so importing two years of history never mails long-gone users. Segment membership does recompute, because segments tell the truth, so an import that moves somebody into a segment can start a segment-triggered journey for them.

Building the segment afterwards is different: it catches up over existing history and starts nobody. Those people enter when somebody turns the journey on, with the count in front of them.

Rule of thumb: raw-event triggers are the live funnel, segment triggers are cohorts, imported ones included.

Give every track item a stable messageId from the source system (stripe-evt_0001, not a random UUID). A crashed import is then simply restarted: replays dedupe per item ("deduped": true) instead of double-writing. The request-level Idempotency-Key header does not apply to batch items, because one shared key would collapse them onto the first.

Backfilled events are metered like any other event you wrote. Events Cowliss records itself never are. See Billing.