Reference

Server-to-server (S2S) API

Send events to Reflect from your backend — verified purchases, subscription renewals, server-side game logic, web apps without the SDK.

When to use S2S vs the SDK

Use caseSDKS2S
Native installs, sessions, ad signals
Tutorial / level / share — anything inside the game
Verified purchases (after server-side receipt validation)
Subscription renewals / cancellations from billing webhook
Backfilling historical data✅ (bulk)
Web app with no Unity client

Both can run side-by-side. Use the same install_uuid in both calls and Reflect stitches the events to the same identity.

Mint a server key

Sign in to the admin panel → API & S2S keys → fill the form. Pick:

  • App — the key is scoped to one app.
  • Label — human name like ci-purchases or billing-webhook.
  • Allowed events — comma-separated event names. Leave blank for "all events". Useful so a leaked key can only fire purchase, not app_install.

The plain-text key is shown once after creation. Copy it into your CI / server env var; you can't recover it later. Format: srv_<48 hex chars> (~192 bits of entropy).

Authentication

Bearer token in the Authorization header:

Authorization: Bearer srv_<your_key>

No HMAC over the body — the bearer token is enough. (HMAC made sense for the SDK because client secrets ship in app binaries; server keys live in env vars and never reach a client.)

POST /s2s/event — single event

Required fields: install_uuid, event_ts_ms, event_name. Everything else is optional.

POST https://reflect.bablu147147.workers.dev/s2s/event
Authorization: Bearer srv_...
Content-Type: application/json

{
  "event_id":       "txn_abc123",
  "event_name":     "purchase",
  "event_ts_ms":    1735999999000,
  "install_uuid":   "8f2a1c0e94d7423b8b53af7c9e21d630",
  "user_id":        "user_42",
  "revenue":        9.99,
  "currency":       "USD",
  "transaction_id": "txn_abc123",
  "product_id":     "sku_pro_pack",
  "props": {
    "channel":     "web_checkout",
    "ab_variant":  "B"
  }
}

Response (200):

{
  "accepted":  1,
  "event_id":  "txn_abc123",
  "audit_key": "s2s/2026-04-26/txn_abc123.json"
}

curl

curl -X POST https://reflect.bablu147147.workers.dev/s2s/event \
  -H "Authorization: Bearer $REFLECT_S2S_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event_name":   "purchase",
    "event_ts_ms":  '"$(date +%s)000"',
    "install_uuid": "8f2a1c0e94d7423b8b53af7c9e21d630",
    "user_id":      "user_42",
    "revenue":      9.99,
    "currency":     "USD",
    "transaction_id": "txn_abc"
  }'

Python

import os, time, uuid, requests

def send(event_name: str, install_uuid: str, **fields):
    body = {
        "event_id":     str(uuid.uuid4()).replace("-", ""),
        "event_name":   event_name,
        "event_ts_ms":  int(time.time() * 1000),
        "install_uuid": install_uuid,
        **fields,
    }
    r = requests.post(
        "https://reflect.bablu147147.workers.dev/s2s/event",
        headers={"Authorization": f"Bearer {os.environ['REFLECT_S2S_KEY']}"},
        json=body, timeout=10,
    )
    r.raise_for_status()
    return r.json()

# After verifying an iOS receipt server-side:
send("purchase",
     install_uuid="8f2a1c0e94d7423b8b53af7c9e21d630",
     user_id="user_42",
     revenue=9.99, currency="USD",
     transaction_id="apple_tx_xyz")

Node.js

async function trackS2S(eventName, installUuid, fields = {}) {
  const body = {
    event_id:     crypto.randomUUID().replace(/-/g, ""),
    event_name:   eventName,
    event_ts_ms:  Date.now(),
    install_uuid: installUuid,
    ...fields,
  };
  const r = await fetch("https://reflect.bablu147147.workers.dev/s2s/event", {
    method:  "POST",
    headers: {
      "Authorization": `Bearer ${process.env.REFLECT_S2S_KEY}`,
      "Content-Type":  "application/json",
    },
    body: JSON.stringify(body),
  });
  if (!r.ok) throw new Error(`Reflect S2S ${r.status}: ${await r.text()}`);
  return r.json();
}

PHP

function reflect_s2s(string $event, string $installUuid, array $fields = []): array {
    $body = array_merge([
        "event_id"     => bin2hex(random_bytes(16)),
        "event_name"   => $event,
        "event_ts_ms"  => (int)(microtime(true) * 1000),
        "install_uuid" => $installUuid,
    ], $fields);

    $ch = curl_init("https://reflect.bablu147147.workers.dev/s2s/event");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($body),
        CURLOPT_HTTPHEADER     => [
            "Authorization: Bearer " . getenv("REFLECT_S2S_KEY"),
            "Content-Type: application/json",
        ],
    ]);
    $resp = curl_exec($ch);
    if (curl_errno($ch)) throw new RuntimeException(curl_error($ch));
    return json_decode($resp, true);
}

POST /s2s/events/bulk — up to 1000 events

Designed for backfills and high-volume webhook fan-out. The endpoint enqueues each event to the same ingest queue used by the SDK — async, no extra D1 row writes per event.

POST https://reflect.bablu147147.workers.dev/s2s/events/bulk
Authorization: Bearer srv_...
Content-Type: application/json

{
  "events": [
    { "event_name": "purchase", "event_ts_ms": ..., "install_uuid": "...", ... },
    { "event_name": "purchase", "event_ts_ms": ..., "install_uuid": "...", ... },
    ... up to 1000 events ...
  ]
}

Response includes per-event accept/reject:

{
  "accepted": 998,
  "rejected": 2,
  "results": [
    { "event_id": "...", "status": "accepted" },
    { "event_id": "...", "status": "rejected", "reason": "missing_required_fields" },
    ...
  ],
  "audit_key": "s2s/bulk/2026-04-26/<uuid>.json"
}

Idempotent on event_id — resubmitting the same events drops duplicates server-side. Re-send only the rejected items.

Errors

StatusReason
400install_uuid_required, event_ts_ms_required, event_name_required, bad_json, too_many_events (bulk >1000)
401missing_bearer_token, unknown_or_revoked_key
403event_not_allowed_by_key (event not in the key's allow-list)
413body_too_large
429cap_exceeded (tenant's plan quota exhausted)

install_uuid — where to get it

The SDK generates and persists an install_uuid on first launch. You need to send it from the client to your server when the user authenticates, then store it alongside the user's account record. After that your backend can fire S2S events tied to the same identity.

// Client (Unity)
StartCoroutine(SendInstallUuidToBackend(ReflectSDK.InstallUuid));

// Server (Python example)
@app.route("/api/auth/me")
def me(user):
    user.reflect_install_uuid = request.json["install_uuid"]
    user.save()
    return jsonify(ok=True)
Don't generate a new install_uuid on the server
Server-generated UUIDs won't match the SDK's install row, breaking attribution. Always reuse the value the SDK created on the device.

Cost & rate limits

S2S events count toward the same per-tenant monthly quota as SDK events (see Plans). No separate per-key rate limit beyond Cloudflare's native protection. If you're hitting your cap, upgrade or contact us.

Security tips

  • Store the key in your CI / server env var. Never commit it to source.
  • Use the allowed events list to scope each key. A billing-webhook key only needs purchase, subscribe, subscription_renewed, subscription_cancelled, subscription_refunded.
  • Mint one key per role (one for CI, one for billing webhook, one for backfills) — easier to revoke when something leaks.
  • Rotate keys when team members leave: revoke + mint a new one.
  • Reflect stores SHA-256 hash, not the plaintext — even DB exfil can't recover live keys.