Skip to documentation content
REST · Pulse v1

REST API (Pulse v1)

Read a visitor profile or list the members of a custom segment from your backend. Use a site-scoped API key and the examples below to make your first request.

Base URL & auth

All endpoints live under /api/pulse/v1/ and authenticate with a bearer key. Create keys in Admin → Pulse; the pls_live_… plaintext is shown once. Keep the key on your server; do not put it in browser code. Each key carries scopes that determine what the endpoint returns.

GEThttps://axoapp.ai/api/pulse/v1/…
Key scopes
ScopeTypeDescription
profiles:exportscopeExport profiles matching a predicate.
profiles:readscopeInclude raw identifiers in bulk profile exports. Separate from the single-profile scopes below.
webhook:writescopeManage site webhook subscriptions and delivery recovery. Creating subscriptions also requires profile:read.
profile:readscopeRead a profile snapshot: identity, segments, activity – no plaintext PII.
profile:read:piiscopeAdditionally return plaintext email / phone / name on profile reads.
segment:readscopeList the profile IDs in a segment (cursor-paginated).

Get a profile

Read one profile by exactly one identifier. Returns a ProfileSnapshot (identity, segments, activity, meta). Requires profile:read. The segments list contains custom segment memberships. With profile:read:pii, the response also includes identity.pii and plaintext identifiers. Without it those fields are omitted; the supplied lookup value is still echoed in queried_by.

GET/api/pulse/v1/profile
Query (exactly one)
ParameterTypeDescription
profile_id
optional
stringLook up by AXO profile ID.
visitor_id
optional
stringLook up by first-party visitor ID.
email
optional
stringLook up by plaintext email (requires profile:read:pii to return PII).
external_id
optional
stringLook up by your own external identifier.
email_sha256 / phone_sha256
optional
stringLook up by a hashed identifier (privacy-preserving).
curl -sS "https://axoapp.ai/api/pulse/v1/profile?email=customer@example.com" \
  -H "Authorization: Bearer pls_live_YOUR_KEY"

List segment members

Requires segment:read and the ID of a custom segment from Segments. Built-in behavioral labels such as deliberator are not custom segment IDs. Each member includes a profile_id and the time it was added.

Set limit to 1–1,000 (default 100). Pass next_cursor unchanged as the URL-encoded cursor query parameter to read the next page. Results are newest first. Stop when next_cursor is null.

GET/api/pulse/v1/segment/{segmentId}/members
curl -sS "https://axoapp.ai/api/pulse/v1/segment/seg_123/members?limit=100" \
  -H "Authorization: Bearer pls_live_YOUR_KEY"

Errors and limits

Except for delivery recovery, errors use { "error": { "code": "…", "message": "…" } }, with optional details. Read the HTTP status and error.code before retrying. Rate limits apply per key; monthly usage limits apply per site.

Error responses
StatusTypeDescription
400validation_errorMissing or conflicting query (supply exactly one identifier).
401unauthorizedMissing or invalid bearer key.
402plan_limitMonthly Pulse usage reached the plan hard limit. Check Admin → Pulse and the X-Pulse-Quota-Limit / X-Pulse-Quota-Used headers.
403forbiddenThe key lacks profile:read or segment:read, as required by the endpoint.
404not_foundNo profile/segment matches the query.
429rate_limitedRate limit exceeded (60 requests/minute, burst 120). Wait for the Retry-After seconds before retrying.

Export profiles matching a rule

Use profiles:export to export visitors who match a custom segment predicate. This example selects visitors with at least two page views and no conversion in the last seven days. The export response differs from the single-profile snapshot above.

The predicate is required. Combine rules with all_of, any_of, and not. Use { "kind": "segment_member", "segment_id": "seg_123" } for an existing custom segment or { "kind": "everyone" } for all profiles. The OpenAPI SegmentPredicate schema includes the full rule vocabulary and is generated from the request validator. Site evaluation-window settings apply when a rule omits within_seconds.

POST/api/pulse/v1/profiles/query
JSON body
ParameterTypeDescription
predicate
required
SegmentPredicateThe rule tree to evaluate.
fields
optional
string[]segment, attributes, lifetime_signals, identifiers, identifiers_hashed. Defaults to all five. profile_id, first_seen, and last_seen are always returned.
limit
optional
integer1–1,000; default 100.
cursor
optional
stringThe previous next_cursor, a profile ID. Omit for the first page.
curl --fail-with-body "https://axoapp.ai/api/pulse/v1/profiles/query" \
  -H "Authorization: Bearer $PULSE_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "predicate": {
    "all_of": [
      {
        "kind": "event_occurred",
        "event_type": "pageview",
        "count_op": "gte",
        "count": 2,
        "within_seconds": 604800
      },
      {
        "not": {
          "kind": "event_occurred",
          "event_type": "conversion",
          "within_seconds": 604800
        }
      }
    ]
  },
  "fields": [
    "segment",
    "identifiers_hashed"
  ],
  "limit": 100
}'

Identifier access: raw identifiers requires profiles:read (plural). Without it the array is empty. This differs from the single-profile endpoint’s profile:read:pii scope. Request only the fields you need; attributes contains stored CRM attributes and is not a guarantee of anonymized data.

Results sort by profile ID. Pass next_cursor unchanged until it is null; a full final page may require one extra request. Membership is evaluated again on each request. An unknown cursor restarts at the beginning, so deduplicate by profile_id when a long export spans membership changes. Exported rows count toward export usage.

