Idempotency
Every POST, PATCH, PUT and DELETE takes an Idempotency-Key header so a retry never double-creates or double-mutates.
Every POST, PATCH, PUT and DELETE request against /v1/* requires an Idempotency-Key header. The key makes retries safe: replaying with the same key returns the cached response, while replaying the same key with a different body is rejected.
Header contract
POST /v1/webhook-endpoints HTTP/1.1
Authorization: Bearer zg_live_...
Idempotency-Key: 0199a4ae-6c02-7b5c-9f51-c5f0da5e2f12
Content-Type: application/json- An opaque string, required on every mutation.
- Any format works; generate a UUID per call.
- Omit the header and the API returns
IDEMPOTENCY_KEY_MISSING(HTTP 400).
Replay semantics
| Condition | Server response | Reply header |
|---|---|---|
| First time this key is seen | Fresh response, normal HTTP status | |
| Same key, same request body hash | Cached response, same HTTP status | Idempotent-Replay: true |
| Same key, different request body hash | 409 IDEMPOTENCY_KEY_REUSED | |
| Same key, concurrent in-flight | 425 IDEMPOTENCY_REPLAY_IN_PROGRESS |
Successful 2xx JSON responses are cached under a hash of the token, method, path and key for 24 hours, with the response body encrypted at rest, so the one-time secrets in a token mint or a webhook endpoint registration never sit in the cache in clear text. After that, the same key rotates out of the cache and behaves as a fresh request. 4xx and 5xx responses are not cached; callers are free to retry them.
Request body fingerprint
Reuse detection hashes the full raw request body. Two requests are "the same" when their token, method, path, key and body all match byte for byte, which keeps accidental reuse obvious.
Client-side recipe
A safe retry loop:
$key = (string) Str::uuid();
$response = Http::withHeaders([
'Authorization' => "Bearer {$token}",
'Idempotency-Key' => $key,
'Content-Type' => 'application/json',
])->retry(
times: 3,
sleepMilliseconds: 500,
when: fn ($exception, $request) => $exception?->response?->status() >= 500,
)->post($apiUrl.'/v1/webhook-endpoints', [
'name' => 'Deploy hook',
'url' => 'https://hooks.example.test/zusegenius',
'events' => ['project.created'],
]);If the first attempt succeeds and the retry fires anyway (your client timed out on a 200 it never saw), the second call hits the cache and returns the same response; you never double-register the endpoint.
Cached replays carry Idempotent-Replay: true so your integration can
recognise them and skip any client-side side effects that might otherwise fire
twice.
What does not require a key
GETrequests, which are naturally idempotent./v1/ping.- Webhook deliveries from Zuse Bifrost to your endpoint, which carry their own event id for consumer-side de-duplication.
Related
How is this guide?
Last updated on
Tenancy & Scopes
Every read and write is scoped to one team, scope entries on a token narrow it further to chosen projects, and a guest's token never reaches beyond their invitations.
Rate Limiting
The named rate-limit buckets applied per token to the REST API and the webhook test endpoint, and the headers that report them.