APIMCP ServerWebhooksEvent TrackerCustom Integration

Event Tracker

Send BusinessChat behavioral events — page views, product views, cart activity — from any backend over one HTTP endpoint. Events that BusinessChat understands drive journeys and automations; anything else is recorded for reporting.

Overview#

The Events API is a single endpoint that accepts events about your customers and routes them by name. It is modeled on the shape of a tracking API: one call, an event name, an identity, and a bag of properties.

Every event you send falls into one of two categories:

Base URLhttps://pixel-gateway.businesschat.io
EndpointPOST /v1/track
AuthenticationYour track token, sent as Authorization: Bearer <token>. The token identifies your workspace.
Content typeapplication/json
!

Your track token is a server-side secret. Anyone holding it can send events as your workspace, so call this endpoint from your backend only — never from a browser or a mobile app — and always over HTTPS. A request without a valid token is refused with 401 and nothing is recorded.

The track call#

Every event — reserved or general — is a POST /v1/track with the same envelope. The event name decides how it is handled; the data object carries the properties for that event.

FieldTypeRequiredDescription
eventstringAlwaysThe event name. A reserved name is handled by that event's pipeline; any other name is a general event.
sessionIdstringReserved eventsA stable ID you choose for one visit. Ties a visitor's events together. Required for every reserved event; not used by orders or general events.
userIdstringSome eventsThe contact's phone number in E.164 form. Required for general events and for contact_identified; optional but recommended on the rest — see Identity & sessions.
occurredAtstringOptionalWhen the event happened, ISO-8601 with a timezone (e.g. 2026-09-06T10:00:00Z). Defaults to the time we receive it.
dataobjectOptionalProperties for this event. Which keys matter depends on the event; see each event below. Unknown keys are ignored.

A minimal call and its response:

curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "event": "product_viewed",
    "sessionId": "sess-9f2c",
    "userId": "+966501234567",
    "data": { "productId": "SKU-1", "productName": "Blue Shirt" }
  }'

A successful call returns 200 with the ID we assigned the event:

{ "eventId": "px-1f3a9c20-6b7e-4a51-9d2c-8e0b4f7a1c33" }
i

The envelope always wins over data: repeating an envelope field like sessionId inside data has no effect. The workspace always comes from your token; a businessId in the body is ignored.

Identity & sessions#

Two fields identify who an event is about and how its events group together.

userId — the contact

userId is the contact's phone number in E.164 form, e.g. +966501234567. Formatting is tolerated — spaces, dashes, brackets, and Arabic-Indic digits are cleaned up — and the endpoint turns away only what is obviously not a number.

!

A number this endpoint accepts can still be refused later. We check only that a value is plausibly a phone number, and our stricter parser decides afterwards — it is less forgiving for some countries, and a national number with no country code is the common case. When it refuses one, the event is dropped after you have already had a 200. Always send the full international form, and if an event never shows up, quote the eventId from the response to support: it appears in the log line that names the field which failed.

!

Without a userId, an event is recorded but nobody can be messaged about it. Journeys and abandonment automations only act on a session once it has a contact. Send the phone as soon as you know it — either on the event itself, or with a contact_identified event for the same sessionId. From then on the whole session becomes actionable.

sessionId — the visit

sessionId is a stable string you choose that scopes one visit. Events in your workspace sharing a sessionId are treated as one journey. Use any stable identifier — a cookie value, a server session ID — and keep sending the same one for the life of the visit.

Reserved events#

These are the events BusinessChat acts on. Each needs its sessionId, and some need specific keys inside data — those are listed as required below and are checked when the event is received. Everything else is enrichment: it improves what an automation can say, but its absence never rejects the event.

i

Required data keys are checked for presence only. A present but wrong value (an unparseable field, an unknown enum name) is accepted here — you still get 200 — but the event is dropped further down and never acted on. The exceptions that are checked up front are called out per event below.

session_started#

The visitor has begun a session. Send it once at the start of a visit.

KeyInRequiredDescription
urldataYesThe page the session started on.
curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "event": "session_started", "sessionId": "sess-9f2c",
        "data": { "url": "https://shop.example/home" } }'

website_visited#

A page was viewed within a session.

KeyInRequiredDescription
urldataYesThe page that was viewed.
curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "event": "website_visited", "sessionId": "sess-9f2c",
        "data": { "url": "https://shop.example/products/blue-shirt" } }'

contact_identified#

