Measure
Events API
Send signups and sales from your server to Sporetag with POST /api/v1/events and a per-site secret key. Examples in Node, Python, PHP and curl.
The Events API records an event from your server. Use it for things the browser never sees, or cannot be trusted with: a paid order, a webhook from Stripe or Shopify, a signup confirmed by email.
Server events count toward goals and the attribution report like browser events. If a goal is set to send to Meta, server events go there too. See Meta Conversions API.
The Events API is on paid plans: Solo, Studio, Agency and Custom. Free does not include it.
Endpoint
POST https://sporetag.com/api/v1/events
Authorization: Bearer st_sk_...
Content-Type: application/jsonEach site has its own secret keys. Make one in the site's Integrations tab, under Events API. The key is shown once, when you make it. Sporetag stores only a hash of it.
Keep the key on your server. Never put it in a page, an app or a public repo. The endpoint sends no CORS headers, so a browser page cannot call it. If a key leaks, press Rotate: you get a new key and the old one stops working at once. Revoke stops a key without a new one.
Fields
Send one event per request, as a JSON object.
| Field | Type | Notes |
|---|---|---|
name | string | Required. The event name, such as Purchase. Up to 120 characters |
url | string | The page the event belongs to. Must be on the site's domain |
timestamp | number or string | When it happened: Unix seconds, milliseconds, or ISO 8601. Up to 7 days back. Default: now |
event_id | string | Your unique id. The same id twice is counted once (kept 30 days) |
value | number | Order value, such as 49.00 |
currency | string | Three letters, such as USD. Required with value |
order_id | string | Your order number |
email | string | Hashed with SHA-256 on arrival if you send it plain |
phone | string | Same as email. Use the full number with country code |
ip | string | The visitor's IP address, if you have it |
user_agent | string | The visitor's browser user agent, if you have it |
visitor | object | Or send ip, ua and id together here. id is the visitor's first-party id |
click_ids | object | Any of fbclid, gclid, gbraid, wbraid, fbc, fbp |
props | object | Extra details, with string or number values, up to 20 keys |
kind | string | For customer profiles: order, refund, email_opened, sms_clicked, call and more. See Bring your data together |
source | string | The tool the event came from, such as klaviyo. Lowercase |
external_ids | object | Ids in other tools, such as {"stripe_customer": "cus_123"} |
payload | object | Short details for a customer item, up to 12 values |
Ids (event_id, order_id, visitor.id) use up to 100 letters, digits and . _ - : ~ @.
Email and phone
Sporetag normalizes them the way Meta asks, then hashes them with SHA-256:
- Email: spaces trimmed, then lowercase.
- Phone: digits only, leading zeros dropped.
+1 (555) 123-4567becomes15551234567.
A value that is already a SHA-256 hash (64 hex characters) is used as it is. The raw phone is never stored, and neither the raw email nor the hash is stored with your stats. The hash goes to Meta when a goal sends to Meta.
Customer profiles
When the site is in Attribution mode, email, phone, visitor.id and external_ids also find or make the customer profile. The hashes are kept on the profile, and the email too (encrypted) while Show customer emails is on. A value adds an order to the customer. The answer then has customer_id.
With a kind other than event or order, the request adds an item to the customer's stream only. It does not count as an event. See Bring your data together.
Tying the event to a visit
ipanduser_agentfrom the visitor's own browser give the same visitor hash as their visit that day. The event then joins that visit's source, campaign and short link.- In Attribution mode, the script keeps a first-party id in the cookie
_st_idon your domain. Your server gets it with every request. Send it asvisitor.id, and the event joins visits from up to 90 days back. - Click ids tie the event straight to an ad click.
Without any of these the event still counts, but it cannot be tied to a source.
Events count at the time Sporetag receives them. timestamp is what goes to Meta as the event time.
Node
await fetch('https://sporetag.com/api/v1/events', {
method: 'POST',
headers: {
authorization: `Bearer ${process.env.SPORETAG_KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({
name: 'Purchase',
url: 'https://shop.example.com/thanks',
event_id: `order-${order.id}`,
value: order.total,
currency: 'USD',
email: order.email,
ip: req.ip,
user_agent: req.get('user-agent'),
visitor: { id: req.cookies._st_id },
}),
});Python
import os, requests
requests.post(
"https://sporetag.com/api/v1/events",
headers={"Authorization": f"Bearer {os.environ['SPORETAG_KEY']}"},
json={
"name": "Purchase",
"url": "https://shop.example.com/thanks",
"event_id": f"order-{order.id}",
"value": order.total,
"currency": "USD",
"email": order.email,
},
timeout=5,
)PHP
$ch = curl_init('https://sporetag.com/api/v1/events');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SPORETAG_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'name' => 'Purchase',
'url' => 'https://shop.example.com/thanks',
'event_id' => 'order-' . $order->id,
'value' => $order->total,
'currency' => 'USD',
'email' => $order->email,
'visitor' => ['id' => $_COOKIE['_st_id'] ?? null],
]),
]);
curl_exec($ch);
curl_close($ch);curl
curl https://sporetag.com/api/v1/events \
-H "Authorization: Bearer $SPORETAG_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Purchase","event_id":"order-1042","value":49,"currency":"USD"}'From a Stripe webhook
When Stripe tells your server that a Checkout Session is complete, send a Purchase with the session id as the event_id. A retried webhook then never counts twice.
// Inside your checkout.session.completed handler, after checking the signature.
const s = event.data.object;
await fetch('https://sporetag.com/api/v1/events', {
method: 'POST',
headers: { authorization: `Bearer ${process.env.SPORETAG_KEY}`, 'content-type': 'application/json' },
body: JSON.stringify({
name: 'Purchase',
event_id: s.id,
timestamp: event.created,
value: s.amount_total / 100,
currency: s.currency.toUpperCase(),
order_id: s.client_reference_id ?? undefined,
email: s.customer_details?.email,
visitor: s.metadata?.st_id ? { id: s.metadata.st_id } : undefined,
}),
});To tie the sale to the visit, copy the _st_id cookie into the Checkout Session's metadata.st_id when you create it.
From a Shopify webhook
Subscribe to orders/paid in Shopify. In your handler, after checking the X-Shopify-Hmac-Sha256 header:
const o = JSON.parse(rawBody);
await fetch('https://sporetag.com/api/v1/events', {
method: 'POST',
headers: { authorization: `Bearer ${process.env.SPORETAG_KEY}`, 'content-type': 'application/json' },
body: JSON.stringify({
name: 'Purchase',
event_id: `shopify-${o.id}`,
timestamp: o.processed_at ?? o.created_at,
value: Number(o.total_price),
currency: o.currency,
order_id: String(o.order_number),
email: o.email,
phone: o.phone ?? o.customer?.phone ?? undefined,
ip: o.browser_ip ?? undefined,
user_agent: o.client_details?.user_agent ?? undefined,
}),
});You can skip this code: the Stripe connector and the Shopify connector read sales for you. If you keep both, send the same event_id (the Checkout Session id, or shopify- and the order id) and each sale is counted and sent to Meta once.
Answers
| Status | Means |
|---|---|
202 | Accepted. The body has event_id and the names of the goals it reached. A repeated event_id answers 202 with "duplicate": true |
400 | A field is not valid. field names it and error says why |
401 | The key is missing, wrong or revoked |
403 | The site's plan does not include the Events API, or a kind was sent while the site is in Private mode |
413 | The body is over 16,000 bytes |
415 | The body is not sent as application/json |
429 | More than 300 events a minute on one key. Wait a minute and try again |
Send events in the background or after your response, so a slow network never holds up your checkout.
Something missing or wrong? Email hello@sporetag.com.