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

Text
POST https://sporetag.com/api/v1/events
Authorization: Bearer st_sk_...
Content-Type: application/json

Each 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.

FieldTypeNotes
namestringRequired. The event name, such as Purchase. Up to 120 characters
urlstringThe page the event belongs to. Must be on the site's domain
timestampnumber or stringWhen it happened: Unix seconds, milliseconds, or ISO 8601. Up to 7 days back. Default: now
event_idstringYour unique id. The same id twice is counted once (kept 30 days)
valuenumberOrder value, such as 49.00
currencystringThree letters, such as USD. Required with value
order_idstringYour order number
emailstringHashed with SHA-256 on arrival if you send it plain
phonestringSame as email. Use the full number with country code
ipstringThe visitor's IP address, if you have it
user_agentstringThe visitor's browser user agent, if you have it
visitorobjectOr send ip, ua and id together here. id is the visitor's first-party id
click_idsobjectAny of fbclid, gclid, gbraid, wbraid, fbc, fbp
propsobjectExtra details, with string or number values, up to 20 keys
kindstringFor customer profiles: order, refund, email_opened, sms_clicked, call and more. See Bring your data together
sourcestringThe tool the event came from, such as klaviyo. Lowercase
external_idsobjectIds in other tools, such as {"stripe_customer": "cus_123"}
payloadobjectShort 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-4567 becomes 15551234567.

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

  • ip and user_agent from 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_id on your domain. Your server gets it with every request. Send it as visitor.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

JavaScript
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

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

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

Shell
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.

JavaScript
// 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:

JavaScript
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

StatusMeans
202Accepted. The body has event_id and the names of the goals it reached. A repeated event_id answers 202 with "duplicate": true
400A field is not valid. field names it and error says why
401The key is missing, wrong or revoked
403The site's plan does not include the Events API, or a kind was sent while the site is in Private mode
413The body is over 16,000 bytes
415The body is not sent as application/json
429More 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.