Skip to content

Public REST API

Concrete cURL, Node.js, Python and PHP examples for the server-to-server API. For architecture and schema, see /documentation#api.

Server-to-server only

Never use API keys in the browser or in a mobile app. The API is built exclusively for server-to-server calls — it sets no CORS headers and does not check the Origin. An API key embedded in the frontend is compromised immediately.

Demo values in the examples

Important: the examples below use demo values like kunde@example.com and +49 30 99999999 (DRA reserved range). Never use real end-customer data when experimenting — produce test requests with test customer profiles or anonymous values.

Quickstart in 4 steps

Get your first successful request running in under 5 minutes:

  1. 1. Generate an API key in the app
    Open /app/settings/api in your ShopiPixel dashboard, select the required permissions (events:read / events:write / stats:read are all pre-selected), and click "Generate key". Copy the displayed key immediately — it is shown in cleartext only once.
  2. 2. Set the Bearer header
    Send the key with every request in the Authorization header: Authorization: Bearer sp_xxxxxxxxxxxx
  3. 3. Call an endpoint
    Three endpoints are available — GET /api/v1/events (read events), POST /api/v1/events (create events, single or bulk), GET /api/v1/stats (aggregated statistics).
  4. 4. Run a test request
    Copy one of the cURL examples below and replace the placeholder sp_xxxxxxxxxxxx with your key. On a successful 2xx response, the server already records a read-audit entry (GDPR Article 5(2)).

GET /api/v1/events — read events

Returns your most recent events with cursor-based pagination. Filter parameters: limit (1-200, default 50), eventName, from/to (ISO date), source (PIXEL | WEBHOOK | API | MERGED). Cleartext PII (email, phone, first/last name) and click IDs are intentionally not returned — the customData object is, however, returned unchanged (1:1 as sent in the POST). Therefore do not send end-customer PII in customData.

curl -X GET "https://app.shopipixel.de/api/v1/events?limit=50&eventName=Purchase" \
-H "Authorization: Bearer sp_xxxxxxxxxxxx"

The cursor in pagination.nextCursor is opaque — store it as-is and pass it unchanged into the next request.

POST /api/v1/events — single event

Sends a single event. Idempotency is handled via the eventId field — use a stable value like purchase_<orderId>. If the same eventId is sent again shortly after, the server responds with success: true, duplicate: true without re-incrementing stats.

curl -X POST https://app.shopipixel.de/api/v1/events \
-H "Authorization: Bearer sp_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"eventName": "Purchase",
"eventId": "purchase_1730000000_abc",
"eventTime": 1730000000,
"userData": {
"email": "kunde@example.com",
"phone": "+493099999999"
},
"customData": {
"currency": "EUR",
"value": 49.99,
"orderId": "ORDER-123",
"contents": [
{ "id": "prod-1", "quantity": 1, "itemPrice": 49.99 }
]
}
}'

userData (email, phone) is hashed by the server per platform — you send cleartext, the server normalises and hashes per platform contract. Custom SHA-256 hashing on your end is not required and frequently produces the wrong format.

POST /api/v1/events — bulk insert

Send an array of up to 100 events. Each event is processed individually — successes and failures are reported in the results array (partial success).

curl -X POST https://app.shopipixel.de/api/v1/events \
-H "Authorization: Bearer sp_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '[
{ "eventName": "PageView", "eventId": "pv_001", "eventTime": 1730000000 },
{ "eventName": "ViewContent", "eventId": "vc_001", "eventTime": 1730000010, "customData": { "contentIds": ["prod-1"] } },
{ "eventName": "AddToCart", "eventId": "atc_001", "eventTime": 1730000020, "customData": { "value": 49.99, "currency": "EUR", "contentIds": ["prod-1"] } },
{ "eventName": "InitiateCheckout", "eventId": "ic_001", "eventTime": 1730000030, "customData": { "value": 49.99, "currency": "EUR" } },
{ "eventName": "Purchase", "eventId": "p_001", "eventTime": 1730000040, "customData": { "value": 49.99, "currency": "EUR", "orderId": "ORDER-123" } }
]'

