PilotFoxDocs

Send events

POST /api/v1/ingest/events: up to 500 events per request, with their fields, visitor matching and timestamps.

POST https://pilotfox.app/api/v1/ingest/events

Records a batch of events. It needs an API key with ingest:write (see Server-side ingest for the key, the limits and how events are matched to visitors). Reference: POST /api/v1/ingest/events.

Request

A JSON object with one field, events: an array of 1 to 500 events, at most 1 MiB in all.

{
  "events": [
    { "site": "SITE_ID", "type": "event", "name": "subscription_renewed", "anonymous_id": "customer_8412" }
  ]
}

Each event has the fields of a browser event, plus four only the server can send (ip, user_agent, anonymous_id, ts), all optional. Like the browser's, events are read tolerantly: text that is too long is cut to its limit, a value PilotFox cannot use is dropped, and unknown fields are ignored, rather than refusing the event. The four server fields are the exception: a value of the wrong form refuses the event instead of being dropped, so an event is never filed under the wrong visitor or time. Only the problems listed under rejections refuse an event.

Event fields

FieldRequiredType and limitWhat it is
siteyesstringThe site's public id.
typeyespageview, event, outbound, click, form, input, copy, error or vitalThe kind of event. Custom events are event.
namefor event and errorstring, 128 charactersThe event's name, such as subscription_renewed, or an error's name. Spaces around it are trimmed.
urlnoabsolute URL, 2 048 charactersThe page the event happened on. Its path, query string and UTM parameters are read as for a browser event. Without it, the site's home page (https:// and the site's domain) is used. A relative URL rejects the event.
refnostring, 2 048 charactersThe referrer URL.
titlenostring, 512 charactersThe page title.
propsnoobjectCustom properties: the first 30, each value a string (cut to 512 characters), a number or a boolean. A key longer than 64 characters and other values (objects, arrays, null) are dropped. On an error event, props are not stored: the error's own fields (err) are.
uidnostring, 255 charactersYour user id: reports count the event for that user. A number is read as text; a value with no text in it is no user.
langnostring, 35 charactersThe visitor's language, such as en-US.
sw, shnointegers, 0 to 65 535Screen width and height in pixels.
tagnostring, 64 charactersA free label for the session (filterable as the tag dimension).
errnoobjectRead on error events only: message (1 000 characters), stack (4 000), file (512), line and col (integers). An error event without it is stored with an empty error. Errors are grouped by name, message and top stack frame.
vitalsnoobjectRead on vital events only: lcp, cls, inp, fcp, ttfb, non-negative numbers.
ipnostring: an IPv4 or IPv6 addressThe visitor's IP address. Without it, the address the request came from (your server's) is used. Anything but a valid address, null included, rejects the event.
user_agentnostring, 512 characters keptThe visitor's User-Agent. Without it, the event has no browser, system or device. A value that is not a string rejects the event.
anonymous_idnostring, 128 characters keptYour own stable id for the visitor; takes precedence over ip and user_agent. An empty string counts as none; a value that is not a string rejects the event.
tsnostring: an RFC 3339 date and time, with seconds and Z or a ±hh:mm offsetWhen the event happened, from 7 days ago to 5 minutes ahead, such as 2026-10-27T18:05:00+02:00 or 2026-10-27T16:05:00.250Z. A time without seconds, without an offset, with a lowercase z or with an offset like +0200 rejects the event. Without ts, the time PilotFox received the request.

Which visitor an event belongs to depends on ip, user_agent and anonymous_id: see Matching server events to visitors.

Timestamps and late events

ts lets you send events after the fact, for example from a queue or a retry: up to 7 days late. An event dated more than 30 minutes ago does not join the visitor's current session. It goes to a session of its own for that visitor and that 30-minute period, so a backlog sent twice lands in the same sessions, and it never shows in the realtime view.

PilotFox does not deduplicate events: a batch sent twice is stored twice. If a request fails without an answer (a network error, a timeout), you cannot tell whether its events were stored; decide whether a retry or a gap matters more to you.

Response

200 with how many events were taken and which were rejected, by their position in events (counting from 0):

{
  "data": {
    "accepted": 2,
    "rejected": [
      { "index": 2, "code": "invalid" },
      { "index": 3, "code": "forbidden_site" },
      { "index": 4, "code": "ts_out_of_range" },
      { "index": 5, "code": "invalid" }
    ]
  }
}

A batch is not all-or-nothing: the events that pass are stored even when others are rejected. Check rejected on every answer.

accepted counts every event PilotFox took, including those it then drops on purpose: events excluded by the site's settings (an excluded IP address, network, country, path or user agent, or a hostname not on the allowed list) and, when the site blocks bots, events from a detected bot. The answer does not say which, as for the tracking script.

Rejected events

codeWhy
invalidNot an object, or a required field is missing or unusable: no site, an unknown type, no name on an event or error, a relative url. Or a server field of the wrong form: an ip that is not an IP address, a user_agent or anonymous_id that is not a string, a ts that is not an RFC 3339 date and time as above.
forbidden_siteThe key cannot write to the site: it does not exist, was deleted, belongs to another organization, or is outside the key's site list.
ts_out_of_rangets is more than 7 days ago or more than 5 minutes ahead.

Errors for the whole request

StatusWhen
400The body is not JSON, or not { "events": [...] } with 1 to 500 elements (Expected { events: [1..500 events] }).
401No API key, an invalid one, or another kind of credential.
403The key lacks ingest:write.
413The body is larger than 1 MiB.
429The key has sent 60 write requests this minute: wait Retry-After seconds.

A complete example

A subscription renews in your billing webhook, and you know the customer's user id; a second event is a purchase made during a request from the customer's browser, whose address and user agent your server saw.

curl -X POST "https://pilotfox.app/api/v1/ingest/events" \
  -H "Authorization: Bearer $PILOTFOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'EOF'
{
  "events": [
    {
      "site": "SITE_ID",
      "type": "event",
      "name": "subscription_renewed",
      "anonymous_id": "customer_8412",
      "uid": "user_8412",
      "props": { "plan": "pro", "amount": 49, "annual": false },
      "ts": "2026-10-27T18:05:00+02:00"
    },
    {
      "site": "SITE_ID",
      "type": "event",
      "name": "order_paid",
      "url": "https://example.com/checkout",
      "ip": "203.0.113.7",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
      "uid": "user_8412",
      "props": { "order_id": "ord_5521", "total": 120 }
    }
  ]
}
EOF
Response
{ "data": { "accepted": 2, "rejected": [] } }

To send many events, collect them and send up to 500 per request, at most 60 requests a minute per key.

On this page