You have learned the visitor's phone number. Send it to attach the contact to the session; from here on the session can be messaged.

KeyInRequiredDescription
userIdenvelopeYesThe contact's phone number. This event is rejected without it.
curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "event": "contact_identified", "sessionId": "sess-9f2c",
        "userId": "+966501234567" }'

product_viewed#

The visitor looked at a product.

KeyInRequiredDescription
productIddataRecommendedThe product identifier. A blank or missing productId is accepted but recorded as generic activity rather than a product view.
productPricedataOptionalThe product's price. If you send it, you must also send a valid productCurrency.
productCurrencydataWith priceAn uppercase ISO-4217 code (e.g. SAR, USD). Checked when a productPrice is present — an invalid code is rejected with 422. Omit both to send no price.
Also accepted (enrichment): productName, productUrl, productImageUrl, productStatus, productInStock, productProvider, productJson.
curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "event": "product_viewed", "sessionId": "sess-9f2c",
        "userId": "+966501234567",
        "data": { "productId": "SKU-1", "productName": "Blue Shirt",
                  "productPrice": 49.0, "productCurrency": "SAR" } }'

added_to_cart#

The visitor added a product to their cart.

KeyInRequiredDescription
productIddataYesThe product added.
cartProductsdataStrongly recommendedThe cart's full current contents — an array of {productId, quantity}. This replaces what we hold for this shopper. Leave it out and we remember only the product in this call, so a recovery message names one item instead of the cart. See how cart recovery works.
addedAtdataRecommendedWhen the cart changed, ISO-8601. This is the idempotency key: the same value resent is ignored, a later value is applied again. Omit it and we substitute the time we received the call, so a retry counts twice.
Also accepted: qty (defaults to 1), cartId, cartUrl, cartTotal, currency, variantId, cartJson.
curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "event": "added_to_cart", "sessionId": "sess-9f2c",
        "userId": "+966501234567",
        "data": { "productId": "SKU-1", "qty": 2, "addedAt": "2026-09-07T10:15:00Z",
                  "cartProducts": [ { "productId": "SKU-1", "quantity": 2 },
                                    { "productId": "SKU-9", "quantity": 1 } ],
                  "cartId": "cart-77", "cartUrl": "https://shop.example/cart",
                  "cartTotal": "98.00", "currency": "SAR" } }'

checkout_started#

The visitor began checkout.

KeyInRequiredDescription
checkoutUrldataYesThe URL to resume checkout.
productsIdAndQtyListdataRecommendedThe cart contents as "productId:qty" strings, e.g. ["SKU-1:2", "SKU-9:1"].
Also accepted (enrichment): cartTotal, currency, cartId, cartUrl.
!

An abandoned-checkout message can only name the cart's contents and value if you send them. Include productsIdAndQtyList (in the "productId:qty" format) along with cartTotal and currency; otherwise the recovery message goes out with no cart details.

curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "event": "checkout_started", "sessionId": "sess-9f2c",
        "userId": "+966501234567",
        "data": { "checkoutUrl": "https://shop.example/checkout/9f2c",
                  "productsIdAndQtyList": ["SKU-1:2", "SKU-9:1"],
                  "cartTotal": "147.00", "currency": "SAR" } }'

removed_from_cart#

The visitor removed a product from their cart.

KeyInRequiredDescription
productIddataYesThe product removed.
cartProductsdataStrongly recommendedThe cart's full contents after the removal. Same rule as added_to_cart: it replaces what we hold. Send an empty array when the cart is now empty.
removedAtdataRecommendedWhen the cart changed, ISO-8601. The idempotency key, exactly as addedAt is for added_to_cart.
Also accepted: qty (defaults to 1), cartId, cartUrl, cartTotal, currency, variantId, cartJson.
curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "event": "removed_from_cart", "sessionId": "sess-9f2c",
        "userId": "+966501234567",
        "data": { "productId": "SKU-1", "qty": 1,
                  "removedAt": "2026-09-07T10:20:00Z",
                  "cartProducts": [ { "productId": "SKU-9", "quantity": 1 } ] } }'

How cart recovery works#

Cart events do more than record activity: they are what makes abandoned-cart recovery possible. It is worth knowing how the timing works, because nothing you send triggers a message directly.

  1. You send cart events. Each one updates what BusinessChat believes is in this shopper's cart, and refreshes their activity clock.
  2. The shopper goes quiet. After roughly four hours with no further events, their session is closed.
  3. If the cart was still open at that moment, recovery runs — the abandoned-cart automation sends its WhatsApp message.

