Idempotency
Retry Wingspan V3 creates and money movement safely with the Idempotency-Key header, without creating duplicates or paying twice.
Retry requests that create resources or move money without duplicating the operation. A dropped connection or timeout does not tell you whether Wingspan completed the request. Use the Idempotency-Key header to retry safely.
How it works
Send a unique Idempotency-Key with a POST or PATCH. Wingspan stores the response for 24 hours. If you send the same request again with the same key, Wingspan returns the stored response instead of running the request a second time.
curl -X POST "https://api.wingspan.app/v3/payments/payables" \
-H "Authorization: Bearer $WINGSPAN_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 5d0b8f7e-2a41-4c3e-9f6d-8b1a2c3d4e5f" \
-d '{
"payeeId": "Rw5Nc2Hk8LqZ4tVp9XmBsa",
"externalId": "INV-2026-0412",
"currency": "USD",
"dueDate": "2026-04-15",
"lineItems": [
{ "description": "March site visits", "totalCost": 1250.00 }
]
}'The first response carries Idempotent-Replayed: false. If you send the identical request again within 24 hours, Wingspan returns the same status and body with Idempotent-Replayed: true and does not create a second payable.
When to send a key
We recommend sending an Idempotency-Key on every POST that creates a resource or moves money, and on any other POST or PATCH you might retry.
Some operations require the header, and their reference pages mark it required. Most are money movement, card, mandate, and webhook secret operations, plus create a batch and processing a batch. A missing required key returns 422 ValidationError (400 on purchases). On other operations, including most creates such as creating a payable or a payee, the header is optional. Sending it costs nothing and protects you from duplicates.
Wingspan honors the key on POST and PATCH only. On GET, DELETE, and other methods, Wingspan accepts the header and ignores it.
Choosing a key
- Use 1 to 255 printable ASCII characters with no spaces. A UUID v4 works well.
- Generate a new key for each distinct operation, such as each payable you mean to create.
- Store the key with the operation in your system before you send the request, so a retry after a crash or restart uses the same key.
- Don't reuse a key for a different operation, even after 24 hours.
What you get back
| Situation | Response |
|---|---|
| First request with this key | The request runs normally. Idempotent-Replayed: false. |
| Same key, same body, within 24 hours | The stored response: same status, same body, and the stored Location, ETag, and Wingspan-Version headers. Idempotent-Replayed: true. |
Same key, different body or different Wingspan-Version | 409 IdempotencyKeyConflict. Wingspan won't guess which request you meant. |
| Same key while the first request is still running | 409 IdempotencyKeyConflict with extensions.retryAfter (seconds). Wait that long and retry with the same key and body. |
Same key after the first request returned 5xx | The request runs again. Server errors aren't stored, so the retry gets a fresh attempt. |
Wingspan stores the first response with a status from 200 to 499 and replays it unchanged, including errors. If the first request returned 422 or 409, a retry with the same key returns the same 422 or 409. Use a new key when you send a corrected request.
Wingspan does not store requests rejected before they reach the operation, for example by authentication, Account scope, the version check, or rate limiting. Retry them with the same key.
Where a key applies
Wingspan scopes a key to the Account (or Person) that the request acts on, as well as the HTTP method and path. The same key sent to two different endpoints, or to the same endpoint with different X-Wingspan-Account values, counts as two separate keys. The request fingerprint includes the Wingspan-Version header, so changing it while using the same key returns 409 IdempotencyKeyConflict.
Limits of the guarantee
Stored-response replay is best-effort, not an exactly-once guarantee:
- Wingspan does not store server errors. After a
5xx, the retry runs again. The first attempt may still have completed before the error. For creates, first look up the record byexternalIdwithfilter[externalId][eq]=.... AnexternalIdprovides a second line of defense: a duplicate returns409 ResourceConflictinstead of creating a second record. - Keys last 24 hours. Wingspan treats a later retry as a new request.
- Wingspan does not store large requests. Request or response bodies over 1 MiB run without replay protection.
- Storage can be unavailable. In rare cases, Wingspan cannot reach its idempotency store and runs the request without replay protection instead of failing it.
Some operations return one-time secrets, such as a new API key or webhook signing secret. Wingspan never stores those responses. Check the relevant reference page to learn what a repeated key returns.
What can go wrong
| Response | Cause | Fix |
|---|---|---|
422 ValidationError on Idempotency-Key, InvalidFormat | The key is empty, longer than 255 characters, or contains spaces or non-ASCII characters | Use a UUID or another printable ASCII string |
422 ValidationError on Idempotency-Key, InvalidContext | You sent the key without an authenticated caller or Account context | Authenticate and send X-Wingspan-Account on Account-scoped endpoints if your token is not tied to an Account |
422 ValidationError naming Idempotency-Key | The operation requires the header. Some operations list it in errors[] with Required; others name it only in detail | Add an Idempotency-Key |
400 BadRequest on a purchase | Creating a purchase requires the header | Add an Idempotency-Key |
409 IdempotencyKeyConflict | Same key with a different body or Wingspan-Version, or the first request is still running | See the table above |
Related pages
Updated 10 days ago