Batch track events
curl --request POST \
--url https://api.example.com/api/v1/events/batch \
--header 'Content-Type: application/json' \
--data '
{
"events": [
{}
],
"suppressTriggers": true
}
'import requests
url = "https://api.example.com/api/v1/events/batch"
payload = {
"events": [{}],
"suppressTriggers": True
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({events: [{}], suppressTriggers: true})
};
fetch('https://api.example.com/api/v1/events/batch', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/events/batch",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'events' => [
[
]
],
'suppressTriggers' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/events/batch"
payload := strings.NewReader("{\n \"events\": [\n {}\n ],\n \"suppressTriggers\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/events/batch")
.header("Content-Type", "application/json")
.body("{\n \"events\": [\n {}\n ],\n \"suppressTriggers\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/events/batch")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"events\": [\n {}\n ],\n \"suppressTriggers\": true\n}"
response = http.request(request)
puts response.read_body{
"data.accepted": true
}Events
Batch track events
POST /api/v1/events/batch — emit multiple events in one POST.
POST
/
api
/
v1
/
events
/
batch
Batch track events
curl --request POST \
--url https://api.example.com/api/v1/events/batch \
--header 'Content-Type: application/json' \
--data '
{
"events": [
{}
],
"suppressTriggers": true
}
'import requests
url = "https://api.example.com/api/v1/events/batch"
payload = {
"events": [{}],
"suppressTriggers": True
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({events: [{}], suppressTriggers: true})
};
fetch('https://api.example.com/api/v1/events/batch', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.example.com/api/v1/events/batch",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'events' => [
[
]
],
'suppressTriggers' => true
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.example.com/api/v1/events/batch"
payload := strings.NewReader("{\n \"events\": [\n {}\n ],\n \"suppressTriggers\": true\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.example.com/api/v1/events/batch")
.header("Content-Type", "application/json")
.body("{\n \"events\": [\n {}\n ],\n \"suppressTriggers\": true\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/events/batch")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"events\": [\n {}\n ],\n \"suppressTriggers\": true\n}"
response = http.request(request)
puts response.read_body{
"data.accepted": true
}Endpoint behaviour is stable; the SDK batching wrapper is the recommended path today. Use
vf.events.batch() for buffered emission — it auto-flushes by size and age and handles retries.Request
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
{
"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
}
Event[]
required
Array of events. Each event has the same shape as the single-event endpoint body (
external_user_id, name, properties). Maximum 100 events per batch.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.Response
HTTP/1.1 202 Accepted
Content-Type: application/json
{ "data": { "accepted": true } }
boolean
Always
true on success. The response confirms the batch was enqueued. Per-event validation happens server-side.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).Examples
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();
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();
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
}'
# `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
}'
Common errors
401 auth_required
401 auth_required
Missing or malformed
Authorization header.422 validation_failed
422 validation_failed
Any event in the batch failed validation;
errors[0].field indicates the offending index.429 rate_limited
429 rate_limited
Wait
Retry-After seconds.5xx server_error
5xx server_error
Retry — the
Idempotency-Key prevents duplicates.Sizing
Max events per batch
100
Max event name length
64 characters
Max body size
5 MB
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.