Login
ZappushZappush Docs
Reference

Server events

Send conversions from your own backend, such as a sign-up, a booking or a late-confirmed order, so they land on the visit and the ad that brought the shopper.

Some conversions happen where the Zappush script cannot run: a sign-up or checkout app on another system, a booking backend, an order that is only confirmed later. Server events let your own backend report those conversions to Zappush. Each one lands on the visit and the ad that brought the shopper, reaches every connected ad platform, and shows in your analytics and attribution.

How it works

You set up two small pieces. One runs in the browser, one runs on your server.

  1. Your own system already keeps an id for each browser in a first-party cookie, for example acme_clid. Zappush cannot see your backend, but it can see that cookie on pages where the Zappush tracker runs.
  2. On those pages, a short snippet reads the cookie and reports its value to Zappush. This is the join key: the shared id that lets Zappush connect the browser visit to the event your server sends later.
  3. When the conversion happens, your server sends the event to Zappush with the same id. Zappush finds the visitor who reported it and records the event on them.

Zappush stores the id only as a one-way hash. It links the id only when the visitor has agreed to marketing cookies. With no consent, nothing is linked, and a later server event is still recorded, just without the ad link. The join_key_seen event that reports the id is never sent to any ad platform.

Add this snippet to every page that has the Zappush tracker installed and your cookie set. Put it wherever you can add custom JavaScript: your site, a tag manager, or, if Zappush support manages your setup, ask support to add it for you. Change KEY to the name of your cookie.

<script>
(function () {
  var KEY = "acme_clid";
  var match = document.cookie.match(new RegExp("(?:^|;\\s*)" + KEY + "=([^;]+)"));
  if (!match) return;
  var value = decodeURIComponent(match[1]);
  var sentKey = "zp_join_" + KEY;
  try { if (localStorage.getItem(sentKey) === value) return; } catch (e) {}
  if (!window.zappush || !window.zappush.track) return;
  window.zappush.track("join_key_seen", { join_key: KEY, value: value });
  try { localStorage.setItem(sentKey, value); } catch (e) {}
})();
</script>

The snippet sends once per value per browser. It remembers the last value it sent in the browser's local storage and skips repeats, so it does not add an event on every page.

The snippet must run on a page that has the tracker installed. It relies on the small inline block from Install the tracker, which holds calls made before the script finishes loading. Place the snippet after that block. If you have not installed the tracker yet, do that first.

Step 2: send the event from your server

POST https://api.zappush.com/v1/events/ingest
HeaderValue
AuthorizationBearer <server API key>
Content-Typeapplication/json

Use a server API key with the events:write scope. Create one in Settings → Store → Developer → Server API keys (see Developer & API) and tick events:write. If this server only sends server events, tick nothing else. A key without events:write is refused with a 401.

Call this endpoint from your server only. Never put the key in browser code or in an app that runs on a shopper's device.

Body

Send JSON, up to 64 KB. Fields not listed here are dropped and never stored. The field names match the data layer and the SDK, so a purchase you already send from the browser has the same shape here.

The event

FieldTypeRequiredNotes
event_idstring, 1 to 200 charactersYesYour own unique id for this event. Sending the same id again is answered as a duplicate and recorded once, so retries are safe.
event_namestringYesOne of generate_lead, user_signed_up, schedule, checkout_started, payment_info_submitted, checkout_completed. The legacy lead_generated is also accepted; use generate_lead for new work.
occurred_atISO 8601 timestampYesWhen it happened. A time more than 10 minutes in the future is refused.
join_keysobject, up to 5 entriesNo{ "<cookie name>": "<value>" }, the same name and value the snippet reported. A name uses letters, digits, _, . and -, up to 64 characters. A value is up to 200 characters. Zappush tries them in the order you send them, and the first one a visitor reported wins.

The buyer, in user. Every field is optional. Send what you have: each one improves how well ad platforms match the conversion to a person.

FieldNotes
emailThe raw email. Handled according to your workspace privacy setting.
email_sha256Or instead: SHA-256 of the email, lowercased and trimmed, as 64 hex characters.
phoneThe raw phone, in E.164, for example +15551234567.
phone_sha256Or instead: SHA-256 of the phone in E.164 digits without the +.
external_idYour own customer id. It also finds the shopper on its own.
first_name, last_name
city, state, zipstate is the state or province.
countryISO 3166-1 alpha-2, for example US.
date_of_birthYYYY-MM-DD.
gender
ip_addressThe buyer's IP address as your server saw it, never your server's own.
user_agentThe buyer's browser user agent.

When a join key matches, the event takes the IP address and user agent of the visit it lands on, so you only need ip_address and user_agent for events that have no join key.

The order. order_id, value and currency are required on checkout_completed; the rest are optional.

FieldTypeNotes
order_idstringYour order id. It identifies the order in reports and refunds.
valuenumber, 0 or moreThe order total. Send together with currency.
currency3-letter ISO codeFor example USD.
couponstringThe discount code used.
itemsarray, up to 100One entry per product line, below.

