Public REST API
Concrete cURL, Node.js, Python and PHP examples for the server-to-server API. For architecture and schema, see /documentation#api.
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.
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. 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. Set the Bearer header
Send the key with every request in the Authorization header: Authorization: Bearer sp_xxxxxxxxxxxx - 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. 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 within 20 minutes, 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 (ECB daily 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 within 20 min behaves as if sent once. - Dedup window: 20 minutes. Within this window, events with identical eventId (or fingerprint-equivalent events) are treated as duplicates. 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 — the worker processes each job atomically.
- 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 hashes per platform with the correct algorithm (e.g. Microsoft's Gmail alias stripping, Meta's lowercase-trim). Custom hashing on your end 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.