Skip to content

Idempotency Guide

This guide explains how to safely handle retries and network failures when integrating with the Stackd API, and what the API guarantees about repeated requests.


Why idempotency matters

When a network request fails — timeout, connection reset, server error — you often cannot know whether the server processed the request before the failure occurred. Without idempotency, retrying a POST /api/v1/transactions could award points twice for the same purchase. This guide explains how to avoid that.


Customer upsert — built-in idempotency

POST /api/v1/customers is naturally idempotent via its upsert behaviour.

  • If you submit the same phone number or email address twice, the existing customer record is returned.
  • No duplicate is created.
  • The response is identical whether the customer was created or found.

You can safely retry this endpoint without risk of duplication.


Transaction recording

POST /api/v1/transactions is not natively idempotent — retrying will create a second transaction and award points/stamps twice.

Before retrying a failed transaction POST, check whether the original succeeded by fetching recent transactions for the customer:

bash
curl "https://yourloyalty.app/api/v1/customers/{ulid}/transactions?per_page=5" \
  -H "Authorization: Bearer $STACKD_TOKEN"

If the most recent transaction matches your expected transaction_type_id, spend_amount_cents, and was created within the last few minutes, the original request succeeded — do not retry.

When recording a transaction, include a unique correlation identifier in the request via a request header or embed it in an optional metadata field (if your integration layer supports this). Store the transaction ULID returned by the first successful call. On retry, look up the stored ULID first before sending a new request.

Safe retry window

A transaction PIN (used in the QR walk-in flow) expires after 90 seconds and is single-use. API transactions bypass the PIN flow entirely, so there is no built-in expiry window — your retry logic must be deliberate.

Recommended approach: Only retry if you receive a 5xx response or a network timeout. Do not retry on 4xx responses — those indicate a definitive failure (bad request, insufficient balance, etc.).


Redemption creation

POST /api/v1/redemptions deducts points and creates a redemption record atomically. Retrying on a timeout may create a duplicate redemption and deduct points twice.

Check before retry: Look up the customer's recent redemptions or check their current points_balance before retrying. If a pending redemption for the same reward already exists, fulfil it rather than creating a new one.


Void — safe to retry

POST /api/v1/transactions/{ulid}/void is idempotent from a business logic perspective — voiding an already-voided transaction returns a 422 with "This transaction has already been voided.". No double-reversal occurs.

Safe retry: If you receive a 422 with this specific message, treat it as a success — the transaction was voided (possibly on the first attempt before the timeout).

json
{
  "type": "https://yourloyalty.app/errors/business-rule",
  "title": "Business Rule Violation",
  "status": 422,
  "detail": "This transaction has already been voided."
}

Fulfilment — safe to retry

POST /api/v1/redemptions/{ulid}/fulfil is similarly safe to retry. Fulfilling an already-fulfilled redemption returns 422. No duplicate fulfilment occurs.


Coupon redemption

POST /api/v1/coupons/{ulid}/redeem is idempotent per voucher instance. A voucher ULID is single-use — the second attempt returns "This voucher has already been redeemed." without applying the reward twice.


Retry strategy recommendations

ScenarioAction
2xx receivedSuccess — do not retry
4xx receivedDefinitive failure — do not retry; fix the request
429 receivedBack off and retry after the Retry-After header value
5xx receivedRetry with exponential back-off (see below)
Network timeoutCheck for existing record first; retry only if not found

Exponential back-off

attempt 1: wait 1s
attempt 2: wait 2s
attempt 3: wait 4s
attempt 4: wait 8s
give up after 4 attempts

Add ±10–20% jitter to each wait to avoid thundering-herd if you have many concurrent clients.


Rate limits

The API enforces a limit of 60 requests per minute per authenticated token. If you exceed this, you will receive a 429 Too Many Requests response with a Retry-After header indicating when the next request can be made.

Spread bulk operations (e.g. customer upsert during data migration) across time. If you need a higher rate limit for a specific integration, contact support.

Stackd Loyalty Platform