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

  1. When you read a resource that supports concurrency checks, the response includes an ETag header. It identifies the exact version you received.
  2. When you update that resource, or call an action on it, you send the value back in an If-Match header.
  3. If the resource hasn't changed since your read, the request goes through, and the response carries the new ETag.
  4. If it has changed, Wingspan rejects the request with 412 PreconditionFailed and makes no change. Some resources also return the current ETag on the 412. 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 ETag header
  • Whether If-Match is 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.

ResourceWhere If-Match appliesIf-Match: *Current ETag on a 412
PayablesRequired on update, open, accept, and rejectAcceptedYes
InvoicesRequired on paying an invoiceAcceptedYes
Invoice payment intentsRequired on setting the payment method and on confirmNot acceptedYes
Recurring invoicesOptional on update, pause, resume, cancel, and generate nowAcceptedNo
GroupsRequired on update, delete, and adding or removing membersNot acceptedNo
PayoutsRequired on updateNot acceptedNo
Payment cardsRequired on update, activate, and deleteNot acceptedNo

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

  1. 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
  2. Send your change with the ETag in If-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" }'
  3. On 200, store the new ETag from the response for your next change.

  4. 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-Key protects you from your own retries running twice.
  • If-Match protects 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

ResponseCauseFix
412 PreconditionFailedThe resource changed after you read itRe-read, reapply your change, retry with the new ETag
409 ResourceConflictOn payables and invoices, another change landed while yours was being writtenRe-read, reapply your change, retry with the new ETag
422 ValidationError naming If-MatchThe 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 InvalidStateTransitionThe version matched, but the resource's status doesn't allow the actionRead the resource and check its status

Related pages


Did this page help you?