Skip to main content

Recipe: End-to-end conversion tracking

The full Neuroon funnel has three key events:

  1. track:click — the user interacts with a result in the widget. Emitted automatically by the widget, no code from you.
  2. track:add-to-cart — optional, derived from the cart bridge.
  3. 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:

BlockTypical impact
Adblockers (uBlock, Ghostery, Brave Shields)25-40 % in European markets.
Safari ITP + iOS Private Relayresidual impact on cookies.
Strict CSPs (banking, government)block any third-party pixel.
Offline service workersdo 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

POST/api/plugin/shops/{shopId}/track/conversion
POST /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 }
]
}

searchLogId is required (@NotBlank on ConversionItem, ShopRequestDTO.java:92). The widget emits it on every track:click and stores it in a cookie in the buyer's browser. Your server must read that cookie at order confirmation and map each productId to its searchLogId. Without searchLogId the API responds 400. The official WordPress plugin does this in wordpress-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

track-conversion.js
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

SymptomCauseFix
400 with orderValue must be >= 0Negative orderValue (refunds)Use the refunds tracking endpoint.
Conversion does not appear in dashboardorigin does not match or shopId from a crossed environmentVerify Origin and that the API Key is for the right environment.
Duplicate conversionsResends without a stable idempotency keyAlways use the same orderId per order (no timestamps).

Next steps