HTTP API
Ingest API
Send events via HTTP when an SDK isn't an option — server-side tracking, Roku, HbbTV, beacons.
Base URL
text
https://ingest.tapemetric.comAuthentication
Send your API key in the X-API-Key header. See Authentication.
POST /v1/ingest/events
Send a batch of events. Max 500 events per batch.
Request
bash
curl -X POST https://ingest.tapemetric.com/v1/ingest/events \
-H "X-API-Key: tm_live_..." \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"event_type": "play_start",
"ts": "2026-04-23T18:30:12.456Z",
"anonymous_id": "a8f3c2d0-1e5b-4a9c-8d4e-7f2a1b3c4d5e",
"session_id": "sess_a1b2c3d4",
"content_id": "aashiqana_s04e12",
"content_type": "series",
"content_title": "Aashiqana S4 E12",
"position_sec": 0,
"bitrate_kbps": 4500,
"context": { "device_type": "tv", "app_version": "4.12.0" }
}
]
}'Response (200)
json
{
"accepted": 1,
"rejected": 0,
"queued": true
}Error responses
| Status | Meaning |
|---|---|
| 400 | Malformed JSON or event validation failed |
| 401 | Missing or invalid API key |
| 403 | Tenant inactive or quota exceeded |
| 429 | Rate limit hit — retry with exponential backoff |
| 503 | Ingest degraded — retry later |
POST /v1/ingest/beacon
Optimized for navigator.sendBeacon on page unload. Accepts URL-encoded JSON in adata query parameter and always responds 200 even on error (per beacon spec).
javascript
const payload = JSON.stringify({ events: [...] });
navigator.sendBeacon(
'https://ingest.tapemetric.com/v1/ingest/beacon?api_key=tm_live_...',
new Blob([payload], { type: 'application/json' }),
);Batching & rate limits
- Batch size: max 500 events per call.
- Rate limit: 120,000 events per minute per tenant by default. Enterprise plans raise this.
- Quota: monthly event quota per plan. 429 responses when exceeded.
Idempotency
Include an event_id field (UUID) if you want the ingest endpoint to de-duplicate within a 24-hour window. Without event_id, retries may double-count.
json
{
"event_id": "a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890",
"event_type": "play_start",
"ts": "2026-04-23T18:30:12Z",
"anonymous_id": "...",
"session_id": "..."
}Clock skew: clients with system clocks off by more than ±30 seconds will have their timestamps corrected server-side. For strict ordering use server-generated ts via the
?server_ts=1 query flag.Server-side tracking (Node.js)
typescript
import { randomUUID } from 'crypto';
async function track(userId: string, eventType: string, props: Record<string, unknown>) {
await fetch('https://ingest.tapemetric.com/v1/ingest/events', {
method: 'POST',
headers: {
'X-API-Key': process.env.TAPEMETRIC_INGEST_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({
events: [{
event_id: randomUUID(),
event_type: eventType,
ts: new Date().toISOString(),
anonymous_id: userId,
user_id: userId,
session_id: `server-${userId}`,
properties: props,
}],
}),
});
}Server-side tracking (Python)
python
import os, httpx
from datetime import datetime, timezone
from uuid import uuid4
INGEST = "https://ingest.tapemetric.com/v1/ingest/events"
async def track(user_id: str, event_type: str, props: dict):
async with httpx.AsyncClient() as client:
await client.post(INGEST, json={"events": [{
"event_id": str(uuid4()),
"event_type": event_type,
"ts": datetime.now(timezone.utc).isoformat(),
"anonymous_id": user_id,
"user_id": user_id,
"session_id": f"server-{user_id}",
"properties": props,
}]}, headers={"X-API-Key": os.environ["TAPEMETRIC_INGEST_KEY"]})