Sending events over HTTP
The browser SDKs are a convenience wrapper around four plain HTTP endpoints. If you’re tracking from a backend, a queue consumer, or a language with no Pivolio SDK, call them directly.
Every ingestion endpoint authenticates with a workspace write key in the
X-Write-Key header — see Authentication.
The four required fields
An event body is a UnifiedEvent. Four fields are required on every single
event, and omitting any one of them returns 422:
A unique ID for this event, used for deduplication. The SDKs generate ULIDs (26 characters, time-sortable), which is a good default.
Must match ^[a-zA-Z0-9_.]{1,128}$. Prefer a
standard event name — custom names still record and
report, but destinations only map the standard ones.
Unix time in seconds (a float is fine). Milliseconds is the most common mistake here — a millisecond timestamp puts your event roughly 50,000 years into the future and destinations will reject it.
The device or visitor identifier. Call /id to get a
server-issued one if you don’t already have it.
Everything else is optional: user_id, traits, context, consent,
properties, test_mode, schema_version (defaults to "1").
context.ip is set by the collector from the connecting socket. A value you
supply is always overwritten — proxy the request from the client’s IP if you
need real geo resolution.
POST /track
Sends exactly one event.
Send raw email and phone values. Pivolio normalizes and hashes them server-side so every destination gets byte-identical digests — hashing them yourself is the fastest way to quietly destroy your match rates.
Every response has the same shape:
POST /batch
Takes an array of the same objects, maximum 500 per request. Use it when you’re replaying a backlog or flushing a buffer — one connection instead of 500.
GET /id
Returns a server-issued anonymous ID and sets it as an httpOnly _at_aid
cookie with a 365-day lifetime.
The reason this endpoint exists: Safari’s tracking prevention caps script-written cookies at about seven days, so a browser-generated ID quietly resets and splits one visitor into many. A server-set httpOnly cookie survives far longer.
GET /pixel
For contexts that can’t run JavaScript or issue a POST — an email open, an AMP
page, a third-party template. Pass the write key as wk in the query string,
or in the header as usual.
With format=img the response is a 1×1 transparent GIF, so the URL can go
straight into an <img> tag. Without it you get the same JSON envelope as
/track.
/pixel accepts no PII. Traits like email and phone are not read from the
query string — a URL travels through referrer headers, proxy logs and CDN
caches, so it’s the wrong place for them. Any conversion that carries PII must
go through /track or /batch.
Deduplication
Idempotency is keyed on (workspace, event_id). Re-sending an event you already
delivered is safe — it is recorded once, and the response tells you what
happened:
This is what makes retries safe. If a request times out, resend it with the
same event_id rather than minting a new one.
Rate limits
There are two independent budgets, and they count different things:
- Per IP — counts requests. A batch of 500 events is one request.
- Per workspace — counts events. A batch of 500 events is 500.
Exceeding either returns 429 with a detail of
"Rate limit exceeded (IP)" or "Rate limit exceeded (workspace)", so you can
tell which one you hit. Batching is the cheapest way to stay under the IP
budget; it does nothing for the workspace budget.
See Errors for the full error envelope.