Skip to documentation content
Server-side ingestion

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.

When to use thisMost sites just install the browser tag, which calls these endpoints for you. Reach for the raw endpoints for headless/SSR flows, mobile apps, or server-side conversions you can't fire from the browser.

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.

POST/api/identify
ParameterTypeDescription
session_id
required
stringThe visitor session to attach the identity to (axo.getSessionId() in the browser).
visitor_id
optional
stringThe 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
stringYour public site identifier.
traits
required
objectAt least one trait (email, external_id, or any custom attribute). Values are strings.
curl
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.

POST/api/convert
ParameterTypeDescription
session_id
optional
stringThe converting session. Supply at least one of session_id, visitor_id, or email_sha256.
visitor_id
optional
stringPersistent visitor ID, used to resolve the session when session_id is unavailable.
email_sha256
optional
string64 lowercase hex characters: SHA-256 of the trimmed, lowercase buyer email. Alternative session lookup when browser IDs are unavailable.
site_id
required
stringYour public site identifier.
revenue
required
number | nullConversion 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
stringYour stable order ID (up to 128 characters). Use the same value across reporters to deduplicate the purchase.
currency
optional
stringThree-character currency code, for example USD.
source
optional
tag | pixel | webhookReporting channel; defaults to tag.
curl
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.

POST/api/lead/capture
ParameterTypeDescription
site_id
required
stringYour public site identifier.
visitor_id
optional
stringPersistent visitor ID to attach the lead to (preferred).
session_id
optional
stringAlternative to visitor_id when only the session is known.
email / name / phone
optional
stringContact 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 stringOptional booked appointment time (surfaces on the Leads page).
source
optional
stringPass 'optin_form' to file the capture as a newsletter/opt-in; anything else (or omitted) files it as a lead form.
curl
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.

EventData fieldsMeaning
profile.identifiedprofile_id, traits (string-valued object), merged (boolean)Identity was attached or merged. traits can contain personal data.
conversionsession_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_urlA conversion was recorded. page_url is currently an empty string. order_id is not forwarded.
lead.capturedlead_id, form_id (nullable), profile_id, source, quality_verdictA lead passed the capture gate or was released from a quality hold.
trigger.firedtrigger_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_pausedexperiment_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_disabledvariant_id, zone_id, zone_key, variant_type, reason, relative_lift, p_value, peek_safe, treatment, holdout, window_started_at, disabled_atThe watchdog disabled a variant after its outcome checks. Treatment and holdout contain the measured comparison.
call.completedcall_id, voice_event_id, action, opportunity_id, stage_moved_to, outcome, escalated, real_discussion, subject, callback_date, callback_time, call_verified_releasesAn AXO Voice call was mapped into Pipeline. Nullable result fields depend on the call. subject can contain caller information.
generation.finishedpage_id, page_slug, page_status, revision_id, revision_no, kindA generated page revision was stored. Inspect page_status before assuming it is published.
crawl.completedcrawl_id, sitemap_url (nullable), total_urls, crawled_urls, failed_urls, started_atA sitemap crawl finished; failed_urls can be greater than zero.
campaign.launched / campaign.pausedcampaign_id, name, job_id, steps[]; launched also includes resumed (boolean)Each step contains kind, ref_id, outcome. Inspect individual outcomes.
campaign.closedcampaign_id, name, job_id, headline, causalThe campaign outcome snapshot was stored.
stall.foundfinding_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_idA 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
SchemeHeaderSigned input and format
stripeX-Axo-SignatureHMAC-SHA256 of timestamp + '.' + rawBody. Header: t=UNIX_SECONDS,v1=LOWERCASE_HEX. Timestamp must be within six hours of server time.
hexX-Webhook-SignatureHMAC-SHA256 of the raw body, as hexadecimal. No timestamp bound.
githubX-Hub-Signature-256HMAC-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.

ResponseMeaning and action
200, ok: true, actionInspect action: activity_logged, opportunity_created, quarantined, denied, skipped_no_identity, or error_recorded. A 200 alone does not establish a successful record update.
200, skippedalready_processed means the ID was seen; module_disabled means Pipeline is disabled.
400 / 401Invalid JSON, or invalid signature with a reason. Correct the body/signing configuration before resending.
404 / 413Inactive/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.