airhop docs

API reference

The /ingest/v1 HTTP API — authentication, every endpoint, call shapes, batching, response codes, rate limits, and backfill.

Everything under https://app.airhop.ai/ingest/v1 accepts product events. The API follows the Segment spec (identify, track, page, group), requests are validated and queued immediately, and the heavy lifting happens asynchronously — so responses come back fast even for large batches. This page is the wire-level reference; for the guided version, see Send product events.

Authentication

Every request must carry an ingestion key. Three forms are accepted:

FormExample
Bearer token (recommended)Authorization: Bearer wk_your_write_key
HTTP Basic, key as usernamecurl -u wk_your_write_key: ... (empty password)
writeKey in the body or query string{ "writeKey": "wk_...", "batch": [...] } or ?writeKey=wk_...

The Basic and writeKey forms exist for tooling that only supports them; prefer the Bearer header everywhere else — including a Segment Webhooks (Actions) destination, where you add Authorization: Bearer sk_... as a request header on the Send mapping.

Two keys authenticate:

  • The write key (wk_...) is write-only and safe to ship in client-side code. You will find it behind the Install snippet button on the Activation page.
  • The secret key (sk_...) is for servers only — never ship it to a browser.

Both keys ingest events, but only secret-key traffic is trusted. Because the write key is visible in your page source by design, anyone could forge calls with it — so write-key traffic gets two safety restrictions: an identify can set an email on a contact that has none, but never replace one already on file, and any playbook it triggers is drafted for your approval instead of auto-sent. The one exception is an in-widget nudge, which still sends immediately — it only surfaces inside your own widget, to the user already looking at it, so there is nothing to forge into a customer's inbox. Send from your server with the secret key when you want full trust.

A missing or unknown key returns 401.

The call object

Every call is a JSON object. Fields common to all types:

FieldTypeNotes
typestringidentify, track, page, screen, or group. Optional on the typed endpoints (implied by the URL); required inside batch.
userIdstringYour stable user id. A call needs userId or anonymousId to be attributable.
anonymousIdstringPre-login visitor id. The browser SDK manages one automatically; events sent with only an anonymousId stay unlinked until an identify merges them onto the user.
timestampstringISO-8601. When the event actually happened — essential for backfills. Missing timestamps default to receipt time. originalTimestamp and sentAt are accepted as fallbacks.
messageIdstringUnique id for this call; the deduplication key (see Idempotency). Generated server-side if omitted.
contextobjectMetadata about how the call was collected (library, page URL). Stored with the event.

Per-type fields:

TypeFieldNotes
identifytraitsobject — email, name, plan, and any custom traits. Email is what links the user to an account.
trackeventstring, required — a track with no event name is dropped. Use stable snake_case names.
trackpropertiesobject — anything about the action.
page / screennamestring — defaults to $page / $screen. properties also accepted.
groupgroupIdstring — the account (company) id to link the user to. traits carries account traits like name.

Endpoints

POST /ingest/v1/identify

curl https://app.airhop.ai/ingest/v1/identify \
  -H "Authorization: Bearer wk_your_write_key" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_123",
    "traits": { "email": "jo@acme.com", "name": "Jo Diaz", "plan": "pro" }
  }'

POST /ingest/v1/track

curl https://app.airhop.ai/ingest/v1/track \
  -H "Authorization: Bearer wk_your_write_key" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_123",
    "event": "project_created",
    "properties": { "projectId": "p_1" }
  }'

POST /ingest/v1/page

curl https://app.airhop.ai/ingest/v1/page \
  -H "Authorization: Bearer wk_your_write_key" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_123",
    "name": "Pricing",
    "properties": { "url": "https://acme.com/pricing" }
  }'

POST /ingest/v1/group

curl https://app.airhop.ai/ingest/v1/group \
  -H "Authorization: Bearer wk_your_write_key" \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_123",
    "groupId": "acme_inc",
    "traits": { "name": "Acme Inc" }
  }'

POST /ingest/v1/batch

Any mix of call types, each with an explicit type, up to 5,000 calls per request:

curl https://app.airhop.ai/ingest/v1/batch \
  -H "Authorization: Bearer wk_your_write_key" \
  -H "Content-Type: application/json" \
  -d '{
    "batch": [
      { "type": "identify", "userId": "user_123", "traits": { "email": "jo@acme.com" } },
      { "type": "track", "userId": "user_123", "event": "project_created" },
      { "type": "group", "userId": "user_123", "groupId": "acme_inc" }
    ]
  }'

Ordering within a batch does not matter; milestone timing is computed from each call's timestamp, not arrival order.

CDP adapters

Three additional endpoints accept other tools' native payloads and normalize them into the same pipeline:

EndpointAccepts
POST /ingest/v1/segmentA Segment Webhooks (Actions) destination — single calls or { "batch": [...] }, unchanged.
POST /ingest/v1/posthogPostHog's event shape (the event uuid becomes the messageId).
POST /ingest/v1/intercomIntercom Data Events (the event id becomes the messageId).

Authentication and limits are identical to the native endpoints. See Send product events for the destination setup.

Responses

StatusBodyMeaning
200{ "received": 2, "backfill": false }Accepted and queued. received is the number of calls taken off the request; per-call validation happens later, so a malformed call (say, a track with no event) is counted here but silently dropped during processing.
401{ "message": "Invalid ingestion key." }Missing or unknown key. Check you are sending the wk_ write key, not the widget's pk_ public key.
413{ "error": "batch too large" }More than 5,000 calls in one request. Split the batch.
429Rate limit exceeded. Back off and retry.

Rate limits

6,000 requests per minute, per key. The limit counts requests, not calls — each request can carry a 5,000-call batch, so the practical ceiling is millions of events per minute. It is a guard against runaway clients, not a constraint on legitimate volume. If you hit 429, batch more aggressively rather than retrying faster.

Idempotency

Give every call a messageId that is stable across retries. Ingestion deduplicates on the combination of workspace, messageId, and timestamp — a duplicate is silently ignored, so re-sending a batch that failed halfway is always safe. If you omit messageId, one is generated per call and every retry counts as a new event.

{ "type": "track", "userId": "user_123", "event": "project_created", "messageId": "pc-p_1" }

Backfill

Mark a request as a historical import with either:

  • "backfill": true at the top level of the request body, or
  • the header X-Airhop-Backfill: 1.

Backfilled calls record milestones and health but never fire playbooks or count billable outcomes — importing a year of history won't send a year of emails. The response echoes the flag ("backfill": true). See Backfill historical data for the full recipe.

A reliable server-side sender is small: batch up to 5,000 calls, set a stable messageId on each, send with your sk_ secret key, and retry failed requests with backoff. Idempotency makes the retries free.

On this page