OptimeleonOptimeleon
Request Access
Setup · Advanced

Server-side conversions

Send conversions from your backend when the browser cannot: an order a webhook confirms, a payment that clears later, a deal closed in your CRM. Create a key, read the visitor's two cookies, send one request.

Who this is for: the developer who sends conversions from a backend, handed this page by a marketer. A conversion that your own system confirms, like an order a webhook confirms, a payment that clears later, or a signed contract, is sent with one small request per conversion. Optimeleon links it to the version of the page the visitor saw.

If the conversion happens on a page, you do not need this. Paste the event's tracking code on that page instead (Conversion events, then the event's code; or "More options" for a page without the Optimeleon tag).

The three steps

  1. Create a key under Settings, then Server-side tracking. It is shown once, when you create it. Put it in your server's configuration as an environment variable, for example OPTIMELEON_INGEST_KEY. Two keys can be active at a time, so you can switch to a new one without a gap.
  2. Read the visitor's two cookies on your server, __opti_vid and __opti_arm, and pass them on unchanged. That is what links the conversion to the page version the visitor saw.
  3. Send one request per confirmed conversion to the address shown on that same page, with the key in the Authorization header. A 202 answer means it arrived.

Every conversion event has a ready-made request, with its exact URL and event name, and step-by-step code for your server stack. To reach it: open Conversion events, click Instructions on the event, then More options, and look under Send from your server.

1. The key

  • Create it under Settings, then Server-side tracking. Copy it right away: it is shown once and never again. If it is gone, create a new one and revoke the old one.
  • A key looks like opti_sk_ followed by 40 characters. Keep it in an environment variable, never in code or a repository, and never in a screenshot.
  • Only an active key sends. A revoked key stops within seconds.
  • Rotating without downtime: create a second key, deploy your server with it, then revoke the old one once its "last used" stops moving.

2. The visitor's two cookies

Optimeleon's tag sets two cookies in the visitor's browser once they accept analytics:

Cookie What it is Why it matters
__opti_vid The visitor's Optimeleon identity. This is the one that links the conversion. Always send it.
__opti_arm Which version of the page the visitor was shown. Links a conversion that happens in the same visit.

Send both when you have them. If you can only send one, send __opti_vid.

Conversions are credited to the visitor who saw the page, for a period your goal defines (7 days by default), and __opti_vid is what identifies them. __opti_arm covers the same visit only: it expires 30 minutes after the visitor's last activity, so on its own it misses anyone who comes back later to buy. That is a large share of purchases on most sites.

Your server receives both on every request that visitor makes to your site. Read them and put the values in the request exactly as they are. Do not decode them twice and do not re-encode them. Some frameworks hand you the raw cookie header, others decode it for you; either form works.

Without __opti_vid the conversion still counts in your reports: we never drop a real conversion. It is just not linked to the page version the visitor saw, so it cannot tell Optimeleon which version wins. A visitor who has not accepted analytics has no cookies at all, and those conversions are unlinkable by design.

Reading them in your stack

// Node.js, Express with cookie-parser
const vid = req.cookies['__opti_vid'];
const arm = req.cookies['__opti_arm'];
// Next.js, App Router (a Route Handler or a Server Action)
import { cookies } from 'next/headers';

const store = await cookies();
const vid = store.get('__opti_vid')?.value;
const arm = store.get('__opti_arm')?.value;
// PHP (WordPress included)
$vid = $_COOKIE['__opti_vid'] ?? null;
$arm = $_COOKIE['__opti_arm'] ?? null;
// Laravel: add both names to the cookie-encryption exceptions first, or they read as null.
// bootstrap/app.php (Laravel 11+): ->withMiddleware(fn ($m) => $m->encryptCookies(except: ['__opti_vid', '__opti_arm']))
$vid = $request->cookie('__opti_vid');
$arm = $request->cookie('__opti_arm');
# Django
vid = request.COOKIES.get("__opti_vid")
arm = request.COOKIES.get("__opti_arm")

# Flask
vid = request.cookies.get("__opti_vid")
arm = request.cookies.get("__opti_arm")
# Ruby on Rails
vid = cookies['__opti_vid']
arm = cookies['__opti_arm']
// Go, net/http
vid, _ := r.Cookie("__opti_vid") // vid.Value
arm, _ := r.Cookie("__opti_arm") // arm.Value

Website and server on different domains

The one-liners above assume the server that handles the visitor's page requests is the one you send from. That is true for WordPress, Rails, Laravel, Django and a Next.js app on one domain. If your website is a static site or a single-page app (Webflow, Framer, a Jamstack build, a React app) and your backend is a separate API, it depends on where the API lives.

