Documentation
Protocol reference

Wire protocol

The HTTP request and response FounderHQ Events SDKs use.

This page describes the exact bytes an SDK sends and gets back. Read it when you write your own client, debug a payload, or point an AI agent at the contract.

You rarely need this

The web, React Native, iOS, Android, and Node SDKs already speak this protocol. Start at Getting started unless you are building a client yourself.

Send the batch

Send one POST to /i/v2/e on your FounderHQ host. Put the publishable key in the Authorization header.

POST https://i.getfounderhq.com/i/v2/e
Authorization: Bearer fhq_pk_XXXX
Content-Type: application/json

The body is an envelope with a send time and a list of events.

{
  "sent_at": "2026-05-23T09:41:02.115Z",
  "batch": [
    {
      "uuid": "0197f6b8-2c31-7a4e-9f0d-5b2c1d8e4a77",
      "event": "trial.started",
      "distinct_id": "user_42",
      "timestamp": "2026-05-23T09:41:01.980Z",
      "properties": { "plan": "pro", "$pathname": "/pricing" },
      "session_id": "0197f6b8-1a02-7c11-8f31-6d0b9e2a4c58",
      "options": { "process_person_profile": false }
    }
  ]
}

A batch holds 1 to 100 events. You may gzip the body: set Content-Encoding: gzip, or add ?compression=gzip-js to the URL.

Envelope fields

FieldTypeRequiredMeaning
sent_atISO 8601 date-timeyesWhen the client sent the request. FounderHQ uses it to correct clock drift.
batcharrayyes1 to 100 events.

Event fields

Normal and cookieless events use the same fields.

FieldTypeRequiredMeaning
uuidUUID stringyesYour idempotency key. Send the same UUID twice and FounderHQ counts it once.
eventstringyesThe event name. Max 200 characters.
distinct_idstringyesWho did it. 1 to 400 characters.
timestampISO 8601 date-timeyesWhen it happened on the device.
propertiesobjectyesEverything else about the event. There is no separate context object.
session_idstringnoThe session the event belongs to.
window_idstringnoThe browser tab or window.
optionsobjectyesCapture and profile-processing hints. See below.

$pageview_id belongs in properties, not at the event's top level.

Options

FieldValuesMeaning
process_person_profilebooleanWhether this event may update the contact profile. Required.
cookieless_modebooleantrue marks the strict cookieless event shape. Omit it for normal events.

Event names

Names that do not start with $ are yours. Use any name up to 200 characters.

Names that start with $ are reserved. FounderHQ rejects any $ name that is not on the allowlist. See Event taxonomy.

Distinct IDs FounderHQ refuses

A distinct ID must identify one person. FounderHQ drops these values, whatever their letter case or surrounding spaces:

"", anonymous, guest, id, undefined, null, none, nil, nan, [object object], "undefined", "null"

Send a real ID, or send your own anonymous ID. Never send the literal string anonymous.

Read the response

An accepted request returns HTTP 202 with one result per event.

{
  "results": [
    { "uuid": "0197f6b8-2c31-7a4e-9f0d-5b2c1d8e4a77", "status": "ok" }
  ]
}

Results come back per UUID, not per batch. One bad event never sinks the other 99.

StatusWhat it meansWhat your client does
okFounderHQ stored the event.Drop it from the queue.
warningFounderHQ accepted the event, but changed something. The message says what. A duplicate UUID and a corrected timestamp both report warning.Drop it from the queue.
dropFounderHQ refused the event and will always refuse it. Common causes: an unknown $ name, an illegal distinct ID, properties over the size limit.Drop it. Retrying wastes calls.
retrySomething transient failed.Keep the event queued and send it again.

Every result may carry a message. Log it — it names the exact reason.

Request-level failures

HTTPMeaning
400The envelope is not valid JSON, does not match the shape, or mixes consent states.
401The key is missing, wrong, or not a publishable key.
403The key cannot capture events, is not scoped to a brand, or the request origin is not allowed.
413The body is too large. Send a smaller batch.
429Your organization is over its ingest budget. Read Retry-After and wait.
503FounderHQ could not check the budget. Retry with backoff.

Size ceilings and rate limits

Both ingest paths — /i/v2/e for publishable keys and /api/events for secret keys — share these ceilings.

LimitValueOver it
Events per batch100400
Request body on the wire256 KiB413
Body after gzip decompression1 MiB413

Budgets are per organization. Every brand, every key, every site, and every installation spends from the same allowance. Creating another key does not buy more capacity.

BudgetSustainedBurst
Requests200 per second400
Events500 per second2,000
Decoded body bytes2 MiB per second8 MiB

When a limit applies to your organization, FounderHQ answers 429 with a Retry-After header in whole seconds, before it reads or stores anything. Nothing in that batch was accepted. Keep the events queued, wait the stated delay, and send the same UUIDs again.

Retry-After and Server-Timing are exposed to browser clients, so a web SDK can read them across origins.

Identity directives

Sometimes FounderHQ decides your anonymous visitor must start a new anonymous identity. That happens when the identity you sent already belongs to somebody else. FounderHQ says so in the response:

{
  "results": [
    {
      "uuid": "0197f6b8-2c31-7a4e-9f0d-5b2c1d8e4a77",
      "status": "warning",
      "message": "Anonymous identity rotated; retry identify"
    }
  ],
  "directives": [{ "type": "rotate_distinct_id" }]
}

Your client must do two things:

  1. Replace the stored anonymous ID. Use the distinct_id in the directive when it is present. Otherwise generate a fresh UUID.
  2. Send the $identify event again with a new uuid, and set $anon_distinct_id and $device_id in its properties to the new anonymous ID.

The official SDKs do this for you. Skip the step and the person stays split across two identities.

Normal and cookieless events

FounderHQ supports cookieless measurement for visitors who have not granted tracking. A cookieless event uses the normal event shape with a fixed non-person identity and can never touch a contact profile.

{
  "uuid": "0197f6b8-4d92-7b30-a1c7-e2f4a90b6d13",
  "event": "$pageview",
  "distinct_id": "$founderhq_cookieless",
  "timestamp": "2026-05-23T09:41:01.000Z",
  "properties": {
    "$pathname": "/pricing",
    "$referring_domain": "news.ycombinator.com",
    "utm_source": "hn",
    "$platform": "web"
  },
  "options": {
    "cookieless_mode": true,
    "process_person_profile": false
  }
}

Differences from a granted event:

NormalCookieless
consent_statenot usednot used
distinct_idvisitor or contact ID"$founderhq_cookieless"
eventany allowed name$pageview or $pageleave only
propertiesanya fixed allowlist only
session_id, window_id, $pageview_idallowednot allowed
optionsprocess_person_profilealso requires cookieless_mode: true and process_person_profile: false

The allowed cookieless properties are listed on Event taxonomy. Two of them are coarsened on purpose: FounderHQ rounds $page_duration_ms to whole seconds and $max_scroll_percentage to steps of 5.

Normal and cookieless events may share one batch. FounderHQ validates, routes, and acknowledges every event independently by UUID.

What FounderHQ does not accept

  • There is no context object. Protocol v2 has one property namespace.
  • The older /api/ingest/batch contract does not work. Use /i/v2/e.
  • The publishable-key path cannot write $revenue or $account_membership. Those need the revenue API and a secret key.

AI agent or LLM? Read this page as markdown

On this page