So recovery is inferred from silence, on our side. There is no event you can send that means "send the recovery message now", and none that skips the wait.

!

Two things decide whether that message is any good, and both are easy to get wrong:

  • Send cartProducts every time. It replaces the contents we hold. Without it we keep only the product named in the most recent call, and the recovery message reflects that.
  • Send a stable addedAt / removedAt. It is what makes a retry safe. Omit it and a repeated call is treated as a second, later change — quantities add up.

An order cancels it. If an order arrives for the shopper before the message goes out, recovery is called off rather than sent — you do not need to tell us to stop.

What the message can say. The recovery template can use the cart link, the cart total, the currency and the cart id. Product names are not available — only product ids reach this stage — so a template cannot list the items by name. Send cartUrl and cartTotal with currency if you want the message to name a value and link back.

heartbeat#

Keeps a session alive while the visitor is active. Sessions also close on their own after inactivity, so heartbeats are optional for a backend integration.

KeyInRequiredDescription
statedataYesOne of the exact values below. The value is used only to accept the event; send SESSION_STARTED unless you have a reason not to.
lastActivityAtdataOptionalWhen the visitor was last active, ISO-8601.

Valid state values (uppercase, exact): SESSION_STARTED, SESSION_ENDED, PRODUCT_VIEWED, CART_ADDED, CHECKOUT_STARTED, ORDER_COMPLETED, CART_ABANDONED.

curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "event": "heartbeat", "sessionId": "sess-9f2c",
        "data": { "state": "SESSION_STARTED" } }'

Orders#

An order sent here becomes the same order BusinessChat builds from a connected store, so everything that already reacts to an order reacts to this one: the contact and conversation are created, post-purchase and order-status automations run, the customer's journey advances, and the revenue is attributed.

There are two event names:

EventWhen to send it
order_createdA new order was placed.
order_updatedAnything about an existing order changed — most often its status.
i

There is no order_paid or order_cancelled event. Both events carry a complete snapshot of the order, and the lifecycle lives in its status — so "the order was paid" is order_updated with "status": "paid". Send the whole order every time, not a diff.

Orders carry no sessionId — an order is not a browsing session. They do require userId: the order creates the contact, and every automation it drives needs someone to message.

Fields#

i

Optional does not mean unchecked. Every value listed below is validated if you send it. An unusable one is rejected with a 422 naming the exact field — it never silently costs you the order. Fields you omit simply take their default.

Required

KeyInTypeRule
userIdenvelopestringThe customer's phone number, E.164 (+966501234567).
order_iddataintegerMust be numeric. Our order id is a number end to end, so a UUID or "ORD-1001" is rejected — put that form in reference_id.
statusdatastringOne of the 26 status values.
total.amountdatanumber0 is valid (a free order); omitting it is not.
total.currencydatastringISO-4217, e.g. SAR, USD, EUR.
created_atdatastringISO-8601 with a timezone — 2026-09-06T10:00:00Z or …+03:00. A naive timestamp is rejected.

Optional — order

KeyTypeAllowed values / ruleDefault if omitted
reference_idstringAny text — your display reference.The order_id.
updated_atstringWhen the order last changed. ISO-8601 with a timezone, same rule as created_at. Send it whenever you know it — it is what status automations are timed against.For order_updated, the time we receive the call. For order_created, created_at.
historicalbooleantrue or false — not "true". true records the order without firing automations; use it when backfilling, or customers get messaged about old purchases.false.
payment_methodstringAny text.—
promo_codestringAny text.—
order_linkstringAny text; a URL is expected but not enforced.—
trackingobjectlink, number, carrier — all strings, all optional.—
shipping_addressobjectMust be an object. country, city, street, postal_code — all strings, all optional; a partial address is fine.—
productsarrayMust be an array of objects. Each entry needs a non-empty id (string); name (string) and quantity (integer) are accepted but not stored — only the ids are kept.Empty.
customerobjectMust be an object. See below.—

Optional — the customer object