The API is on a subdomain of your site, for example api.yourshop.com next to www.yourshop.com. The two cookies are set for your whole root domain, so the browser sends them to the API as well, as long as your own API calls include credentials and the API allows it:

// In the browser: your own API call, with cookies attached.
fetch('https://api.yourshop.com/orders', {
  method: 'POST',
  credentials: 'include',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(order),
});
# On the API: answer the CORS preflight for your site with
Access-Control-Allow-Origin: https://www.yourshop.com
Access-Control-Allow-Credentials: true

The API then reads the two cookies exactly as in the one-liners above.

The API is on a different domain, or you would rather not touch CORS. Read the two cookies in the browser and send the values along in your own API call, then take them from the body:

// In the browser: read both cookies and include them in your own API call.
const read = (name) => document.cookie.match('(?:^|; )' + name + '=([^;]*)')?.[1] ?? '';

fetch('https://api.example.net/orders', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    ...order,
    optimeleon: { vid: read('__opti_vid'), arm: read('__opti_arm') },
  }),
});
// On the API: take the two values from the body instead of the cookie header, unchanged.
const { vid, arm } = req.body.optimeleon ?? {};

Both values are plain strings. Pass them on as they are, without decoding or re-encoding.

Confirmed later, without the visitor's request

A payment webhook or a background job runs without the visitor's browser, so there are no cookies to read at that moment. The same is true when the payment happens on another company's checkout page (a hosted checkout on a different domain): that page cannot read your site's cookies, but your server saw them when the visitor started the checkout on your site. In both cases, save the two values with the order when it is placed or the checkout starts, in the request that still carries the cookies, and read them back when the confirmation arrives. The WooCommerce guide below shows this pattern end to end.

3. The request

Where to send it

Every request goes to the same address, which never changes:

https://edge.optimeleon.com/t/<EDGE_TOKEN>

The last part names your project. Your own address, with it filled in, is on the Server-side tracking page under Settings, under "Where your server sends conversions". Copy it from there. Read inside Optimeleon, the addresses on this page are already yours.

The address is not a secret: the same token is in the Optimeleon code on your website, and it only says which project a conversion belongs to. The key in the Authorization header is the secret part, and it is what decides whether a request is accepted.

The request itself

One POST per confirmed conversion:

curl -X POST "https://edge.optimeleon.com/t/<EDGE_TOKEN>" \
  -H "Authorization: Bearer $OPTIMELEON_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "schema_version": 1,
    "event": "purchase_completed",
    "value": 49.99,
    "currency": "EUR",
    "eventId": "0192f9c4-7b5a-7c31-9c2e-2b8f4a1d6e00",
    "properties": { "plan": "pro" },
    "visitor": {
      "vid": "<the __opti_vid cookie value>",
      "arm": "<the __opti_arm cookie value>"
    }
  }'

The exact event name is in the ready-made request: Conversion events, then the event's Instructions, then More options, then Send from your server. That request also repeats the address, with your token filled in, and comes with code for your stack. Copy both from there rather than typing them.

Body fields

Field Type Required Meaning
schema_version 1 yes Always 1.
event string yes The event name, exactly as in the ready-made request.
value number no The conversion's amount, in currency. Leave it out for a conversion with no amount.
currency string no The ISO 4217 code for value, for example EUR.
eventId UUID no Your own id for this conversion. Send the same one on a retry and it counts once. Leave it out and Optimeleon makes one per request, so a retry would count twice.
properties object no Extra details. Values must be strings, numbers or booleans, not nested objects or lists.
visitor.vid string no The __opti_vid cookie value, unchanged.
visitor.arm string no The __opti_arm cookie value, unchanged.

Answers

Status Meaning
202 Accepted. The conversion is on its way.
400 The body is not valid JSON, or a field has the wrong shape.
401 The key is missing, revoked or wrong.
404 The address is unknown, usually a mistyped token. Copy it from the Server-side tracking page.
413 The body is too large, usually an oversized value in properties.
502 Optimeleon could not take the conversion in right now. Retry later with the same eventId.

In your language

// Node.js (18+), any framework
const response = await fetch('https://edge.optimeleon.com/t/<EDGE_TOKEN>', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.OPTIMELEON_INGEST_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    schema_version: 1,
    event: 'purchase_completed',
    value: 49.99,
    currency: 'EUR',
    eventId: order.optimeleonEventId, // a UUID you minted once and stored with the order
    visitor: { vid, arm },
  }),
});
// response.status === 202 means it arrived
// PHP, with curl
$payload = json_encode([
    'schema_version' => 1,
    'event'          => 'purchase_completed',
    'value'          => 49.99,
    'currency'       => 'EUR',
    'visitor'        => ['vid' => $vid, 'arm' => $arm],
]);
$ch = curl_init('https://edge.optimeleon.com/t/<EDGE_TOKEN>');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('OPTIMELEON_INGEST_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 5,
]);
curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); // 202 means it arrived
# Python, with requests
import os, requests