Subscribe to profile changes

  1. Create a server endpoint that accepts POST requests and verifies the raw request body.
  2. Create a Pulse key with webhook:write and profile:read. Add profile:read:pii only when you need plaintext identity data.
  3. Send the request below. Store the returned secret securely; it is shown only once.
  4. Accept signed profile.changed events, persist their IDs, and return a 2xx response promptly.

The destination must resolve to a public HTTP(S) address without credentials in its URL. Use HTTPS. profile.changed is currently the only event. Omitting events selects it; include_pii defaults to false. Subscriptions belong to the key’s site.

POST/api/pulse/v1/webhooks
curl --fail-with-body "https://axoapp.ai/api/pulse/v1/webhooks" \
  -H "Authorization: Bearer $PULSE_KEY" \
  -H "Content-Type: application/json" \
  --data '{
  "url": "https://example.com/hooks/axo",
  "events": [
    "profile.changed"
  ],
  "include_pii": false
}'

All shortened paths in this section are relative to https://axoapp.ai/api/pulse/v1.

GET /webhooks returns { "webhooks": [...] }. Each row contains id, site_id, url, events, include_pii, active, created_by_key_id, last_delivery_at, failure_count, created_at, and updated_at. It never returns the secret.

DELETE /webhooks/{id} returns { "ok": true }, or 404 if the subscription does not exist on the key’s site. Both operations require webhook:write. To rotate a subscription secret, create a replacement, configure its secret at your receiver, then delete the old subscription. Deduplicate overlapping notifications.

Verify and process a delivery

Headers are X-Pulse-Signature (hex HMAC-SHA256 of the raw body), X-Pulse-Event: profile.changed, and X-Pulse-Delivery-Id. The body is { "id": "…", "event": "profile.changed", "timestamp": "…", "data": { … } }. data has the same ProfileSnapshot shape as a profile read; id matches the delivery header.

Node.js signature verification
import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody must be the original request bytes, before JSON parsing.
export function verifyPulse(rawBody, signature, secret) {
  if (typeof signature !== 'string' || !/^[a-f0-9]{64}$/i.test(signature)) return false;
  const expected = createHmac('sha256', secret).update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(signature, 'hex'));
}

// In your POST handler:
// 1. Read raw bytes and X-Pulse-Signature. Reject an invalid signature.
// 2. Parse JSON; check event === 'profile.changed' and required fields.
// 3. Atomically persist/enqueue the event using event.id as a unique key.
// 4. Return 2xx after durable acceptance (also for an already accepted ID).
// 5. Process the queued snapshot outside the request.

The signed timestamp is the enqueue time, not a fresh timestamp for every attempt. Deduplicate by delivery ID; a short timestamp rejection window can reject legitimate retries. After durable acceptance, retain your accepted snapshot and ignore further attempts with the same ID; use a fresh profile read when you need the latest state. A retry keeps its ID but rebuilds the latest privacy-filtered snapshot, so its body can change. Treat this as a latest-state notification, not an immutable history of every profile edit.

A 2xx response marks success. Network failures, HTTP 408, 429, and 5xx responses retry up to six total attempts. Backoff starts at 30 seconds and doubles; worker scheduling can add delay. Other non-2xx responses fail immediately. The receiver has an eight-second request timeout. Revoked keys, removed scopes, inactive subscriptions, and unavailable profile authorization can cancel delivery. There is no exactly-once guarantee.

Inspect and recover deliveries

GET /webhooks/deliveries returns { "deliveries": [...] }, the newest 100 rows across subscriptions on the key’s site, with no pagination. Both delivery operations require webhook:write. Add ?terminal=true for failed and cancelled rows. Each row has id, webhook_id, profile_id, status, attempts, available_at, response_code, error, created_at, and updated_at. Status is pending, processing, success, failed, or cancelled. Completed rows are retained for 30 days.

Fix the receiver or authorization first. Then POST { "id": "delivery_id" } to /webhooks/deliveries to reset a failed/cancelled delivery. Replay preserves its ID and rebuilds the latest snapshot. A receiver that already durably accepted that ID should ignore the replay; use a fresh profile read to reconcile it. A 202 response contains { "replayed": true }. A 404 response contains { "replayed": false } when the ID is absent or not replayable.

To reconcile a profile’s latest state, POST { "profile_id": "prf_123" } without id. This also needs profile:read. Responses are 202 with { "queued": true } or 404 with { "queued": false }. A true result confirms that the profile exists and enqueue was attempted; inspect active subscriptions and the delivery log to verify delivery.

Error exception: the deliveries endpoint returns { "error": "message" }, with a string rather than the structured error used elsewhere. A missing webhook:write scope returns 401 here. Invalid replay input returns 400; reconciliation without profile:read returns 403.

Build clients that tolerate change

Use the /v1/ paths, accept additional response fields, and handle nullable values. Keep unknown event types out of business processing. Validate the fields your application needs rather than rejecting every unrecognized field. Pin generated clients to a reviewed copy of the OpenAPI document and compare it before updating.

These contracts describe the current implementation. AXO has not published a fixed deprecation notice period. If your rollout requires one, agree it with AXO before depending on that guarantee.

OpenAPIThe machine-readable OpenAPI 3.1 spec at /api/pulse/v1/openapi.json describes profile reads, segment members, profile queries, subscriptions, and delivery recovery.