KeyTypeAllowed values / ruleDefault if omitted
namestringAny text."-"
genderstringExactly male or female (case-insensitive). Anything else is rejected.—
birthdaystringISO-8601 date, 1990-04-01. A full timestamp is accepted — only the date part is read.—
opted_inbooleantrue or false — not "true". Whether the customer may be messaged for marketing.true
languagestringen, ar, es, pt_BR (also english, arabic, …). An unrecognised value falls back to English rather than failing.en
emailstringAny text; a valid address is expected but not enforced.—
external_idstringYour own customer id. Only kept if it is numeric — a non-numeric value is accepted and dropped.—
avatarstringAny text; a URL is expected but not enforced.—
curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "event": "order_created",
    "userId": "+966501234567",
    "data": {
      "order_id": 1001,
      "reference_id": "ORD-1001",
      "status": "processing",
      "total": { "amount": 149.00, "currency": "SAR" },
      "created_at": "2026-09-06T10:00:00Z",
      "order_link": "https://shop.example/orders/1001",
      "products": [ { "id": "SKU-1", "name": "Blue Shirt", "quantity": 2 } ],
      "customer": { "name": "Sara" }
    }
  }'

Reporting a status change later is the same call with order_updated and the new status:

curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "event": "order_updated",
    "userId": "+966501234567",
    "data": {
      "order_id": 1001,
      "status": "paid",
      "total": { "amount": 149.00, "currency": "SAR" },
      "created_at": "2026-09-06T10:00:00Z",
      "updated_at": "2026-09-06T11:15:00Z"
    }
  }'

Status values#

Send one of these words, lower-case. They are listed in lifecycle order:

new, created, pending, payment_pending, authorized, partially_paid, processing, under_review, preparing, ready, shipped, delivering, paid, delivered, completed, closed, abandoned, restored, restoring, partially_refunded, cancelled, refunded, failed, denied, voided, expired.

!

A status outside this list is accepted and recorded, but drives nothing: no status automation runs and the order is left out of revenue attribution. Nothing in the response tells you — the request still returns 200 — so send a word from this list.

General events#

Any event name that is not a reserved name is a general event. Use these to record facts BusinessChat has no built-in behavior for — a loyalty tier change, a newsletter signup — so you can report on them alongside everything else.

FieldRequiredDescription
eventYesYour event name.
userIdYesThe contact's phone number. A general event must name a contact.
dataOptionalAny properties. The whole request body is recorded exactly as you sent it.
curl -X POST 'https://pixel-gateway.businesschat.io/v1/track' \
  -H 'Authorization: Bearer YOUR_TRACK_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "event": "loyalty_tier_changed", "userId": "+966501234567",
        "data": { "tier": "gold", "previous": "silver" } }'

Like every accepted event, a general event returns its eventId. There is no sessionId requirement.

Responses#

Every accepted event, reserved or general, returns 200 with the ID we assigned it:

{ "eventId": "px-1f3a9c20-6b7e-4a51-9d2c-8e0b4f7a1c33" }

A request without a valid track token returns 401, whatever is wrong with the token, and nothing is recorded:

{ "error": "Missing or invalid track token. Send it as `Authorization: Bearer <token>`." }

A request that fails validation returns 422 with a message naming the problem:

{ "error": "Missing field: data.url, required for session_started" }
i

A 200 means the event was accepted, not that every downstream automation ran. A value we can accept but cannot fully interpret is recorded and then dropped from journey processing — see Good to know.

Errors#

A missing, malformed or invalid track token returns 401 before the body is read; see Responses. All validation failures return 422 with an error string. The full set:

MessageCause
Body is not a JSON objectThe body did not parse as a JSON object.
Missing field: eventevent is absent or blank.
Missing field: sessionId, required for a reserved eventA reserved event has no sessionId.
Missing field: userId, required for <event>An event that needs a contact (a general event, or contact_identified) has no userId.
Field userId is not a phone number: <value>userId is not a recognizable phone number.
Missing field: data.<field>, required for <event>A reserved event is missing a required data key.
Field data.productCurrency is not an ISO-4217 currency code: <value>A product_viewed sent a productPrice with an invalid productCurrency.
Field data.<field> must be <expected>An order field is present but unusable, and the message names it: a non-numeric order_id, a total.amount that is not a number, a total.currency that is not ISO-4217, a timestamp with no timezone, a historical or customer.opted_in that is not true/false, a shipping_address or customer that is not an object, a products that is not an array or whose entry has no id, an unrecognised customer.gender, or an unreadable customer.birthday.
Field occurredAt is not an ISO-8601 timestamp: <value>occurredAt could not be parsed.
Field data must be an objectdata was present but not a JSON object.

Good to know#