Bulk track events
curl --request POST \
--url https://api.example.com/api/v1/events/bulk \
--header 'Content-Type: application/json' \
--data '
{
"events": [
{}
]
}
'import requests
url = "https://api.example.com/api/v1/events/bulk"
payload = { "events": [{}] }
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: [{}]})
};
fetch('https://api.example.com/api/v1/events/bulk', 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/bulk",
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' => [
[
]
]
]),
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/bulk"
payload := strings.NewReader("{\n \"events\": [\n {}\n ]\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/bulk")
.header("Content-Type", "application/json")
.body("{\n \"events\": [\n {}\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/events/bulk")
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}"
response = http.request(request)
puts response.read_body{
"data.accepted": 123,
"data.rejected": 123,
"data.errors": [
{}
],
"meta.received": 123
}Events
Bulk track events
POST /api/v1/events/bulk — backfill up to 1,000 events in one request.
POST
/
api
/
v1
/
events
/
bulk
Bulk track events
curl --request POST \
--url https://api.example.com/api/v1/events/bulk \
--header 'Content-Type: application/json' \
--data '
{
"events": [
{}
]
}
'import requests
url = "https://api.example.com/api/v1/events/bulk"
payload = { "events": [{}] }
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: [{}]})
};
fetch('https://api.example.com/api/v1/events/bulk', 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/bulk",
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' => [
[
]
]
]),
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/bulk"
payload := strings.NewReader("{\n \"events\": [\n {}\n ]\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/bulk")
.header("Content-Type", "application/json")
.body("{\n \"events\": [\n {}\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/api/v1/events/bulk")
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}"
response = http.request(request)
puts response.read_body{
"data.accepted": 123,
"data.rejected": 123,
"data.errors": [
{}
],
"meta.received": 123
}The backfill counterpart to single-event track. Send up to 1,000 event records in one POST. Replaces the older
/api/v1/events/batch endpoint for new integrations — the bulk endpoint reports per-record errors instead of failing the whole batch.
SDK is the recommended path.
vf.events.bulk() wraps this endpoint with strong typing and pairs with chunk() for backfills larger than 1,000 records.Request
POST /api/v1/events/bulk HTTP/1.1
Host: api.vibefollow.com
Authorization: Bearer sk_live_••••
Content-Type: application/json
Body
{
"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 }
}
]
}
Event[]
required
Array of event records. Each entry has the same shape as the single-event body. Minimum 1, maximum 1,000 records.
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"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 }
}
number
Records that passed validation and were enqueued.
number
Records that failed validation. Each has a corresponding entry in
data.errors.object[]
Per-record validation failures with the original array
index.number
Total records in the request.
accepted + rejected === meta.received.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.Examples
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);
}
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"
}
]
}'
Common errors
401 auth_required
401 auth_required
Missing or malformed
Authorization header.400 validation_failed
400 validation_failed
Envelope failed validation:
events missing, not an array, empty, or > 1,000 records. Per-record validation failures surface in data.errors.429 rate_limited
429 rate_limited
Standard per-project rate limit; honor
Retry-After.429 backfill_queue_full
429 backfill_queue_full
The project’s ingest queue is saturated (50,000+ jobs waiting or delayed). Back off, drain, then resume in smaller chunks.
5xx server_error
5xx server_error
Retry the same chunk — per-event dedup makes duplicate ingestion a no-op.
Sizing
Max records per request
1,000
Recommended chunk size
1,000
Backfill queue cap
50,000 waiting jobs / project