Why idempotency matters
Payroll lodgement is unforgiving: a duplicate pay event submission means two entries on the ATO side, which means reconciliation work and potentially amendment events. If your network hiccups while posting a pay event, you want to retry without worrying about creating duplicates.
The Beeswax STP API supports idempotency via the Idempotency-Key HTTP header. When present, we dedupe retries of the same request transparently — first request processes normally, subsequent retries with the same key return the cached response without re-executing the action.
Using it
Add an Idempotency-Key header on any mutating request (POST, PATCH):
curl https://stp.beeswaxapp.com/api/v1/pay_events \
-X POST \
-H "Authorization: Bearer stp_live_..." \
-H "Idempotency-Key: pay-event-acme-2026-04-14" \
-H "Content-Type: application/json" \
-d '{ "pay_event": { ... } }'
The key is an arbitrary string up to 255 characters. You choose what goes in it — common patterns:
- A UUID generated per request:
5e4b3a2c-... - A deterministic ID derived from your own data:
pay-event-{employer_id}-{pay_period_end} - A timestamp plus a transaction ID:
20260414-t12345
How it works
- First request with a given key → we process the request normally and cache the response for 24 hours.
- Subsequent requests with the same key → we look up the cached response and return it as-is, without re-running the controller action.
- Retries return the same HTTP status code, body, and
Idempotent-Replay: trueheader so you can tell they're replays.
The cache key is scoped by API token, so two different customers can't collide even if they happen to pick the same idempotency key.
Response header
Look for the Idempotent-Replay header on responses to know whether you got a fresh execution or a cached replay:
HTTP/1.1 201 Created
Idempotent-Replay: false # fresh execution
HTTP/1.1 201 Created
Idempotent-Replay: true # cached response — action did NOT run again
What's cached
We cache:
- The HTTP status code
- The JSON response body
We don't cache:
- Other response headers (so things like X-Request-Id change between fresh and replayed responses)
- Non-JSON responses (defensive — avoids storing anything weird)
- Non-2xx responses (if the first request failed, your retry should be allowed to succeed)
Which endpoints support idempotency?
Idempotency is enabled on all mutating endpoints that create or modify resources:
| Endpoint | Methods |
|---|---|
/employers |
POST, PATCH |
/employers/:id/employees |
POST, PATCH |
/pay_events |
POST, PATCH |
/pay_events/:id/items |
POST |
/pay_events/:id/mark_ready |
POST |
/pay_events/:id/submit |
POST |
GET, DELETE, and query-only endpoints ignore the header because they're naturally idempotent or their semantics make replay meaningless.
Key length limit
Keys must be 255 characters or fewer. Longer keys return:
{
"error": "Invalid Idempotency Key",
"message": "Idempotency-Key must be 255 characters or fewer",
"code": "invalid_idempotency_key"
}
Cache TTL
Cached responses expire 24 hours after the first request. After that, the key is forgotten and a retry will be processed as a fresh request. In practice, retry loops should complete in seconds or minutes; 24 hours is a generous buffer.
Best practices
- Always set an idempotency key on mutating requests in production. It's one header and it's free.
- Derive the key from your own data when possible.
pay-event-{employer_id}-{pay_period_end}makes your retries inherently safe even if your process crashes and a fresh worker picks up the same work. - Don't reuse keys across different requests. If you send the same key for two genuinely different requests, the second will receive the first's cached response — wrong.
- Don't include secrets in the key. It's logged in request traces for debugging. Use opaque identifiers.
- For test tokens, keys are scoped per-token. Test and live tokens can happily share the same key value without interference.
Gotchas
Validation failures on the first request: Idempotent caching only kicks in for successful (2xx) responses. If your first request returns 422 because of a validation error, the next retry with the same key will be processed fresh. That's intentional — you want the opportunity to fix and resubmit.
Server errors on the first request: Same as validation failures. 5xx responses aren't cached, so you can retry with the same key and expect a fresh execution.
Race conditions on nearly-simultaneous requests: If you fire two requests with the same key within milliseconds, they might both execute before the first one's response is cached. This is a rare edge case but worth knowing about for high-concurrency code. Serialize your retries client-side.
Cache expiry: If you retry after 24 hours, the key is no longer cached and the retry will be processed fresh. This shouldn't happen in practice — retry loops should be seconds long, not days.