Skip to main content

Events over HTTP

Each event is one HTTP request. The tracker id goes in the URL, your user identifier and the event payload go in the path, and the answer is a 1×1 GIF. Any HTTP client will do, from your backend or from the application itself.

Each event type has its own tracker id, and all of them accept your own user identifier — the same value you use on your website and in your app. The Mobile SDK and the JavaScript snippets on Tracking & Events report to the same trackers, so the data lands in one place regardless of which transport a given surface uses.

There is a second endpoint, a single key for all event types, used by our mobile SDKs. It identifies users by the device advertising identifier, so use the per-event endpoint described here if you identify users yourself.

Why the identifier matters​

The identifier is the only thing that links an ad, a click and an order. We do not merge profiles, so one user seen under two identifiers is two users to us.

That is what makes this endpoint the right choice for a shop: you send your own identifier, the same value on your website and in your app, and both surfaces resolve to one profile. An order placed in the app is then attributed to an impression served on the web.

The request​

GET https://delivery.mageads.com/tracker={TRACKER_ID}/track/shoppers/{values}/track.gif

Every event type has its own tracker id; we give you all eight with your credentials. There is no API key here — the tracker id identifies the tracker, and the trackers belong to your account. POST works on the same URL if it suits your stack better; the response is identical.

The panel gives you the ready URL

In the MageAds panel, open the tracker and pick the URL code type. You get the full address for that exact tracker with placeholders — {USER_ID}, {ID}, {PRICE}, {QUANTITY}, {CATEGORY_NAME}, {SALE_VALUE}, {ORDER} — already in the order that tracker expects. Substituting placeholders in that string is the shortest correct path, and it is authoritative if anything on this page ever disagrees with it.

Values are carried in path segments. Two notations work, and they can be mixed:

named       …/track/shoppers/user_id=42f1b8c0/sale_value=249.90/order=ORD-123/products=%5B…%5D/track.gif
positional …/track/shoppers/products_ordered/42f1b8c0/249.90/ORD-123/%5B…%5D/track.gif
Prefer the named form

With named segments the order does not matter, you can leave out what you do not have, and the request keeps working if we ever add a field to your tracker. The named form is also the only one that does not depend on how your tracker is configured.

Every value must be URL-encoded, and no value may contain an unencoded /. This applies to the JSON payloads and to category names.

Positional form​

If you build the path positionally, the segments are:

/tracker={TRACKER_ID}/track/shoppers/{event}/{user_id}/{payload…}/track.gif

The first segment is always shoppers, the second is the event name, the third is your identifier, and the rest is the payload in the order below:

EventPath after /track/
homepage_visitedshoppers/homepage_visited/{user_id}
browsed_categoryshoppers/browsed_category/{user_id}/{products}/{category_name}
product_browsedshoppers/product_browsed/{user_id}/{product}
product_addedshoppers/product_added/{user_id}/{product}
product_removedshoppers/product_removed/{user_id}/{product}
cartshoppers/cart/{user_id}/{products}
order_startedshoppers/order_started/{user_id}
products_orderedshoppers/products_ordered/{user_id}/{sale_value}/{order}/{products}

Example — products_ordered with sale_value 249.90, order ORD-123 and one product:

curl 'https://delivery.mageads.com/tracker=<ID_PRODUCTS_ORDERED>/track/shoppers/products_ordered/42f1b8c0/249.90/ORD-123/%5B%7B%22id%22%3A%22SKU-1%22%2C%22price%22%3A%22249.90%22%2C%22in_basket%22%3A1%7D%5D/track.gif'
The positional order belongs to your tracker, not to this table

The table above is the default layout. The real order is whatever is configured on your tracker, and it can contain additional fields. Always confirm it against the URL code in the panel for that tracker before you hard-code a path.

A mismatch does not produce an error. If the path is one segment short, every following value lands on the wrong field: we have seen an order recorded with the correct total while the product list silently ended up in another field and the products were stored empty. The call answers 200 and the conversion looks healthy in reporting. An empty segment does the same, because empty segments are skipped rather than counted.

