DocsMeasure

Events API

Send signups and sales from your server to Datacenter with POST /api/v1/events and a per-site secret key. Examples in Node, Python, PHP and curl.

On this page

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://datacenter.so/api/v1/events
Authorization: Bearer st_sk_...
Content-Type: application/json

Each site has its own secret keys. Make one on the site's Integrations page (under Settings), under Events API. The key is shown once, when you make it. Datacenter 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 and 10 minutes ahead. 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
gpcbooleantrue when the visitor's browser sent Global Privacy Control. Also accepted as visitor.gpc
click_idsobjectAny of gclid, gbraid, wbraid, fbclid, ttclid, msclkid, li_fat_id, ScCid, epik, twclid, rdt_cid, 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 . _ - : ~ @.

A refund is kind: "refund" with the amount refunded as value and the sale's order_id. When that sale went to Google Ads or Microsoft Ads, its value there drops by the refund. See Refunds.

Email and phone

Datacenter 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. Neither the plain email or phone nor their hashes are stored with your stats. The hash goes to Meta when a goal sends to Meta.

Customer profiles

When the site is in 90-day tracking, email, phone, visitor.id and external_ids also find or make the customer profile. The hashes are kept on the profile, and the email, name and phone too (encrypted) while Show contact details 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.

Global Privacy Control

A browser with Global Privacy Control on sends the header Sec-GPC: 1 with every request, to your server too. Pass it on as gpc: true. Datacenter treats it exactly as it treats the signal on a visit: the event still counts in your stats, but nothing about it goes to Meta or any other ad platform. In 90-day tracking, with visitor.id, the signal is kept for that visitor, so their later sales are not sent either.

JavaScript
gpc: req.get('sec-gpc') === '1',

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 90-day tracking, the script keeps a first-party id in the cookie _dc_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 Datacenter receives them. timestamp is what goes to Meta as the event time.

Sent from the page and from your server

Some sites send one signup twice: the page sends Signup (or the tag picks it up from another tag), and the server sends it again through the Events API. Datacenter counts it once.

Exact: share one id. Give both the same id, and they are one event with no guessing:

JavaScript
// On the page, once the signup succeeds
datacenter('Signup', { event_id: 'signup-' + user.id });
JavaScript
// On your server
{ name: 'Signup', event_id: `signup-${user.id}`, email: user.email, visitor: { id: req.cookies._dc_id } }

The same works for Purchase and any other event: one id, counted once and sent to each ad platform once.

Without a shared id, a page event and a server event are one when all of these are true:

  • They reach the same goal.
  • They are for the same person: the same visitor.id, the same visitor hash (the server passed the visitor's ip and user_agent), or the same customer (the server's email or external_ids, for a customer whose browser Datacenter already knows).
  • They are no more than 10 minutes apart.

Then the signup counts once in Goals and in the attribution report, and keeps what each side knew: the visit and its source from the page, the customer from the server. Only the first of the two goes to the ad platforms. When the server's event is the second, its answer has "paired": true. Two page events, or two server events, are never joined this way, and neither are two different people. Send visitor.id and the match never depends on the customer being known yet.

Node

JavaScript
await fetch('https://datacenter.so/api/v1/events', {
  method: 'POST',
  headers: {
    authorization: `Bearer ${process.env.DATACENTER_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._dc_id },
  }),
});

Python

Python
import os, requests

requests.post(
    "https://datacenter.so/api/v1/events",
    headers={"Authorization": f"Bearer {os.environ['DATACENTER_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://datacenter.so/api/v1/events');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 5,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('DATACENTER_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['_dc_id'] ?? null],
    ]),
]);
curl_exec($ch);
curl_close($ch);

curl

Shell
curl https://datacenter.so/api/v1/events \
  -H "Authorization: Bearer $DATACENTER_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://datacenter.so/api/v1/events', {
  method: 'POST',
  headers: { authorization: `Bearer ${process.env.DATACENTER_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?.dc_id ? { id: s.metadata.dc_id } : undefined,
  }),
});

To tie the sale to the visit, copy the _dc_id cookie into the Checkout Session's metadata.dc_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://datacenter.so/api/v1/events', {
  method: 'POST',
  headers: { authorization: `Bearer ${process.env.DATACENTER_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. A copy of an event the page already sent answers with "paired": true, and is not sent to ad platforms again
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@datacenter.so.