IngestionServer-Side Events

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/events

Authentication

Server-side requests are authenticated using HMAC-SHA256 signatures. Every request must include two headers:

HeaderValueDescription
X-Signaturesha256={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 the X-Timestamp header (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:

FieldTypeRequiredDescription
typestringYestrack, page, identify, or group
eventstringTrack onlyEvent name
user_idstringRecommendedKnown user ID
anonymous_idstringNoAnonymous ID (use if user_id is not available)
propertiesobjectNoEvent properties
contextobjectNoContextual data (IP, user agent, etc.)
context.ipstringRecommendedClient IP address for geolocation enrichment
context.user_agentstringRecommendedClient user agent for device parsing
timestampstringNoISO 8601 timestamp; defaults to server receipt time
message_idstringNoYour idempotency key for the event
event_idstringNoCustomer-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.ip and context.userAgent when you have them. This enables geolocation enrichment and device parsing.
  • Use user_id whenever possible. Server-side events are most valuable when tied to a known user identity.
  • Set timestamp to 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.