If a tracker has no fields configured at all, a positional path returns 404, because your identifier never reaches us. The named form works in every one of these cases.

Event reference​

user_id is required in every event. Send your own identifier as-is — a UUID such as 0040e036-24f1-44c0-863c-d397c991f8dd is a good choice. It must match:

^[a-zA-Z0-9-_]{1,50}$

A value that does not match is rejected with 404.

Use one identifier everywhere

It must be the exact same string you send as user.id when requesting ads, on your website and in your app alike, and it must not change when the user logs in.

If you base64url encode the identifier for the ads API, as User Identification describes, send that same encoded value here — encoded in both places, with the padding removed. What matters is that the two requests carry an identical string.

A value that differs between the ad request and the event is a different user to us, the order stops matching the impression, and no conversion is recorded. The call still answers 200, so nothing tells you it happened.

EventWhen to send itFields besides user_idPayload shape
homepage_visitedapp start, or home page opened——
browsed_categorycategory or listing openedproducts, category_nameproducts = array of {"id","price","in_basket"}
product_browsedproduct detail openedproductproduct = object {"id","price","in_basket"}
product_addeditem added to cartproductproduct = object {"id","price","added"}
product_removeditem removed from cartproductproduct = object {"id","price","removed"}
cartcart openedproductsproducts = array of {"id","price","in_basket"}
order_startedcheckout started——
products_orderedorder completed — the only event that creates a conversionsale_value, order, productsproducts = array of {"id","price","in_basket"}
FieldMeaning
idproduct identifier — must match the id attribute of <o> in your product feed, which is also what /products returns as gtin
priceunit price, . as the decimal separator
in_basketquantity in the cart or in the listing
added / removedquantity added to or removed from the cart — used only by product_added / product_removed
sale_valueorder total
orderyour order identifier; keep it stable, it is what makes retries safe
category_namecategory name as shown to the user

Note the singular product for the three single-product events and the plural products for the three list events, and that the quantity field name differs between them. This is the most common copy-paste mistake.

Product identifiers must match your feed

If your feed is keyed by GTIN and your app sends internal database IDs, events are accepted with 200 but never match a campaign and no conversion is recorded.

Copy-paste examples​

Replace <ID_…> with the tracker id for that event and 42f1b8c0 with your user identifier.

homepage_visited​

curl 'https://delivery.mageads.com/tracker=<ID_HOMEPAGE_VISITED>/track/shoppers/user_id=42f1b8c0/track.gif'

browsed_category​

products = [{"id":"SKU-1","price":"249.90","in_basket":1}], category_name = Obuwie > Sneakers

curl 'https://delivery.mageads.com/tracker=<ID_BROWSED_CATEGORY>/track/shoppers/user_id=42f1b8c0/products=%5B%7B%22id%22%3A%22SKU-1%22%2C%22price%22%3A%22249.90%22%2C%22in_basket%22%3A1%7D%5D/category_name=Obuwie%20%3E%20Sneakers/track.gif'

product_browsed​

product = {"id":"SKU-1","price":"249.90","in_basket":1}

curl 'https://delivery.mageads.com/tracker=<ID_PRODUCT_BROWSED>/track/shoppers/user_id=42f1b8c0/product=%7B%22id%22%3A%22SKU-1%22%2C%22price%22%3A%22249.90%22%2C%22in_basket%22%3A1%7D/track.gif'

product_added​

product = {"id":"SKU-1","price":"249.90","added":1}

curl 'https://delivery.mageads.com/tracker=<ID_PRODUCT_ADDED>/track/shoppers/user_id=42f1b8c0/product=%7B%22id%22%3A%22SKU-1%22%2C%22price%22%3A%22249.90%22%2C%22added%22%3A1%7D/track.gif'

product_removed​

product = {"id":"SKU-1","price":"249.90","removed":1}

curl 'https://delivery.mageads.com/tracker=<ID_PRODUCT_REMOVED>/track/shoppers/user_id=42f1b8c0/product=%7B%22id%22%3A%22SKU-1%22%2C%22price%22%3A%22249.90%22%2C%22removed%22%3A1%7D/track.gif'

