Sending Business Events
Send one event per authoritative business fact, from your backend, after it is final (payment captured, account created). Storelayer queues the event and processes it asynchronously against your rules. See Loyalty events for the model.
Request
Section titled “Request”curl -X POST https://api.storelayer.io/ingest/events \ -H "X-API-Key: $STORE_LAYER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "purchase", "userId": "cust_8421", "payload": { "orderId": "ord_1001", "amount": 7450, "currency": "USD", "items": [{ "sku": "COFFEE-1KG", "quantity": 2, "price": 3725 }] } }'- Auth: project API key via
X-API-KeyorAuthorization: Bearer <key>. NoprojectIdin the path. - Body:
type(non-empty string, required),userId(string, optional),payload(object, required). - Only those three top-level fields survive. Anything else (
amount,eventId,idempotencyKey,timestamp) is silently stripped. Put business data inpayload.
Payload design
Section titled “Payload design”| Decision | Do | Why |
|---|---|---|
| Event type | Stable snake_case: purchase, order_completed, user_registered | An event resource key must match ^[a-zA-Z][a-zA-Z0-9_]*$ to bind a rule to the type. |
| Money | Integer minor units (7450 = $74.50) plus currency | Rule thresholds compare raw numbers. 74.50 vs 5000 silently never matches. |
| Customer | Your stable external ID in userId | Wallet actions default to $('event').userId. Without it, reward fails with Reward action requires userId. |
| Order identity | payload.orderId | Lets rules tag wallet entries and lets you trace and deduplicate. |
| Reserved | Never send payload._meta | Used internally for event chaining. |
Responses
Section titled “Responses”201 Created:
{ "success": true, "data": { "id": "evt_x7Kq…", "status": "accepted" } }Store data.id. It is also the workflow id.
400 validation failure. Note that error is an array of issues here, not the { code, message } object other endpoints return:
{ "success": false, "error": [{ "expected": "string", "code": "invalid_type", "path": ["userId"], "message": "Invalid input: expected string, received number" }], "data": { "type": "purchase", "userId": 123, "payload": {} }}data echoes the body you sent.
401 (error.code: 1002) means the API key is missing or invalid.
Duplicates and idempotency
Section titled “Duplicates and idempotency”There is no idempotency key. Each call gets a new evt_ id and is processed independently, so the same purchase sent twice earns twice. Wallet entries created by rules reference the rule, not your order.
- Deduplicate at the source: record “sent to Storelayer” against your order inside the same transaction or outbox that triggers the send.
- On a timeout or network error, do not resend blindly. If you saved the
evt_id, pollGET /projects/{projectId}/workflows/{eventId}. If the request never returned an id, check the customer’s recent events (GET /projects/{projectId}/users/{userId}/events) or wallet transactions before retrying. - Put
orderIdin a reward’stags("tags": ["{{ $('event').payload.orderId }}"]) so you can audit the ledger for duplicates.
Refunds and reversals
Section titled “Refunds and reversals”Send a separate event, such as refund with the original orderId and refunded amount in cents, and give it its own rule with a redemption action that mirrors the earning formula. If the customer has already spent the points, the redemption fails (Insufficient balance…) and that workflow is failed. Handle that case in your business logic, for example by allowing it or by reversing a smaller amount. See the loyalty program guide.
Follow the event
Section titled “Follow the event”201means queued only. No rule has run yet.- Poll
GET /projects/{projectId}/workflows/{eventId}. It returns404until processing finishes, then the full record. - Check
rules[].matchedandactions[].status/error, then confirm the wallet.
See Workflows for the record shape and polling.
See also
Section titled “See also”Loyalty events · Workflows · Debugging rules · Events and Workflows in the API reference