Batch API
The Batch endpoint allows you to submit multiple events in a single HTTP request. This is useful for data imports, historical backfills, and high-throughput server-side integrations where reducing HTTP overhead matters.
Endpoint
POST /v1/batchAuthentication
The Batch endpoint supports both authentication methods:
- HMAC-SHA256 (recommended for server-to-server) — same as the Server-Side Events endpoint
- Pipeline key via
Authorization: Bearer dk_...header
Request Format
The request body is a JSON object with a single batch array containing the events:
{
"batch": [
{
"type": "track",
"event": "Order Completed",
"userId": "user_001",
"properties": {
"order_id": "ORD-001",
"revenue": 99.99,
"currency": "USD"
},
"timestamp": "2026-02-24T10:00:00.000Z"
},
{
"type": "track",
"event": "Order Completed",
"userId": "user_002",
"properties": {
"order_id": "ORD-002",
"revenue": 249.50,
"currency": "USD"
},
"timestamp": "2026-02-24T10:05:00.000Z"
},
{
"type": "identify",
"userId": "user_001",
"traits": {
"email": "alice@example.com",
"name": "Alice Johnson",
"plan": "premium"
},
"timestamp": "2026-02-24T10:00:00.000Z"
}
]
}Each item in the batch array follows the same schema as a single event sent to the Server-Side Events endpoint.
Limits
| Constraint | Default |
|---|---|
| Maximum events per batch | 500 |
| Maximum payload size | 1 MB |
These are deployment settings with fixed upper bounds; see Size Limits for every limit and how to raise it.
If the batch exceeds the event limit, the entire request is rejected with a 413 Payload Too Large response. Split large imports into multiple requests, or use the server-side endpoint, which accepts 2,000 events and 10 MB per request by default.
Response
The response lists what happened to each event, by messageId:
{
"accepted": ["msg-001", "msg-002", "msg-003"],
"duplicates": 1,
"rejected": [
{ "message_id": "msg-003", "reason": "invalid" }
]
}| Field | Meaning |
|---|---|
accepted | Events you can stop sending: published, already received, or refused on their own (those are also in rejected). |
duplicates | How many events carried a messageId Signal had already received in the last 24 hours. They are acknowledged and not published again. |
rejected | Events refused on their own, with the reason. The rest of the batch is accepted. |
An event that cannot be accepted on its own is refused without failing the rest of the batch:
| Reason | Meaning |
|---|---|
invalid | The event could not be read, for example a timestamp that is not RFC 3339 or a number where text is expected. |
too_deep | Properties or traits nested more deeply than the deployment allows. |
too_many_properties | More properties and traits than the deployment allows. |
property_too_large | A property value longer than the deployment allows. |
unknown_type | type is not one of page, track, screen, identify or group. |
too_large | The event is larger than the event queue accepts as one message (about 1 MB once Signal has added its own fields). |
Refused events appear in the Event Debugger with the reason.
Status codes:
| Status | Meaning |
|---|---|
200 | Every event is published, a duplicate, or listed in rejected. |
400 | The body is not valid JSON, or has no batch array. |
401 / 403 | Authentication failed, or the pipeline refuses the request. |
413 | The body or the number of events is over the limit. Split the batch. |
429 | Rate-limited. Send again after Retry-After seconds. |
503 | Send the whole batch again after Retry-After seconds. Not every event could be written yet: the event queue failed, an earlier request is still processing one of them, or a backend Signal needs to check the request was unavailable. The body lists what was accepted. |
A 503 never means some events were dropped. Resending the whole batch is safe when every event carries a messageId: events already received are acknowledged as duplicates and published once. An event without a messageId is given a new one on arrival, so it cannot be de-duplicated.
The body may be sent as application/json or as text/plain, which is what a browser’s sendBeacon uses.
Example: Historical Backfill
Here is a Node.js example that reads events from a JSON file and submits them in batches of 500:
import crypto from "node:crypto";
import { readFileSync } from "node:fs";
const SECRET_KEY = "sk_live_your_secret_key";
const ENDPOINT = "https://data.example.com/v1/batch";
const BATCH_SIZE = 500;
async function sendBatch(events) {
const timestamp = Date.now().toString();
const body = JSON.stringify({ batch: events });
const signature = crypto
.createHmac("sha256", SECRET_KEY)
.update(`${timestamp}.${body}`)
.digest("hex");
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Signature": `sha256=${signature}`,
"X-Timestamp": timestamp,
},
body,
});
return response.json();
}
// Load events from a JSON file
const allEvents = JSON.parse(readFileSync("events.json", "utf-8"));
// Send in batches of 500
for (let i = 0; i < allEvents.length; i += BATCH_SIZE) {
const batch = allEvents.slice(i, i + BATCH_SIZE);
const result = await sendBatch(batch);
console.log(
`Batch ${Math.floor(i / BATCH_SIZE) + 1}: ${result.accepted.length} accepted`
);
if (result.rejected?.length) {
console.warn("Refused:", result.rejected);
}
}Example: Python Backfill
import hashlib
import hmac
import json
import time
import requests
SECRET_KEY = "sk_live_your_secret_key"
ENDPOINT = "https://data.example.com/v1/batch"
BATCH_SIZE = 500
def send_batch(events: list[dict]) -> dict:
timestamp = str(int(time.time() * 1000))
body = json.dumps({"batch": events}, separators=(",", ":"))
signature = hmac.new(
SECRET_KEY.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256,
).hexdigest()
response = requests.post(
ENDPOINT,
headers={
"Content-Type": "application/json",
"X-Signature": f"sha256={signature}",
"X-Timestamp": timestamp,
},
data=body,
)
return response.json()
# Load events
with open("events.json") as f:
all_events = json.load(f)
# Send in batches
for i in range(0, len(all_events), BATCH_SIZE):
batch = all_events[i : i + BATCH_SIZE]
result = send_batch(batch)
print(f"Batch {i // BATCH_SIZE + 1}: {len(result['accepted'])} accepted")Use Cases
| Use Case | Description |
|---|---|
| Historical backfill | Import past events from a data warehouse or CSV export |
| Data migration | Migrate events from another analytics platform |
| High-throughput ingestion | Reduce HTTP overhead by batching events from a busy server |
| Periodic sync | Batch events collected offline and send them on a schedule |
Best Practices
- Set accurate timestamps. For historical imports, always include the
timestampfield with the original event time. The processing layer and downstream vendors rely on this for attribution. - Include
messageIdfor deduplication. If you might retry a batch (e.g. after a network error), include amessageIdon each event so the Signal can deduplicate. - Respect rate limits. If you receive a
429response, back off using theRetry-Afterheader value before sending the next batch. - Resend on
503. Send the same batch again, with the samemessageIdvalues, afterRetry-Afterseconds. - Monitor refused events. Check the
rejectedarray in the response and fix or log the events it lists.