POST /api/track ingests events from Salla themes (Angel + Neptune), verifies HMAC, and stores them in tracking_events for analytics.
#Request
POST /api/track HTTP/1.1
Host: afterads.com
Content-Type: application/json
X-Signature: 5e88e2d4...
X-Timestamp: 1715174400
{
"event": "product_view",
"store_id": "salla:1234567890",
"session_id": "...",
"user_id": "salla:9876543210",
"metadata": {
"product_id": "1828908128",
"category": "fashion",
"currency": "SAR",
"price": 350
}
}#Event types
| Event | Description | metadata required |
|---|---|---|
store_view | Generic visit | referrer, page |
product_view | Product page view | product_id, category |
addtocart | Add to cart | product_id, quantity, price |
begin_checkout | Checkout start | carttotal, itemscount |
purchase | Order complete | order_id, total, items |
signup | Customer signup | email_hash |
review_submit | Product review | product_id, rating |
search | Site search | query, results_count |
#HMAC verification
The server verifies X-Signature like this:
import { createHmac } from 'crypto';
const expected = createHmac('sha256', SECRET_KEY)
.update(timestamp + ':' + JSON.stringify(body))
.digest('hex');
if (!constantTimeEqual(expected, providedSignature)) {
return 401;
}#Signing key
#Full example (Salla theme)
// Inside a Salla theme (server- or client-side)
async function trackEvent(event, metadata) {
const ts = Math.floor(Date.now() / 1000);
const body = JSON.stringify({
event,
store_id: salla.config.store.id,
session_id: getSessionId(),
user_id: salla.user?.id,
metadata,
});
const signature = await sign(ts + ':' + body, AFTERADS_TRACKING_KEY);
await fetch('https://afterads.com/api/track', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Signature': signature,
'X-Timestamp': String(ts),
},
body,
});
}
trackEvent('add_to_cart', {
product_id: '1828908128',
quantity: 1,
price: 350,
});#Response
| — | "Code" | "Body" |
|---|---|---|
| 200 | { "ok": true, "event_id": "uuid" } | |
| 401 | { "error": "invalid_signature" } | |
| 401 | { "error": "stale_timestamp" } | |
| 409 | { "error": "replay" } (nonce reused) | |
| 400 | { "error": "invalideventtype" } | |
| 429 | { "error": "ratelimited", "retryafter_seconds": 60 } |
#Anti-replay
Each request has a unique session_id + timestamp. The server stores them for 5 minutes and rejects duplicates — preventing replay attacks if someone sniffs the network.