Each entry in items:

FieldRequiredNotes
item_idYesIdentifies the product.
item_nameNoProduct name in your dashboard.
item_variantNoVariant name in your dashboard.
item_categoryNoProduct type in your dashboard.
priceNoUnit price.
quantityNoA whole number. Defaults to 1.

Products reach Meta only under Zappush's own catalog ids, never under your item_id and never with a name.

How an event is matched

Zappush matches each event in this order and stops at the first rule that works.

  1. A join key a visitor reported. The event lands on that visitor, their visit, and the ad click they came from.
  2. Otherwise, the email, phone or external id. The event lands on that shopper. A raw email or phone, or an external_id, always works here. A hash alone works only when Privacy mode is on, because that is when Zappush keeps contact details as hashes. With Privacy mode off, a hash still reaches your ad platforms as a match key, but it cannot find the shopper.
  3. Otherwise, the event is still recorded so your revenue counts, but it is not tied to a visit.

Send the email hash along with the join key. It improves match quality on ad platforms.

Examples

A purchase with a join key, the buyer and the order lines:

curl -X POST https://api.zappush.com/v1/events/ingest \
  -H "Authorization: Bearer $ZAPPUSH_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "evt_8f3a1c92",
    "event_name": "checkout_completed",
    "occurred_at": "2026-11-11T09:30:00Z",
    "join_keys": { "acme_clid": "c1a7e5d0-93b2-4f6e-8a41-52d9b0e7f314" },
    "user": {
      "email_sha256": "b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514",
      "external_id": "cust_5521",
      "first_name": "Alex",
      "last_name": "Smith",
      "city": "Austin",
      "state": "TX",
      "zip": "78701",
      "country": "US"
    },
    "order_id": "ord_20481",
    "value": 158.0,
    "currency": "USD",
    "coupon": "SUMMER10",
    "items": [
      { "item_id": "SKU-001", "item_name": "Running Shoes", "item_variant": "Blue / 10", "item_category": "Footwear", "price": 79.0, "quantity": 2 }
    ]
  }'

The same call from Node:

const res = await fetch("https://api.zappush.com/v1/events/ingest", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ZAPPUSH_SERVER_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    event_id: "evt_8f3a1c92",
    event_name: "checkout_completed",
    occurred_at: new Date().toISOString(),
    join_keys: { acme_clid: clickIdFromYourDatabase },
    user: { email_sha256: emailHash },
    order_id: "ord_20481",
    value: 158.0,
    currency: "USD",
  }),
});
const result = await res.json();

Responses

A new event is accepted:

{ "status": "accepted", "data": { "event_id": "evt_8f3a1c92", "matched_by": "join_key" } }

matched_by is join_key, contact or none. It tells you which matching rule worked, which is useful while you test.

StatusMeaning
202Accepted.
200{ "status": "duplicate", "data": { "event_id": "..." } }. This event_id was already received, and nothing new was recorded.
400The request is wrong. The body is { "status": "error", "message": "..." }. Examples: "checkout_completed needs order_id, value and currency", "value and currency are sent together", "email_sha256 must be a 64-character hex SHA-256 digest", "occurred_at must be an ISO 8601 timestamp", "occurred_at is in the future", "user.ip_address must be an IPv4 or IPv6 address", "store is not set up to receive events yet".
401The key is missing, invalid, or does not have the events:write scope.
413The body is over 64 KB.
422The body failed validation, for example an unknown event_name, an item without item_id, or a wrong type.
429More than 600 requests a minute on this key. Wait for the number of seconds in the Retry-After header, then retry.
500A fault on our side. Retry later with the same event_id.

Keep sensitive details out

Send only the fields listed above.

  • Never put a product name, a plan name, or anything that describes why someone bought into event_id or order_id. Use opaque ids, such as a random string or a plain counter.
  • If a product name or category would reveal something sensitive about the buyer, send an opaque item_id and leave item_name and item_category out.
  • Prefer email_sha256 and phone_sha256 over the raw email and phone when Privacy mode is on.

Timing

Ad platforms accept a conversion for only a limited time after it happens, and some allow as little as 7 days. Send events promptly.

If an event is confirmed days after the shopper acted, set occurred_at to the time it was confirmed or the time it happened, whichever you want credited. Either way, the event must still arrive inside that window.

Test it

  1. Open a page on your site that has the Zappush tracker and your cookie set, so the snippet reports the cookie value.
  2. Send an event from your server using that cookie's value in join_keys.
  3. Check that the response has matched_by set to join_key.
  4. Open the Events screen in analytics. The event appears there.

If matched_by is none, no visitor has reported that value and no contact matched. Confirm the visitor agreed to marketing cookies, that the snippet runs on a page with the tracker installed, and, if you send only a hash, that Privacy mode is on.

On this page