Concurrency and ETags
Use ETag and If-Match on Wingspan V3 resources so your update never overwrites a change someone else made after you read the record.
Update a Wingspan V3 resource without overwriting a change made after you read it. Two people or services can read the same payable, change it, and save it. Without a concurrency check, the second save silently undoes the first. ETags turn that conflict into an error you can handle.
How it works
- When you read a resource that supports concurrency checks, the response includes an
ETagheader. It identifies the exact version you received. - When you update that resource, or call an action on it, you send the value back in an
If-Matchheader. - If the resource hasn't changed since your read, the request goes through, and the response carries the new
ETag. - If it has changed, Wingspan rejects the request with
412 PreconditionFailedand makes no change. Some resources also return the currentETagon the412. If it's missing, read the resource again to get it.
Which resources use ETags
Concurrency checks are set per operation, not per resource. Payables, invoices, invoice payment intents, recurring invoices, groups, payouts, and payment cards each have operations that accept If-Match. The reference page for each operation shows:
- Whether the response includes an
ETagheader - Whether
If-Matchis required, optional, or not accepted
An ETag on a response doesn't mean every write to that resource accepts If-Match. Send If-Match only where the reference lists it.
| Resource | Where If-Match applies | If-Match: * | Current ETag on a 412 |
|---|---|---|---|
| Payables | Required on update, open, accept, and reject | Accepted | Yes |
| Invoices | Required on paying an invoice | Accepted | Yes |
| Invoice payment intents | Required on setting the payment method and on confirm | Not accepted | Yes |
| Recurring invoices | Optional on update, pause, resume, cancel, and generate now | Accepted | No |
| Groups | Required on update, delete, and adding or removing members | Not accepted | No |
| Payouts | Required on update | Not accepted | No |
| Payment cards | Required on update, activate, and delete | Not accepted | No |
For example, updating a payable (PATCH /v3/payments/payables/{payableId}) and actions such as POST /v3/payments/payables/{payableId}/open require If-Match. Other payable actions, such as pay and cancel, don't accept it. On operations where it's optional, sending it still gives you the check.
A race on payables and invoices can return 409
On payables and invoices, Wingspan compares your If-Match with the current version and then writes the change as a separate step. If another change lands between those two steps, you get 409 ResourceConflict instead of 412. Handle it the same way: read the resource again and retry with the new ETag.
Update a payable safely
-
Read the payable and keep its
ETag:curl -i "https://api.wingspan.app/v3/payments/payables/b3VhT6gJq1MzR8xKd5WnPe" \ -H "Authorization: Bearer $WINGSPAN_TOKEN"HTTP/1.1 200 OK ETag: "nQ3v8KzP1xLm7RtW4cYb9HdJs2V" Content-Type: application/json -
Send your change with the
ETaginIf-Match. Copy the value exactly, including the double quotes:curl -X PATCH "https://api.wingspan.app/v3/payments/payables/b3VhT6gJq1MzR8xKd5WnPe" \ -H "Authorization: Bearer $WINGSPAN_TOKEN" \ -H "Content-Type: application/json" \ -H 'If-Match: "nQ3v8KzP1xLm7RtW4cYb9HdJs2V"' \ -H "Idempotency-Key: 9a7c1e44-6b2d-4f0a-8e3c-2d5b7f9a1c60" \ -d '{ "notes": "Approved by regional manager" }' -
On
200, store the newETagfrom the response for your next change. -
On
412 PreconditionFailed, someone changed the payable after your read:{ "type": "https://api.wingspan.app/errors/precondition-failed", "title": "Precondition Failed", "status": 412, "code": "PreconditionFailed", "requestId": "3f2c9a7e1b4d4e0f9a8c6b5d2e1f0a93" }Read the payable again, check whether your change still makes sense against the current version, and retry with the new
ETag. Don't retry blindly with the old value. It will fail the same way.
Optional: Skip the check deliberately
If-Match: * matches any current version. Use it when you really do want your write to win, for example in a one-off repair script. It isn't accepted on every resource; the reference page says whether it is. Where it's not accepted (see the table above), * returns 422 ValidationError, and you need to send the exact ETag.
Optional: Use ETags and idempotency together
If-Match and Idempotency-Key solve different problems, and you can send both:
Idempotency-Keyprotects you from your own retries running twice.If-Matchprotects you from overwriting someone else's change.
If you retry with the same Idempotency-Key, you get the stored response from the first attempt, including the ETag it returned.
What can go wrong
| Response | Cause | Fix |
|---|---|---|
412 PreconditionFailed | The resource changed after you read it | Re-read, reapply your change, retry with the new ETag |
409 ResourceConflict | On payables and invoices, another change landed while yours was being written | Re-read, reapply your change, retry with the new ETag |
422 ValidationError naming If-Match | The operation requires If-Match and you didn't send it, or you sent * where it isn't accepted. The detail names If-Match; only some operations also list it in errors[] | Read the resource first and send its ETag, or send * where it's accepted |
409 InvalidStateTransition | The version matched, but the resource's status doesn't allow the action | Read the resource and check its status |
Related pages
Updated 10 days ago