Server-side events API
The pixel handles 80% of tracking, but some events should always come from the server:
- Orders / purchases — the user may close the tab before the thank-you page fires.
- Refunds / cancellations — never happen in the browser.
- Anything billing-related — accuracy beats convenience.
- Headless / mobile clients that don't ship the pixel.
This is the same architectural shape as Meta's Conversions API +
Pixel: send the same event_id from both channels, Skryx dedupes,
each event is counted once.
# Endpoint
POST https://api.skryx.io/v1/events
Authorization: Bearer sk_live_…
Content-Type: application/json
The key needs the track:write scope. Admin keys (sk_admin_…)
have it via the index:admin super-scope. To issue a narrower
ingestion-only key, create one with type=search and explicit
scopes=["track:write"].
# Batch shape
{
"events": [
{
"event_id": "ord_42",
"event_name": "order.completed",
"occurred_at": "2026-05-26T14:42:00Z",
"session_id": "sess_abc123",
"user_id": "u_99",
"source": "server",
"properties": {
"order_id": "42",
"total": 199.99,
"currency": "EUR",
"products": [
{ "id": "SKU-123", "quantity": 1, "price": 199.99 }
]
}
},
{ "event_id": "…", "event_name": "cart.add", … }
]
}
Cap: 200 events per request. For larger batches, split client-side and post in parallel.
# Response shape
{
"accepted": 198,
"rejected": 2,
"errors": [
{
"index": 5,
"errors": [
{ "field": "properties.total", "error": "required for order.completed" }
]
},
{ "index": 17, "errors": [{ "field": "event_name", "error": "unknown event type: cart.checkout" }] }
]
}
- HTTP 200 when every event passed.
- HTTP 207 (Multi-Status) when some passed, some failed.
- HTTP 400 / 401 / 403 / 429 for whole-batch failures.
Validation errors are per-row so you can fix the bad rows and resend without touching the good ones.
# Idempotency: event_id
event_id is the dedup key. The same event_id from the same tenant
gets persisted once regardless of how many times you send it. Use this
to:
- Retry safely: if your background job dies mid-batch, just resend.
- Pixel + server combo: if you also fire the same event via the
pixel (highly recommended for orders), use the same
event_idfrom both sides. Skryx counts once.
A typical pattern for orders:
event_id = "ord_" + order.id // stable across pixel + server
Pixel: skryx.track('order.completed', { event_id_override: 'ord_42', … })
(pixel pulls event_id from the event_id_override property when set,
otherwise generates evt_… internally)
Server: send { "event_id": "ord_42", … } directly.
# Rate limits
- 60 req/sec/IP burst cap (same as the rest of /v1).
- Daily event cap per plan (see Plans page). Free = 10k/day, Starter = 100k, Growth = 1M, Business = 10M, Enterprise unlimited.
- Soft warning at 80% of daily cap — events still flow, dashboard shows a banner.
- Hard 429 at 100% on plans without overage; plans with
overage_per_1k_…continue at billable overage.
# Code samples
# PHP / Laravel
use Illuminate\Support\Facades\Http;
$response = Http::withToken(env('SKRYX_API_KEY'))
->post('https://api.skryx.io/v1/events', [
'events' => [[
'event_id' => 'ord_'.$order->id,
'event_name' => 'order.completed',
'occurred_at' => now()->toIso8601String(),
'session_id' => $session->id,
'user_id' => $user->id,
'source' => 'server',
'properties' => [
'order_id' => (string) $order->id,
'total' => (float) $order->total,
'currency' => $order->currency,
'products' => $order->items->map(fn ($i) => [
'id' => $i->product_id,
'quantity' => $i->quantity,
'price' => (float) $i->price,
])->all(),
],
]],
]);
# Python
import requests
requests.post(
"https://api.skryx.io/v1/events",
headers={"Authorization": f"Bearer {SKRYX_API_KEY}"},
json={
"events": [{
"event_id": f"ord_{order.id}",
"event_name": "order.completed",
"occurred_at": order.completed_at.isoformat() + "Z",
"session_id": session.id,
"source": "server",
"properties": {"order_id": str(order.id), "total": float(order.total)},
}]
},
timeout=5,
)
# Node
await fetch("https://api.skryx.io/v1/events", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SKRYX_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
events: [{
event_id: `ord_${order.id}`,
event_name: "order.completed",
occurred_at: new Date().toISOString(),
session_id: session.id,
source: "server",
properties: { order_id: String(order.id), total: order.total },
}],
}),
});
# Ruby
require 'net/http'
require 'json'
Net::HTTP.post(
URI('https://api.skryx.io/v1/events'),
{
events: [{
event_id: "ord_#{order.id}",
event_name: 'order.completed',
occurred_at: Time.now.utc.iso8601,
session_id: session.id,
source: 'server',
properties: { order_id: order.id.to_s, total: order.total.to_f }
}]
}.to_json,
'Content-Type' => 'application/json',
'Authorization' => "Bearer #{ENV['SKRYX_API_KEY']}"
)