REST ingest API
The public endpoints the SDK uses - and that your backend can use too.
Base URL: your PixelFox ingest API (e.g. https://api.pixelfox.app). Authenticate with the
X-PixelFox-Key header, or ?key= where headers aren't possible. CORS is open; origin
allow-listing is enforced per key (dashboard → Settings).
POST /v1/ingest
Send up to 50 events per request (body cap 256 KiB):
curl -X POST https://api.pixelfox.app/v1/ingest \
-H "Content-Type: application/json" \
-H "X-PixelFox-Key: pfx_live_…" \
-d '{
"events": [{
"event": "purchase",
"uuid": "3f1c…-…",
"distinct_id": "user-123",
"timestamp": "2026-08-05T12:00:00Z",
"properties": {
"order_id": "1001",
"total": 129.5,
"currency": "USD",
"email": "[email protected]"
}
}]
}'Response: {"accepted": 1} - returned as soon as the batch is accepted; destination
fan-out happens asynchronously. When the account's monthly event allowance (plus a 20%
grace margin) is exhausted, the excess is dropped and reported as
{"accepted": n, "dropped": m}; bot traffic and events matching the site's block rules
answer {"accepted": 0} without an error.
| Field | Required | Notes |
|---|---|---|
event | ✅ | Event name (see conventions) |
uuid | ✅ | Unique id per event - the browser↔server dedup key. Omit only if no browser pixel will ever fire this event |
distinct_id | - | Visitor/user id; defaults to an anonymous id derived server-side |
timestamp | - | ISO 8601; defaults to arrival time |
properties | - | Event properties. email/phone here trigger identity stitching on identify-type events |
Errors: 401 missing key · 403 invalid key or origin not allowed · 413 too many
events / body too large · 429 rate limited (200 req/10s per IP, 1000 req/10s per key).
GET /v1/config
What the SDK fetches on load - public per-site config, never secrets:
{
"mode": "online",
"business_category": "ecommerce_retail",
"clarity": { "project_id": "…" } | null,
"facebook": { "pixel_id": "…", "route_pixels": [{ "pixel_id": "…", "url_pattern": "/eu/*" }] } | null,
"tiktok": { "pixel_code": "…" } | null,
"ga4": { "measurement_id": "…" } | null,
"gtm": { "container_id": "GTM-…" } | null,
"custom_loader": false,
"events": [ { "id": "…", "name": "hero_cta_clicked", "trigger": "click",
"selector": ".hero .btn", "url_pattern": "/", "text_contains": "",
"properties": {}, "conversion_event": "lead",
"destinations": { "tiktok": false },
"value_selector": "", "field_selectors": { "email": "input[name=email]" },
"enabled": true } ]
}mode is online or offline (offline sites are anonymous-only and the edge strips
identifiers). ga4 is null when the destination routes server-side, so the tag does
not load gtag. custom_loader is true when a first-party domain is configured and the
tag should source vendor scripts through it. Per-definition destinations flags
(absent = send) and the value_selector / field_selectors read at fire time are
described under event definitions.
business_category is the site's Meta-style business category (ecommerce_retail,
entertainment_media, education, real_estate, technology, financial_services,
automotive, travel, other), set in the dashboard's site settings. It is
informational - integrations use it to surface the conversion events recommended for
that kind of business; capture behaves identically for every category.
POST /v1/inventory
Used by the SDK's auto-discovery; body
{ page, url, title, elements: [{ type, selector, text, href?, event_hint? }] } with at
most 300 elements. Deduplicated server-side per (key, page) content hash. Response:
{"accepted": n}.
When a site is past the plan's site limit
A plan includes a number of sites. If an account has more than that, its oldest
sites keep collecting and the rest answer 200 with
{"accepted": 0, "reason": "site_over_plan_limit"} until the account upgrades
or removes a site. It is a 200, not an error, so a tag does not retry; no data
already collected is removed.
GET /v1/health
{"ok": true, "ratelimit": "redis" | "memory", "queue": "rabbitmq" | "inline"} - for uptime
checks. ratelimit says whether rate-limit and quota counters are shared across replicas;
queue says whether background work is going through the durable queue or being done
inline because no broker is configured or reachable. Neither value affects whether events
are accepted.
Forwarded hits
/g/collect normally identifies the site by the hostname it was called on. A
server-side GTM container forwarding events instead authenticates itself, and
says who the visitor was:
| Header | Value |
|---|---|
X-PixelFox-Forward-Secret | The container's forwarding secret (pff_…). Generated in the dashboard, shown once, stored only as a hash. |
X-PixelFox-Visitor-IP | The visitor's IP address. |
X-PixelFox-Visitor-UA | The visitor's user agent. |
The two visitor headers are honoured only alongside a valid forwarding secret. They are ignored on every other request, including one carrying an API key: an API key is public, so it cannot be allowed to vouch for an IP address.
A forwarded hit differs from a direct one in four ways: geo, block rules and
bot filtering use the stated visitor rather than the connection; the rate
limit is per visitor IP and per container; no first-party cookies are set on
the response; and it is never sent on to GA4 (and to no destination at all if
the container's forwarding fan-out is off). An invalid secret is 403.
A request may carry several events: shared parameters in the query string and
one event per line in a POST body (GA4's own batch format), up to 20.
Other public endpoints
| Route | Purpose |
|---|---|
GET /pixelfox.min.js, /pixelfox.mjs, /pixelfox-engage.min.js (also under /v1/) | The tag itself, with ETag and a 5-minute cache |
GET /v1/script-status | Whether the tag is built, its size and ETag |
GET /v1/tls-check?domain= | Certificate gate for first-party domains (used by the edge proxy) |
GET/POST /g/collect | GA4 wire-format intake for server-side GTM |
POST /v1/engage, /v1/surveys/respond, /v1/promos/track, /v1/form | Surveys, forms and promos |
/sg.js, /sx.js, /sp.js, /tt.js, /a/c, /s/p, /t/e | First-party custom loader proxies, served only on a claimed domain |