cart​

products = [{"id":"SKU-1","price":"249.90","in_basket":1}]

curl 'https://delivery.mageads.com/tracker=<ID_CART>/track/shoppers/user_id=42f1b8c0/products=%5B%7B%22id%22%3A%22SKU-1%22%2C%22price%22%3A%22249.90%22%2C%22in_basket%22%3A1%7D%5D/track.gif'

order_started​

curl 'https://delivery.mageads.com/tracker=<ID_ORDER_STARTED>/track/shoppers/user_id=42f1b8c0/track.gif'

products_ordered​

sale_value = 249.90, order = ORD-2026-000123, products = [{"id":"SKU-1","price":"249.90","in_basket":1}]

curl 'https://delivery.mageads.com/tracker=<ID_PRODUCTS_ORDERED>/track/shoppers/user_id=42f1b8c0/sale_value=249.90/order=ORD-2026-000123/products=%5B%7B%22id%22%3A%22SKU-1%22%2C%22price%22%3A%22249.90%22%2C%22in_basket%22%3A1%7D%5D/track.gif'

Responses​

ResponseMeaning
200, image/gifaccepted
404, text/plainrejected: unknown or inactive tracker id, or user_id in an invalid format

The response is a 1×1 GIF, so the same URL works in an <img> tag in a browser and a backend can ignore the body. A rejection does not say what was wrong, so log your URLs while integrating.

200 does not prove the event was understood

A malformed positional path answers 200 as well, and so does an event from a user who has never seen an ad — correctly, since there is nothing to attribute. During integration, confirm with us that events landed instead of relying on the status code.

Retries​

products_ordered is safe to retry as long as order stays the same: we recognise the repeated value and do not count the conversion twice. The check has no time limit. The other events are not deduplicated, so a retry records them again — they feed audience segments rather than conversions, so the impact is limited.

Retry on 5xx and on timeouts, with a growing delay. Do not retry a 404; fix the request first.

Never retry impression or click pixels

The pixel_view and pixel_click URLs returned by /products have no deduplication: the same pixel_view URL fired three times counts three impressions and bills for three. At-most-once delivery for those pixels is your responsibility, and it covers re-rendering a list or returning to a screen, not only retries.

Attribution​

  • The same identifier must be sent as user.id when requesting ads and as user_id in events.
  • The last click takes precedence over the last impression (post-click before post-view).
  • The attribution window is configured per tracker, separately for post-click and post-view, and is measured from the moment we recorded the click or impression.
  • A user's first conversion on a tracker is marked FIRST, later ones NEXT.
  • The conversion carries the campaign, creative and placement the click or impression came from, so it is visible per ad placement in reporting.
  • None of this uses browser cookies, which is why an app with no cookies attributes correctly.
The identifier must not change when the user logs in

If the ad was seen under anonymous identifier A and the order arrives with account identifier B, there is no conversion. Send the same identifier before and after login, or map A to B on your side and keep sending A.

Verifying the integration​

  1. Walk the whole funnel: home, category, product, add to cart, cart, checkout, order.
  2. Check that every call returned 200 with image/gif.
  3. Ask us to confirm the events landed and the order was attributed. Only products_ordered produces a conversion record; the other seven update the audience profile, so we confirm those on our side.

Single-key endpoint (mobile SDK)​

Our mobile SDKs report to a different endpoint, which takes one shared tracker key and the event as JSON:

POST https://delivery.mageads.com/tracker=multi/track/multi/track.json
Content-Type: application/x-www-form-urlencoded

data=<URL-encoded JSON object>

One shared key covers all event types, and the event travels as JSON rather than in the path. It still carries one event per request — it is not a batch endpoint.

This endpoint identifies users by the device advertising identifier, which the SDK reads from the platform. If you identify users yourself, use the per-event endpoint above; we document this one because the SDK uses it, not as an alternative for a shop integration. A successful call answers {"code":200}; anything else, including a 200 with an HTML body, means the event was not recorded.