Example response: 4 successful events, 1 failed (event_not_found for an undefined custom event). Successful events keep their status even if other events in the batch fail.

Pagination loop for GET /api/v1/events: while pagination.hasMore is true, pass nextCursor into the next request. Honour 429 responses (Retry-After header) and use exponential backoff for repeated limits.

async function fetchAllEvents(eventName) {
const allEvents = [];
let cursor = null;
do {
const params = new URLSearchParams({ limit: "200", eventName });
if (cursor) params.set("cursor", cursor);
const response = await fetch(
`https://app.shopipixel.de/api/v1/events?${params}`,
{ headers: { "Authorization": "Bearer sp_xxxxxxxxxxxx" } },
);
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? 60);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
continue;
}
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const { data, pagination } = await response.json();
allEvents.push(...data);
cursor = pagination.hasMore ? pagination.nextCursor : null;
} while (cursor);
return allEvents;
}
const purchases = await fetchAllEvents("Purchase");
console.log(`Insgesamt ${purchases.length} Purchase-Events geladen`);

GET /api/v1/stats — statistics

Aggregated metrics: revenue, conversion rate, success rates per platform. Granularity day | week | month | hour. With hour the range is limited to 7 days.

curl -X GET "https://app.shopipixel.de/api/v1/stats?granularity=day&from=2026-04-01&to=2026-05-01" \
-H "Authorization: Bearer sp_xxxxxxxxxxxx"

Multi-currency stores are converted to the store currency automatically at the current daily exchange rate. The currency field in the response indicates the target currency.

Best Practices

  • Idempotency via eventId: use stable values like purchase_, lead_. Replays and retries are silently deduplicated — the same eventId sent again shortly after behaves as if sent once.
  • Deduplication: events with an identical eventId are automatically treated as duplicates within a short time window. Response: success: true, duplicate: true, no stats increment, no platform send.
  • Retry strategy on 429: honour the Retry-After header (in seconds). On repeated limit hits, use exponential backoff — e.g. 1s, 2s, 4s, 8s, 16s with jitter.
  • Bulk size: maximum 100 events per request, ~50 is optimal for latency. For very large imports, sequential batches of 50 are better than two parallel batches of 100 — this is the most reliable approach.
  • eventTime window: the server only accepts eventTime in the range [now-86400, now+300] seconds. Older than 24h or further than 5 min in the future → per-event validation_failed in the bulk result.
  • PII hashing: send plain values (email: "kunde@example.com", phone: "+493099999999") — the server normalises and hashes the data automatically in the format each platform requires. Custom hashing on your end is not needed and frequently produces the wrong format.

Error diagnostics

401 unauthorized
The Authorization header is missing, malformed, or contains an invalid, expired or revoked key. Check in /app/settings/api whether the key is still active and that the format Authorization: Bearer sp_… is correct (Bearer word + space + key).
403 Permission … required
The key lacks the required permission. Example: GET /api/v1/events requires events:read. Check the permissions in /app/settings/api or generate a new key with the necessary scopes.
403 Enterprise plan required
The store is not on the Enterprise plan or the subscription is not ACTIVE. Check plan status in the billing area. Tester and dev stores have a bypass.
413 Request body too large
The request body exceeds 512 KB. For bulk requests: fewer events per batch (≤50 instead of 100), smaller customData objects, or split the data into multiple requests.
429 Rate limit exceeded
Each API key allows 1,000 requests/h. The Retry-After header indicates the wait time in seconds. For frequent 429s: fewer single POSTs, use bulk inserts instead.
event_not_found in bulk response
You used a custom event name that has not yet been defined in /app/event-triggers/events. Create the custom event in the app (with isActive: true) and resend the request.
validation_failed due to eventTime
eventTime is outside the allowed window [now-86400, now+300] seconds. Send events within 24h of their actual occurrence — historical backfills are not supported.

See also