Ingestion & webhooks
Send events from a server or headless stack, and receive signed webhooks. These public endpoints accept a site_id and visitor or session context. The site_id identifies your site; it is not a secret or proof that the request came from your server.
Identify
Attach a known identity to a session server-side. A successful response is { "ok": true, "profile_id": "prf_123", "merged": false }. The merged flag reports whether profiles were joined.
| Parameter | Type | Description |
|---|---|---|
| session_id required | string | The visitor session to attach the identity to (axo.getSessionId() in the browser). |
| visitor_id optional | string | The persistent visitor ID (axo.getVisitorId(); the vt_sid cookie). Strongly recommended: it keys the profile server-side so traits land on the profile the visitor's sessions link to, not on a visit-scoped one. |
| site_id required | string | Your public site identifier. |
| traits required | object | At least one trait (email, external_id, or any custom attribute). Values are strings. |
curl -sS https://axoapp.ai/api/identify \
-H "Content-Type: application/json" \
-d '{
"session_id": "sess_456uvw",
"visitor_id": "vis_789xyz",
"site_id": "YOUR_SITE_ID",
"traits": { "email": "customer@example.com", "plan": "pro" }
}'Convert
Record a conversion and forward it to configured ad destinations. A successful response is { "ok": true }. An already-recorded order_id also returns deduped: true. A 404 means the supplied identifiers did not resolve a session; a 429 can mean a rate limit or session conversion cap.
| Parameter | Type | Description |
|---|---|---|
| session_id optional | string | The converting session. Supply at least one of session_id, visitor_id, or email_sha256. |
| visitor_id optional | string | Persistent visitor ID, used to resolve the session when session_id is unavailable. |
| email_sha256 optional | string | 64 lowercase hex characters: SHA-256 of the trimmed, lowercase buyer email. Alternative session lookup when browser IDs are unavailable. |
| site_id required | string | Your public site identifier. |
| revenue required | number | null | Conversion revenue, 0–10,000,000. Null records a non-revenue conversion. |
| line_items optional | LineItem[] | Up to 100 items ({ sku, quantity, price }). Each item needs product_id, sku, or url. quantity is a positive integer (default 1). |
| order_id optional | string | Your stable order ID (up to 128 characters). Use the same value across reporters to deduplicate the purchase. |
| currency optional | string | Three-character currency code, for example USD. |
| source optional | tag | pixel | webhook | Reporting channel; defaults to tag. |
curl -sS https://axoapp.ai/api/convert \
-H "Content-Type: application/json" \
-d '{
"session_id": "sess_456uvw",
"site_id": "YOUR_SITE_ID",
"revenue": 149.00,
"line_items": [{ "sku": "SKU-1", "quantity": 2, "price": 24.0 }]
}'Lead capture
Capture the lead and link its profile. Feeds the Leads pipeline. A successful response includes success: true, profile_id, and appointment_slot when supplied. On passive-mode sites, identify and lead capture return success with passive: true and profile_id: null, without storing the submitted identity.
| Parameter | Type | Description |
|---|---|---|
| site_id required | string | Your public site identifier. |
| visitor_id optional | string | Persistent visitor ID to attach the lead to (preferred). |
| session_id optional | string | Alternative to visitor_id when only the session is known. |
| email / name / phone optional | string | Contact fields (email ≤ 320, name ≤ 200, phone ≤ 40 chars). None is enforced server-side, so validate before posting – a lead with no contact value is stored but cannot be worked. |
| appointment_slot optional | ISO 8601 string | Optional booked appointment time (surfaces on the Leads page). |
| source optional | string | Pass 'optin_form' to file the capture as a newsletter/opt-in; anything else (or omitted) files it as a lead form. |
curl -sS https://axoapp.ai/api/lead/capture \
-H "Content-Type: application/json" \
-d '{
"site_id": "YOUR_SITE_ID",
"visitor_id": "vis_789xyz",
"email": "lead@example.com",
"name": "Jordan Lee",
"appointment_slot": "2026-07-20T15:00:00Z"
}'Limits
The behavioural ingest endpoints /api/events and /api/score accept custom keys verbatim – extra properties on an event, extra signal fields on a score – and store them as-is. Because those endpoints accept the public site_id, each request body is capped at 64 KB.
Oversized bodies are rejected, never truncated: you get 413 payload_too_large with the observed size and the limit in the message, plus X-AXO-Body-Limit and X-AXO-Body-Size response headers. Nothing from a rejected request is stored, so a clipped payload is always visible as a failed request rather than as quietly missing fields.
Browsers also limit the amount of queued data sent with navigator.sendBeacon and fetch with keepalive. Keep request bodies below the API cap and check responses when sending from a server.
Webhooks
Configure a webhook destination in Integrations with your endpoint URL and a secret. Always set a secret: if none is configured, deliveries are signed with your public site_id, which anyone who has seen your install snippet can reproduce. Every delivery is a JSON POST with two headers: X-VibeTag-Signature, the hex HMAC-SHA256 of the raw body keyed by your secret, and X-VibeTag-Event, the event type. Verify the signature before trusting the body. Deliveries time out after 5 s; a non-2xx response is logged as a failed delivery in the integration's health log. Endpoints must be publicly routable (private and loopback addresses are refused). The body contains event, timestamp, and an event-specific data object; the example below shows profile.identified.
// POST to your endpoint
// Headers: X-VibeTag-Signature: <hex hmac-sha256 of the raw body>
// X-VibeTag-Event: profile.identified
{
"event": "profile.identified",
"timestamp": "2026-07-14T14:32:01Z",
"data": {
"profile_id": "prf_123",
"traits": { "email": "customer@example.com", "plan": "pro" },
"merged": false
}
}Integration webhook data by event
The following fields belong inside data. Consumers should accept additional fields, handle nullable values, and ignore unknown event types. These are current producer field contracts; nested analysis results can grow as reporting changes.
| Event | Data fields | Meaning |
|---|---|---|
profile.identified | profile_id, traits (string-valued object), merged (boolean) | Identity was attached or merged. traits can contain personal data. |
conversion | session_id, revenue (number|null); optional visitor_id, segment_id, segment_confidence, profile_id, fbp, fbc, gclid, ip, user_agent, content_ids, content_type, line_items; page_url | A conversion was recorded. page_url is currently an empty string. order_id is not forwarded. |
lead.captured | lead_id, form_id (nullable), profile_id, source, quality_verdict | A lead passed the capture gate or was released from a quality hold. |
trigger.fired | trigger_id, fire_id, profile_id, integration_id, identifier_type (last three nullable), in_holdout (boolean) | A successful trigger dispatch to a visitor. Test fires are not sent. The identifier type is included; its value is omitted. |
experiment.ended / experiment.shipped / experiment.guardrail_paused | experiment_id, name, page_type (nullable), status, reason (nullable), by, winner_arm (nullable), shipped (boolean), arms[] | Each arm contains key, kind, name, sessions, cvr, relative_lift, significant. A manual pause does not emit guardrail_paused. |
variant.watchdog_disabled | variant_id, zone_id, zone_key, variant_type, reason, relative_lift, p_value, peek_safe, treatment, holdout, window_started_at, disabled_at | The watchdog disabled a variant after its outcome checks. Treatment and holdout contain the measured comparison. |
call.completed | call_id, voice_event_id, action, opportunity_id, stage_moved_to, outcome, escalated, real_discussion, subject, callback_date, callback_time, call_verified_releases | An AXO Voice call was mapped into Pipeline. Nullable result fields depend on the call. subject can contain caller information. |
generation.finished | page_id, page_slug, page_status, revision_id, revision_no, kind | A generated page revision was stored. Inspect page_status before assuming it is published. |
crawl.completed | crawl_id, sitemap_url (nullable), total_urls, crawled_urls, failed_urls, started_at | A sitemap crawl finished; failed_urls can be greater than zero. |
campaign.launched / campaign.paused | campaign_id, name, job_id, steps[]; launched also includes resumed (boolean) | Each step contains kind, ref_id, outcome. Inspect individual outcomes. |
campaign.closed | campaign_id, name, job_id, headline, causal | The campaign outcome snapshot was stored. |
stall.found | finding_id, week, window {since, until}, source, funnel, funnel_kind, funnel_label, step_index, from_step, to_step, cohort, comparison, corroborated, cohort_rate, baseline_rate, gap, relative_gap, q_value, sessions_per_week, stalled_sessions_per_week, value_per_week, currency, headline, proposed_action, read_plan_summary, notification_id, proposal_id | A weekly analysis found a stalled funnel step. The proposed action still needs review. Value fields can be absent or null when no value estimate exists. |
Catalog limitation: profile.updated, segment.changed, add_to_cart, abandon_cart, pageview, and click are selectable event names, but this release has no verified integration-dispatch producer for them. Do not depend on those subscriptions for delivery. Use Pulse profile.changed for profile-state notifications.
Integration delivery and recovery
Integration event forwarding is best effort. There is no automatic retry queue or stable delivery ID in this protocol. Open the connector’s Health log to inspect failures and retry after correcting the receiver. A manual replay generates a new envelope timestamp. Deduplicate using event-specific IDs, such as fire_id or lead_id, and your operation’s semantics.
Verify the signature on raw bytes, durably accept the work, and return 2xx within five seconds. Process slow work after acknowledgement. Treat duplicate and out-of-order events as possible. The generic Send test event action sends an unsigned test_ping with a different body; it does not validate signature processing. Keep signature enforcement enabled and verify a signed intended event separately.
Pulse webhooks use X-Pulse-* headers, a different envelope, and a durable retry queue. Use the Pulse delivery contract for that API.
Receive a custom webhook in AXO
Create a Custom inbound webhook connector. Its receiving URL is POST /api/inbound/{siteId}/{integrationId}. The site and connector must be active, and Pipeline must be enabled for records to be processed. A GET returning ok only checks reachability.
For the example below, select signature_scheme: stripe, set a private secret, set event_id_path: id, and save this mapping:
customer.email: contact.email
customer.name: contact.name
plan: trait.plan| Scheme | Header | Signed input and format |
|---|---|---|
| stripe | X-Axo-Signature | HMAC-SHA256 of timestamp + '.' + rawBody. Header: t=UNIX_SECONDS,v1=LOWERCASE_HEX. Timestamp must be within six hours of server time. |
| hex | X-Webhook-Signature | HMAC-SHA256 of the raw body, as hexadecimal. No timestamp bound. |
| github | X-Hub-Signature-256 | HMAC-SHA256 of the raw body, prefixed with sha256=. No timestamp bound. |
The body limit is 512 KiB. Deduplication uses the configured ID path within each integration; if absent, it hashes the raw body. Reuse the same delivery ID for a retry. Mapped email or phone is needed to match or create a record. Existing custom-field keys must already be defined. Arbitrary inbound events do not move deal stages.
| Response | Meaning and action |
|---|---|
200, ok: true, action | Inspect action: activity_logged, opportunity_created, quarantined, denied, skipped_no_identity, or error_recorded. A 200 alone does not establish a successful record update. |
200, skipped | already_processed means the ID was seen; module_disabled means Pipeline is disabled. |
| 400 / 401 | Invalid JSON, or invalid signature with a reason. Correct the body/signing configuration before resending. |
| 404 / 413 | Inactive/missing site or connector, or body too large. Check setup or reduce the payload. |
Quarantined and failed records need inspection in AXO. Repeatedly resending the same ID will not release them. This inbound protocol is separate from the AXO Voice call contract and from outbound integration events.
Ingestion errors and safe retries
Identify and convert errors use { "error": { "code": "validation_error", "message": "Payload failed validation", "details": { … } } }. Invalid JSON or fields return 400. Identify returns 404 for an unknown site; convert returns 404 when no session resolves. Correct validation and identity errors before retrying. For 429, honor Retry-After when present and back off; a per-session conversion cap will not clear by rapid retry.
Lead capture uses a string error for malformed JSON or missing site_id, such as { "error": "Missing site_id" }. Inspect the response body as well as the status. Retry conversions with the same order_id to avoid duplicate purchases. Identify and lead capture do not expose a general idempotency-key contract.