Server-Side Events
The server-side endpoint allows your backend services to submit events directly to Datafly Signal without going through the browser. This is the recommended approach for events that originate on the server, such as order completions, subscription changes, or backend-computed conversions.
Endpoint
POST /v1/eventsAuthentication
Server-side requests are authenticated using HMAC-SHA256 signatures. Every request must include two headers:
| Header | Value | Description |
|---|---|---|
X-Signature | sha256={hmac_hex} | HMAC-SHA256 signature of the request |
X-Timestamp | {unix_ms} | Current time in Unix milliseconds |
HMAC Computation
The signature is computed as:
HMAC-SHA256(secret_key, timestamp + "." + request_body)Where:
secret_key— your source’s secret key (available in the Management UI)timestamp— the value of theX-Timestampheader (Unix milliseconds as a string)request_body— the raw JSON request body
Signal rejects requests where the timestamp is more than 5 minutes from the server’s current time, in either direction, and answers 403. The timestamp is inside the signed string, so a captured request cannot be replayed once it ages out of the window. Make sure your server’s clock is synchronised via NTP.
Deprecated: an older scheme sent X-Datafly-Signature: {hmac_hex} computed over the request body alone, with no timestamp. It is still accepted so existing integrations keep working, but it carries no replay protection. Use X-Signature with X-Timestamp for new integrations. If you send both, the timestamped signature is the one that is enforced.
Request Format
Events are sent as an array under events, so one request can carry a batch. Field names are snake_case.
{
"events": [
{
"type": "track",
"event": "order_completed",
"user_id": "user_12345",
"properties": {
"order_id": "ORD-98765",
"revenue": 109.97,
"currency": "GBP",
"products": [
{ "product_id": "SKU-1234", "name": "Wireless Headphones", "price": 79.99, "quantity": 1 },
{ "product_id": "SKU-5678", "name": "USB-C Cable", "price": 14.99, "quantity": 2 }
]
},
"context": {
"ip": "203.0.113.42",
"user_agent": "Mozilla/5.0 ..."
},
"timestamp": "2026-02-25T14:30:00.000Z"
}
]
}Fields
Each element of events accepts:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | track, page, identify, or group |
event | string | Track only | Event name |
user_id | string | Recommended | Known user ID |
anonymous_id | string | No | Anonymous ID (use if user_id is not available) |
properties | object | No | Event properties |
context | object | No | Contextual data (IP, user agent, etc.) |
context.ip | string | Recommended | Client IP address for geolocation enrichment |
context.user_agent | string | Recommended | Client user agent for device parsing |
timestamp | string | No | ISO 8601 timestamp; defaults to server receipt time |
message_id | string | No | Your idempotency key for the event |
event_id | string | No | Customer-supplied identifier used for cross-platform vendor deduplication |
You should provide at least one of user_id or anonymous_id. If both are present, Signal will link them together.
Response
{
"success": true
}Status codes: 200 on success, 400 for invalid payload, 401 for an invalid or missing signature, 403 for an expired timestamp, 429 if rate-limited.
Examples
Each example signs {timestamp}.{body} and sends the exact bytes it signed. Serialise the body once and reuse that string — re-serialising can reorder keys and invalidate the signature.
curl
SECRET="your_pipeline_secret"
BODY='{"events":[{"type":"track","event":"order_completed","user_id":"user_12345","properties":{"order_id":"ORD-98765","revenue":109.97,"currency":"GBP"}}]}'
TIMESTAMP=$(date +%s000)
SIGNATURE=$(printf '%s.%s' "$TIMESTAMP" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://collect.example.com/v1/events \
-H "Content-Type: application/json" \
-H "X-Pipeline-Key: your_pipeline_key" \
-H "X-Signature: sha256=$SIGNATURE" \
-H "X-Timestamp: $TIMESTAMP" \
-d "$BODY"Node.js
import crypto from 'node:crypto'
const SECRET = process.env.DATAFLY_PIPELINE_SECRET
const ENDPOINT = 'https://collect.example.com/v1/events'
async function send(events) {
// Serialise once and sign exactly what is sent.
const body = JSON.stringify({ events })
const timestamp = Date.now().toString()
const signature = crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${body}`)
.digest('hex')
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Pipeline-Key': process.env.DATAFLY_PIPELINE_KEY,
'X-Signature': `sha256=${signature}`,
'X-Timestamp': timestamp,
},
body,
})
if (!res.ok) {
throw new Error(`Datafly ingest failed: ${res.status} ${await res.text()}`)
}
}
await send([
{
type: 'track',
event: 'order_completed',
user_id: 'user_12345',
properties: { order_id: 'ORD-98765', revenue: 109.97, currency: 'GBP' },
},
])Python
import hashlib
import hmac
import json
import os
import time
import requests
SECRET = os.environ["DATAFLY_PIPELINE_SECRET"]
ENDPOINT = "https://collect.example.com/v1/events"
def send(events):
# Serialise once and sign exactly what is sent.
body = json.dumps({"events": events}, separators=(",", ":"))
timestamp = str(int(time.time() * 1000))
signature = hmac.new(
SECRET.encode(),
f"{timestamp}.{body}".encode(),
hashlib.sha256,
).hexdigest()
response = requests.post(
ENDPOINT,
data=body,
headers={
"Content-Type": "application/json",
"X-Pipeline-Key": os.environ["DATAFLY_PIPELINE_KEY"],
"X-Signature": f"sha256={signature}",
"X-Timestamp": timestamp,
},
timeout=10,
)
response.raise_for_status()
send([
{
"type": "track",
"event": "order_completed",
"user_id": "user_12345",
"properties": {"order_id": "ORD-98765", "revenue": 109.97, "currency": "GBP"},
}
])Go
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type Event struct {
Type string `json:"type"`
Event string `json:"event,omitempty"`
UserID string `json:"user_id,omitempty"`
Properties map[string]interface{} `json:"properties,omitempty"`
}
func send(events []Event) error {
// Marshal once and sign exactly what is sent.
body, err := json.Marshal(map[string][]Event{"events": events})
if err != nil {
return err
}
timestamp := fmt.Sprintf("%d", time.Now().UnixMilli())
mac := hmac.New(sha256.New, []byte(os.Getenv("DATAFLY_PIPELINE_SECRET")))
mac.Write([]byte(timestamp + "." + string(body)))
signature := hex.EncodeToString(mac.Sum(nil))
req, err := http.NewRequest("POST", "https://collect.example.com/v1/events", bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Pipeline-Key", os.Getenv("DATAFLY_PIPELINE_KEY"))
req.Header.Set("X-Signature", "sha256="+signature)
req.Header.Set("X-Timestamp", timestamp)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("datafly ingest failed: %s", resp.Status)
}
return nil
}Best Practices
- Always include
context.ipandcontext.userAgentwhen you have them. This enables geolocation enrichment and device parsing. - Use
user_idwhenever possible. Server-side events are most valuable when tied to a known user identity. - Set
timestampto the actual event time, not the time your server processes it. This ensures accurate attribution windows in downstream vendors. - Rotate secret keys periodically via the Management API. Signal supports key rotation with a grace period for the previous key.