Developer documentation
Install the JavaScript snippet
For the developer of a custom-built website (not Shopify or WooCommerce, which have their own plugins). Five steps: get the snippet, paste it, connect your cookie banner, send the shop events, and report real orders from your server.
1. Get your snippet
Sign in to Aopy and open Tracking → Configuration → Website tracking. The Installation Snippet card holds the code for your account, with your public key already filled in. Press Copy Snippet. It looks like this:
<!-- Aopy Tracking -->
<script>
!function(){var c=window.cdm=window.cdm||[];if(!c.init){
c.methods=["identify","track","page","viewedProduct","addedToCart","removedFromCart","startedCheckout","checkoutStep","completedOrder","searched","addedToWishlist","removedFromWishlist","watchProduct","consent","consentState","reset","visitorId","adoptAid"];
c.factory=function(m){return function(){c.push([m].concat([].slice.call(arguments)))}};
for(var i=0;i<c.methods.length;i++)c[c.methods[i]]=c.factory(c.methods[i]);
c.load=function(k,o){c._tk=k;c._opts=o||{};var s=document.createElement("script");s.async=1;
s.src="https://app.aopy.com/tracker/v1/cdm.min.js";document.head.appendChild(s)};
}}();
cdm.load("tk_YOUR_PUBLIC_KEY", { apiUrl: "https://app.aopy.com" });
cdm.page();
</script>Your own copy may point at a first-party tracking subdomain instead of https://app.aopy.com once you have verified one. Always paste the copy from the dashboard, not the sample above.
The keys themselves are in Tracking → Configuration → Developers. There are two:
- the public key (
tk_…), already inside the snippet. It is meant to be visible in your page source. - the secret REST key (
rk_…). It is for your server only: never put it in a page, a script or an app bundle. You need it to report orders (see below).
2. Paste it in the head of every page
Put the snippet inside <head> on every page of the site: product pages, cart, checkout and the thank-you page included. In a shared layout or template that is one place. The script loads asynchronously, so it does not slow the page down. It sends the page view for you (cdm.page() is the last line of the snippet).
On a single-page app the page does not reload when the visitor moves around. Call cdm.page() yourself after every route change.
3. Connect your cookie banner (consent)
cdm.consent(true), your data will stay empty. That is expected, not a bug.If your site uses Cookiebot, OneTrust, an IAB TCF banner, Google Consent Mode or the WordPress Consent API (Gomag stores too), the tracker picks the visitor's answer up by itself: do not also hand-wire cdm.consent(). An explicit cdm.consent() call overrides what those banners say for the page, so a second, hand-written hook can only get in the way.
Any other banner: add two calls to it, cdm.consent(true) when the visitor accepts, cdm.consent(false) when the visitor rejects or later withdraws. It is safe to call them before the script has finished loading: the snippet queues the call and runs it once the tracker is ready.
// Your cookie banner calls this when the visitor ACCEPTS:
cdm.consent(true);
// ...and this when the visitor REJECTS (or withdraws later):
cdm.consent(false);
// On every page load, replay a decision you stored earlier:
if (localStorage.getItem("cookie-consent") === "accepted") {
cdm.consent(true);
}If your banner asks for several purposes separately, pass an object. Advertising consent is what allows Aopy to send a purchase to Meta, TikTok and Google Ads, so pass it only if the visitor really agreed to it.
// Analytics only - advertising stays off:
cdm.consent({ analytics: true, advertising: false });
// Each purpose takes true, false, "granted", "denied" or "unknown":
cdm.consent({
analytics: true,
advertising: true,
personalization: false,
saleSharing: false,
});
// What the tracker currently knows (mode, consent, purposes, visitor id):
cdm.consentState();
// Logout or account switch on a shared browser - forget this visitor:
cdm.reset();- A Global Privacy Control or Do Not Track signal from the browser is a binding opt-out:
cdm.consent(true)does not override it. - The consent mode is a setting of your Aopy account (all your stores share it), under Tracking → Configuration → Developers › Tracking Consent: Strict (default), Anonymous analytics, or No cookie banner. Leave it on Strict for a store in the EU.
4. Tell Aopy what visitors do
These calls take one object each. Call them when the thing happens; the ones made before the tracker has loaded are queued, so ordering is not a worry.
| Call | When |
|---|---|
| cdm.page(properties?) | Every page view. The snippet already calls it once; call it again on each route change in a single-page app. |
| cdm.viewedProduct({ product_id, product_name, value, currency, category, brand }) | On a product page. |
| cdm.addedToCart({ product_id, product_name, value, currency, quantity }) | When something goes into the cart. value is unit price x quantity. |
| cdm.removedFromCart({ product_id, quantity }) | When something leaves the cart. |
| cdm.startedCheckout({ value, currency, items }) | When the visitor reaches checkout. |
| cdm.identify({ email, firstName?, lastName?, phone? }) | When you learn the visitor's email (login, sign-up, checkout form). email is required. |
| cdm.completedOrder({ order_id, value, currency, items }) | On the thank-you page. order_id is required. This is a browser event, not an order. |
| cdm.searched({ search_query }) | On a site search. An empty query is never sent. |
| cdm.track(name, properties?) | Your own events. |
// Product page. product_id MUST equal the id of the product in your product feed.
cdm.viewedProduct({
product_id: "SKU-123",
product_name: "Wireless Headphones",
value: 99.99, // MAJOR units: 99.99, never 9999
currency: "EUR",
category: "Audio",
brand: "Acme"
});
// Add to cart - value is unit price x quantity
cdm.addedToCart({
product_id: "SKU-123",
product_name: "Wireless Headphones",
value: 99.99,
currency: "EUR",
quantity: 1
});
// Checkout page
cdm.startedCheckout({
value: 99.99,
currency: "EUR",
items: [{ product_id: "SKU-123", quantity: 1 }]
});
// The visitor gives you their email (login, sign-up, checkout form)
cdm.identify({ email: "customer@example.com", firstName: "Ana", lastName: "Popescu" });
// Thank-you page. order_id is REQUIRED, otherwise a page reload records the
// purchase twice. This is a browser event - NOT an order (see below).
cdm.completedOrder({
order_id: "1042",
value: 99.99,
currency: "EUR",
items: [{ product_id: "SKU-123", quantity: 1 }]
});- Money in the browser is in major units:
99.99, never9999. (The server Orders API below is the opposite: cents.) product_idmust equal the product's id in your product feed, so a view, a cart and a purchase land on the right product.currencyis the 3-letter code of the shop, for example EUR or RON.
5. Orders come from your server
cdm.completedOrder() in the browser is not an order. It records a purchase event in the visitor's journey. It creates no order record, is never sent to Meta, TikTok or Google Ads as a conversion, and anyone could fake it from a browser. A real order has to be reported by your server, after the payment, with your secret key.Your server calls POST /api/v1/orders with the secret rk_ key (and one for refunds and status changes). The full description, with every field and an example, is in the Orders API v1 section of the Tracking API page. Use the same value for order_id in the browser and external_id on the server, so the two are recognised as the same purchase.
Pass the visitor id to your server
The order only joins the browsing that led to it (which ad, which page) if the server sends cdm.visitorId() as aid. Read it in the browser at checkout and submit it with the order to your own server. In Strict mode it is empty until the visitor has accepted analytics consent and the tracker has loaded (in the other consent modes the id exists as soon as the tracker has loaded); when it is empty, just send the order without it.
// On the page where the buyer pays (checkout), read the visitor id and
// submit it with the order to YOUR server, for example in a hidden field:
var aid = (window.cdm && window.cdm.visitorId && window.cdm.visitorId()) || "";
document.querySelector("#aopy_aid").value = aid;
// Your server then sends it to Aopy as "aid" in POST /api/v1/orders.
// An empty value is fine: send the order without "aid".Allowed domains
Under Tracking → Configuration → Developers there is an Allowed Domains list. While it is empty, any website that has your public key can send events (“development mode”). Add your real domains (for example shop.example.com, up to 20) and only requests coming from those sites are accepted; events from anywhere else are rejected. Add your staging domain too if you want to test there. Do this before you go live.
How to check it works
- Open your site in a private window and accept your cookie banner (the tracker sends nothing before that on the default setting).
- Open the browser developer tools, tab Network. A request to
/api/trackon the address in your snippet should answer200. A401or403means the key or the allowed-domains list is wrong. No request at all usually means consent was never given. - In the Console, run the two lines below to see the tracker's own state.
// In the browser console of your site: cdm.consentState() // -> { consent: "granted", mode: "strict", purposes: {...}, aid: "..." } cdm.visitorId() // -> a string such as "a3f9c1..." once the visitor has accepted; in Strict mode it is null before that - In Aopy, Tracking → Configuration → Website tracking shows Live visitors (people on your site in the last 5 minutes) and a Test integration button that fetches your storefront and checks that tracking is installed on it.