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.
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
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:
| Event | Path after /track/ |
|---|---|
homepage_visited | shoppers/homepage_visited/{user_id} |
browsed_category | shoppers/browsed_category/{user_id}/{products}/{category_name} |
product_browsed | shoppers/product_browsed/{user_id}/{product} |
product_added | shoppers/product_added/{user_id}/{product} |
product_removed | shoppers/product_removed/{user_id}/{product} |
cart | shoppers/cart/{user_id}/{products} |
order_started | shoppers/order_started/{user_id} |
products_ordered | shoppers/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 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.
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.
| Event | When to send it | Fields besides user_id | Payload shape |
|---|---|---|---|
homepage_visited | app start, or home page opened | — | — |
browsed_category | category or listing opened | products, category_name | products = array of {"id","price","in_basket"} |
product_browsed | product detail opened | product | product = object {"id","price","in_basket"} |
product_added | item added to cart | product | product = object {"id","price","added"} |
product_removed | item removed from cart | product | product = object {"id","price","removed"} |
cart | cart opened | products | products = array of {"id","price","in_basket"} |
order_started | checkout started | — | — |
products_ordered | order completed — the only event that creates a conversion | sale_value, order, products | products = array of {"id","price","in_basket"} |
| Field | Meaning |
|---|---|
id | product identifier — must match the id attribute of <o> in your product feed, which is also what /products returns as gtin |
price | unit price, . as the decimal separator |
in_basket | quantity in the cart or in the listing |
added / removed | quantity added to or removed from the cart — used only by product_added / product_removed |
sale_value | order total |
order | your order identifier; keep it stable, it is what makes retries safe |
category_name | category 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.
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
| Response | Meaning |
|---|---|
200, image/gif | accepted |
404, text/plain | rejected: 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 understoodA 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.
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.idwhen requesting ads and asuser_idin 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 onesNEXT. - 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.
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
- Walk the whole funnel: home, category, product, add to cart, cart, checkout, order.
- Check that every call returned
200withimage/gif. - Ask us to confirm the events landed and the order was attributed. Only
products_orderedproduces 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.