New to custom events? Start with the Events API Overview
for concepts and use cases.
Authentication
All requests require a Bearer token using your store’s API key:Identity Resolution
Each event must include at least one customer identifier inside thecustomer object. The API resolves
customers using the following priority order:
| Priority | Field | Description |
|---|---|---|
| 1 | customer.id | Redo customer ObjectId |
| 2 | customer.email | Customer email address |
| 3 | customer.phoneNumber | Customer phone number (E.164) |
customer.id.
The email is ignored for resolution but may be used for profile enrichment.
Request Shape
202 Accepted with no response body.
Datetime Formatting
All datetime values must be formatted as ISO 8601 (RFC 3339) strings. Example:2026-04-02T12:00:00Z
This applies to eventTimestamp, any date-valued properties in data, and any
date-valued customFields. Strings matching this format are automatically
detected and indexed as dates for segmentation.
Customer Fields
Thecustomer object supports identity, profile, location, and custom fields:
Properties Structure
Thedata object carries event-specific data. Top-level keys are
segmentable — you can use them in segment conditions and automation filters.
Supported Property Types
| Type | Example | Notes |
|---|---|---|
string | "gold" | |
number | 99.99 | Integer or decimal |
boolean | true | |
date | "2025-03-15T10:30:00Z" | ISO 8601 strings are auto-detected |
Ignored Values
Values of0, null, and empty string ("") are treated as unset and are
not stored. This prevents polluting segments with meaningless zero/empty values.
Non-Segmentable Metadata with $extra
To include metadata that should be stored but not indexed for segmentation,
nest it under the $extra key inside data. This is useful for arrays, nested
objects, debug info, or context that doesn’t need to be queried.
Unlike top-level data properties, values inside $extra are not stripped —
0, null, and "" are preserved as-is.
Milestone Type, Total Spent, and VIP Tier are segmentable. Everything
inside $extra is stored on the event but excluded from segmentation indexes.
Value Tracking
Use thevalue and valueCurrency fields to associate a monetary or
conversion value with an event:
valueCurrency uses ISO 4217 currency
codes (e.g., USD, EUR, GBP).
Idempotency
Use theuniqueId field to safely retry requests without creating duplicate
events. If you send two events with the same uniqueId for the same store,
only the first is persisted — subsequent requests are silently deduplicated.
If you don’t provide a
uniqueId, each request creates a new event. Use
uniqueId whenever your system may retry requests (webhooks, queues, etc.).Timestamps
By default, events are timestamped at ingestion time. To set a custom timestamp (e.g., for backfilling historical events), use theeventTimestamp field with
an ISO 8601 value:
Bulk Ingestion
For high-volume use cases, use the bulk endpoint to send up to 100 events per request:Request
202 Accepted with no response body.
Common Patterns
Migrating from Klaviyo custom events
Migrating from Klaviyo custom events
Redo custom events use a similar model to Klaviyo’s Track API. Key
differences:
- Use
eventNameinstead ofevent - Customer identity goes in the nested
customerobject (customer.email,customer.phoneNumber,customer.id), not as root-level fields - Use
datainstead ofpropertiesfor event-specific attributes - Use
$extrainsidedatafor non-segmentable metadata instead of relying on Klaviyo’s property flattening - Use
uniqueIdfor deduplication instead of Klaviyo’s$event_id
Webhook-triggered events
Webhook-triggered events
If you’re sending events from a webhook handler, always include
uniqueId
to handle retries safely:Backfilling historical events
Backfilling historical events
To import historical events, use the bulk endpoint with
eventTimestamp:Python example
Python example
Rate Limits
| Endpoint | Limit |
|---|---|
POST /stores/{storeId}/events | 100 requests/second/store |
POST /stores/{storeId}/events/bulk | 100 requests/second/store |
429 Too Many Requests. Implement
exponential backoff in your retry logic.