Signed Server API
Send purchases and order-status changes from SellSull, WooCommerce, Shopify or any custom backend. PixelMili verifies every request, stores it durably, then processes it on reserved commerce queues. Browser events continue through the normal tracking script.
1. Create credentials
- Dashboard → Domains → Install → Server API.
- Generate a server secret and copy it immediately; PixelMili only shows it once.
- Save the Tracking ID and secret in your platform's encrypted server settings. Never expose the secret in HTML or JavaScript.
Keep Meta/TikTok pixel IDs and access tokens in PixelMili's Integrations page. A SellSull vendor only enters the PixelMili Tracking ID and server secret in SellSull—platform access tokens are never copied there.
2. Endpoint and signature
POST https://track.pixelmili.com/v1/connectors/events
Content-Type: application/json
X-PixelMili-Timestamp: CURRENT_UNIX_TIMESTAMP
X-PixelMili-Signature: sha256=HEX_HMAC
The signature is HMAC-SHA256 over these exact bytes:
{unix_timestamp}\n{connector}\n{raw_json_request_body}
Valid connectors are sellsull, woocommerce, shopify and custom.
The timestamp must be within five minutes of PixelMili's clock. Sign the exact JSON string you send—do not encode it a second time.
3. Event contract
| Field | What to send |
|---|---|
tracking_id required | The domain Tracking ID, for example pm_abc123xyz. |
connector required | The same connector name included in the signature. |
event_name required | Purchase, OrderConfirmed, OrderDelivered, OrderCancelled, OrderReturned, OrderFailed or Refund. A store whose server also sees the shopper's cart actions may send AddToCart and InitiateCheckout too — use the same event_id as the tracker script's own event (pm('track', 'AddToCart', data, identity, { event_id: '…' })): PixelMili forwards whichever of the two arrives first and drops the other, so a blocked script loses nothing and an unblocked one is not counted twice. |
event_id required | A stable, unique ID for this event, e.g. sellsull:purchase:1001. Every retry must reuse it. |
event_time | Unix seconds or milliseconds. Defaults to the receipt time. |
client_ip, user_agent | The buyer's checkout IP and browser—not your server's—for better ad matching. |
client_id, fbp, fbc, ga | Visitor identifiers captured by the browser script. |
identity | Customer phone, email, first_name, last_name, city, state, zip, country. PixelMili normalises and hashes these for each destination. |
custom | order_id, value, currency, contents, content_ids, num_items, cod and captured click IDs. |
{
"tracking_id": "pm_YOUR_TRACKING_ID",
"connector": "sellsull",
"event_name": "Purchase",
"event_id": "sellsull:purchase:1001",
"event_time": 1792814400,
"client_ip": "203.0.113.10",
"user_agent": "Mozilla/5.0 ...",
"client_id": "visitor-id-from-pm_cid",
"fbp": "fb.1.1700000000.123456789",
"identity": {
"phone": "01712345678",
"email": "buyer@example.com",
"first_name": "Rahim",
"city": "Dhaka",
"country": "BD"
},
"custom": {
"order_id": "1001",
"value": 1490,
"currency": "BDT",
"cod": true,
"contents": [{"id": "SKU-1", "quantity": 1, "item_price": 1490}]
}
}
4. Delivery and retry rules
202withaccepted: truemeans PixelMili durably owns the event. Stop retrying.queued: falseon that accepted response is still success: the event is safe in PixelMili's SQL inbox and will be queued automatically when worker capacity returns.duplicate: truemeans the sameevent_idwas already accepted; this is a successful idempotent retry.409means thatevent_idwas reused with different JSON. Stop retrying and fix the sender's event-ID generation.- Retry
429and5xxwith exponential backoff and jitter, always using the sameevent_id. - Do not retry
401or422forever; fix the secret/signature or payload and alert the merchant. - Keep failed events—and the exact serialized JSON body—in your own durable queue/outbox until PixelMili returns an accepted
202.
PHP example
$payload = [
'tracking_id' => getenv('PIXELMILI_TRACKING_ID'),
'connector' => 'sellsull',
'event_name' => 'Purchase',
'event_id' => 'sellsull:purchase:' . $order->id,
'client_ip' => $checkoutIp,
'user_agent' => $checkoutUserAgent,
'client_id' => $visitorCookies['pm_cid'] ?? null,
'fbp' => $visitorCookies['_fbp'] ?? null,
'fbc' => $visitorCookies['_fbc'] ?? null,
'identity' => ['phone' => $order->phone, 'email' => $order->email, 'country' => 'BD'],
'custom' => ['order_id' => (string) $order->id, 'value' => (float) $order->total, 'currency' => 'BDT', 'cod' => $order->is_cod],
];
$body = json_encode($payload, JSON_UNESCAPED_SLASHES);
$timestamp = (string) time();
$signature = hash_hmac('sha256', $timestamp . "\n" . $payload['connector'] . "\n" . $body, getenv('PIXELMILI_SERVER_SECRET'));
$ch = curl_init('https://track.pixelmili.com/v1/connectors/events');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-PixelMili-Timestamp: ' . $timestamp,
'X-PixelMili-Signature: sha256=' . $signature,
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
]);
$responseBody = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
// Mark the local outbox row delivered only when $status === 202 and accepted === true.
Node.js example
import crypto from 'node:crypto';
async function sendPixelMili(payload) {
const body = JSON.stringify(payload);
const timestamp = Math.floor(Date.now() / 1000).toString();
const canonical = `${timestamp}\n${payload.connector}\n${body}`;
const signature = crypto.createHmac('sha256', process.env.PIXELMILI_SERVER_SECRET)
.update(canonical).digest('hex');
const response = await fetch('https://track.pixelmili.com/v1/connectors/events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-PixelMili-Timestamp': timestamp,
'X-PixelMili-Signature': `sha256=${signature}`,
},
body,
signal: AbortSignal.timeout(8000),
});
const result = await response.json().catch(() => ({}));
if (response.status !== 202 || result.accepted !== true) {
throw new Error(`PixelMili did not accept event (${response.status})`);
}
return result;
}
Purchase ownership and COD
Exactly one system should own the initial Purchase. If your platform sends it server-side, add
data-server-events="Purchase" to the PixelMili script so the thank-you page does not send a second browser Purchase.
Send order-status events with the same custom.order_id, but give each status event its own stable event_id.
<script async src="https://track.pixelmili.com/t.js"
data-tracking-id="pm_YOUR_TRACKING_ID"
data-server-events="Purchase"></script>
PixelMili's COD setting decides whether a COD Purchase is sent when placed, held until confirmation/delivery, or sent at both stages. The commerce platform should report facts—placed, confirmed, delivered, cancelled or returned—and must not independently invent a second Purchase.
Go-live checklist
- Secret stored only in encrypted server-side configuration.
- Stable event IDs persisted before the first send.
- Local durable retries for timeouts,
429and5xx. - Buyer cookies, IP and user-agent captured at checkout and reused for later COD release.
- Same
custom.order_idacross the purchase lifecycle. - Clock synchronised with NTP so signed requests stay inside the five-minute window.