Recipe: End-to-end conversion tracking
The full Neuroon funnel has three key events:
track:click— the user interacts with a result in the widget. Emitted automatically by the widget, no code from you.track:add-to-cart— optional, derived from the cart bridge.track:conversion— order paid / completed. You must call it server-side from your backend handler.
Why the conversion goes server-side
Relying solely on a JS pixel loses a significant share of conversions:
| Block | Typical impact |
|---|---|
| Adblockers (uBlock, Ghostery, Brave Shields) | 25-40 % in European markets. |
| Safari ITP + iOS Private Relay | residual impact on cookies. |
| Strict CSPs (banking, government) | block any third-party pixel. |
| Offline service workers | do not send beacons before the tab closes. |
Calling the endpoint server-to-server from the handler that confirms the order eliminates those four vectors.
The endpoint
/api/plugin/shops/{shopId}/track/conversionPOST /api/plugin/shops/shop_xxxxxxxx/track/conversion HTTP/1.1
Host: api.neuroon.ai
X-Shop-API-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Origin: https://your-shop.example
Content-Type: application/json
{
"orderId": "ORD-2026-0001",
"orderValue": 199.95,
"currency": "EUR",
"conversions": [
{ "productId": "SKU-001", "searchLogId": "log_aaa", "quantity": 2, "lineTotal": 79.98 },
{ "productId": "SKU-042", "searchLogId": "log_bbb", "quantity": 1, "lineTotal": 119.97 }
]
}
searchLogIdis required (@NotBlankonConversionItem,ShopRequestDTO.java:92). The widget emits it on everytrack:clickand stores it in a cookie in the buyer's browser. Your server must read that cookie at order confirmation and map eachproductIdto itssearchLogId. WithoutsearchLogIdthe API responds 400. The official WordPress plugin does this inwordpress-plugin/neuroon-search/includes/class-neuroon-conversion-tracking.php:335-374.
Idempotency: Neuroon deduplicates by orderId. Resending the same conversion does not inflate metrics.
Implementations
import fetch from 'node-fetch';
export async function trackConversion(order) {
const payload = {
orderId: String(order.id),
orderValue: Number(order.total),
currency: order.currency,
conversions: order.lines.map((l) => ({
productId: String(l.sku),
quantity: Number(l.quantity),
lineTotal: Number(l.unitPrice * l.quantity),
})),
};
try {
const r = await fetch(
`${process.env.NEUROON_API_URL}/api/plugin/shops/${process.env.NEUROON_SHOP_ID}/track/conversion`,
{
method: 'POST',
headers: {
'X-Shop-API-Key': process.env.NEUROON_API_KEY,
'Origin': process.env.NEUROON_ORIGIN,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
},
);
if (!r.ok) console.warn('Neuroon conversion tracking failed', r.status);
} catch (err) {
console.warn('Neuroon conversion tracking error', err);
}
}
// Usage from your order-confirmed handler
export async function onOrderConfirmed(order) {
// 1) mark the order as paid in your DB
// 2) track (fire-and-forget)
trackConversion(order); // no await: do not block the response to the customer
}Outbox for retries
If your integration cannot tolerate event loss (e.g. enterprise shop), persist the payload in an outbox table and retry from a job:
CREATE TABLE neuroon_conversion_outbox (
order_id VARCHAR(64) PRIMARY KEY,
payload JSON NOT NULL,
attempts INT NOT NULL DEFAULT 0,
last_error VARCHAR(1024),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
synced_at TIMESTAMP NULL
);
Job that runs every N minutes:
async function flushOutbox() {
const rows = await db.query(`
SELECT * FROM neuroon_conversion_outbox
WHERE synced_at IS NULL AND attempts < 5
ORDER BY created_at LIMIT 100`);
for (const row of rows) {
try {
await trackConversionRaw(row.payload);
await db.query(`UPDATE neuroon_conversion_outbox SET synced_at = NOW() WHERE order_id = $1`, [row.order_id]);
} catch (err) {
await db.query(`
UPDATE neuroon_conversion_outbox
SET attempts = attempts + 1, last_error = $1
WHERE order_id = $2`, [err.message, row.order_id]);
}
}
}
Validate
After processing a test order:
# The endpoint does not return the conversion on GET, but you can verify that
# the Neuroon dashboard reflects the new conversion within a few seconds.
curl -s "https://dev-api.neuroon.ai/api/plugin/shops/$NEUROON_SHOP_ID/products/SKU-001" \
-H "X-Shop-API-Key: $NEUROON_API_KEY"
Common errors
| Symptom | Cause | Fix |
|---|---|---|
400 with orderValue must be >= 0 | Negative orderValue (refunds) | Use the refunds tracking endpoint. |
| Conversion does not appear in dashboard | origin does not match or shopId from a crossed environment | Verify Origin and that the API Key is for the right environment. |
| Duplicate conversions | Resends without a stable idempotency key | Always use the same orderId per order (no timestamps). |
Next steps
plugins/dnn/conversion-tracking.plugins/wordpress/admin-dashboard— dashboard visualization.- API ·
/track/conversion— full reference (when published).