response = requests.post(
    "https://edge.optimeleon.com/t/<EDGE_TOKEN>",
    headers={"Authorization": f"Bearer {os.environ['OPTIMELEON_INGEST_KEY']}"},
    json={
        "schema_version": 1,
        "event": "purchase_completed",
        "value": 49.99,
        "currency": "EUR",
        "visitor": {"vid": vid, "arm": arm},
    },
    timeout=5,
)
# response.status_code == 202 means it arrived

Guides

WordPress and WooCommerce

The site itself is the server, so the cookies are there while the customer checks out. Save them with the order when it is placed, and send the conversion when payment completes, which may happen later from the payment provider's callback.

// functions.php of a child theme, or a small plugin.
// Define the key once in wp-config.php: define('OPTIMELEON_INGEST_KEY', 'opti_sk_...');

// 1. When the order is placed (runs in the customer's own request): keep the two cookie values.
add_action('woocommerce_checkout_order_processed', function ($order_id) {
    $order = wc_get_order($order_id);
    $order->update_meta_data('_opti_vid', $_COOKIE['__opti_vid'] ?? '');
    $order->update_meta_data('_opti_arm', $_COOKIE['__opti_arm'] ?? '');
    $order->save();
});

// 2. When payment completes (may run from the payment provider's callback): send the conversion.
add_action('woocommerce_payment_complete', function ($order_id) {
    $order = wc_get_order($order_id);
    wp_remote_post('https://edge.optimeleon.com/t/<EDGE_TOKEN>', [
        'timeout' => 5,
        'headers' => [
            'Authorization' => 'Bearer ' . OPTIMELEON_INGEST_KEY,
            'Content-Type'  => 'application/json',
        ],
        'body' => wp_json_encode([
            'schema_version' => 1,
            'event'          => 'purchase_completed',
            'value'          => (float) $order->get_total(),
            'currency'       => $order->get_currency(),
            'visitor'        => [
                'vid' => $order->get_meta('_opti_vid'),
                'arm' => $order->get_meta('_opti_arm'),
            ],
        ]),
    ]);
});

Replace the URL and the event name with the ones from the ready-made request. If a purchase is all you need, the ordinary tracking code on the order-received page is simpler: use this only for a conversion the page never sees.

Shopify

Shopify runs your checkout and sends order webhooks from its own servers, where the visitor's cookies are not available. You do not need server-side tracking for purchases, checkouts or adds to cart: the Optimeleon pixel already sends them, so pick them under Conversion events, Shopify events. Use server-side tracking only for a conversion that happens outside Shopify, for example a subscription renewal in another system or an offline sale.

To link a webhook-driven conversion to the page version the visitor saw, your storefront has to store the two cookie values on the cart before checkout. They then arrive in the order webhook as note attributes, and your app or a Shopify Flow HTTP action forwards them in the request. Without them the conversion is counted, not linked.

// On your storefront (theme code), once the visitor has the cookies: store them on the cart.
const read = (name) => document.cookie.match('(?:^|; )' + name + '=([^;]*)')?.[1] ?? '';
fetch('/cart/update.js', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    attributes: { __opti_vid: read('__opti_vid'), __opti_arm: read('__opti_arm') },
  }),
});

Zapier and Make

No code. Add an HTTP action to your automation ("Webhooks by Zapier" with a POST, or Make's HTTP module) with the URL from the ready-made request, the two headers (Authorization: Bearer followed by your key, and Content-Type: application/json) and the JSON body from section 3.

The two cookie values can only be passed on if your trigger carries them, for example as a custom field on the order or lead the automation starts from. Without them the conversion is counted, not linked. Never paste the key into a shared screenshot or a public template.

Any other backend

Go, Java, .NET, Rust: the request is the same. Read the two cookies from the incoming request, save them with the order if the confirmation comes later, and send the JSON body above with your HTTP client of choice. The step-by-step kit under "More options" writes it for your stack.

Checking it works

  1. Send one request. A 202 answer means Optimeleon accepted it.
  2. Open Conversion events. The event's status turns active when the first conversion lands, usually within a minute.
  3. Send a conversion with the cookies of a visitor who saw a campaign page, and it shows up against that campaign.

Limits while in beta

  • There is no client library yet; this is a plain HTTP request.
  • There is no rate limit yet. Send one request per conversion, not one per page view.
  • A checkout page on another company's domain (a hosted checkout) cannot read your site's cookies, so it cannot send an attributed conversion by itself. Your server can: save the two values when the visitor starts the checkout on your site, and send the conversion when the payment confirmation arrives (see "Confirmed later" above).