Concurrency

Every Run carries its revision; every write that changes an existing run accepts Expected-Revision and refuses a stale revision with 409 revision_mismatch, whose body names currentRevision — checked inside the run's lock. The API sends no ETag and refuses If-Match.

View this page as Markdown

Two people, an automation and your integration can all touch the same run. The API prevents lost updates with optimistic concurrency on the run's revision.

Revision

Every Run the API returns carries its revision in the body — reads, starts and every write alike (a new run starts at 1; a finalized upload answers it as run.revision). It is an opaque, monotonic integer bumped on every persisted change to the run: a field value, a task's state, the header, a file, a trash or restore. It tracks the run's persisted state; derived display data (a formatted value, a due date computed from today's date) may differ at the same revision. Comments and the upload ticket never move it.

revision is the one version field. The API sends no ETag header — not on a read, not on a create, not on a write.

The revision may jump by more than one between two reads: the engine can record a logical change as two bumps. Only equality matters.

Expected-Revision

Send the revision you last saw — from a read or from the body of your last write — as the Expected-Revision header on any write that changes the run: field writes, task actions, PATCH /v1/runs/{id}, trash and restore, finalizing or removing a file. Those are the only operations that read the header: a read (GET /v1/runs/{id}) and a start (POST /v1/runs) answer a revision but have nothing to compare it to, and comments and upload tickets never touch the revision. Removing a file answers 204 with no body; read the run for its new revision before the next conditional write. Workflows, views, webhooks, schedules and automation rules carry no revision at all: their writes ignore Expected-Revision, and the last write wins.

curl -X PATCH https://app.processcart.com/api/v1/runs/{id}/fields \
  -H "Authorization: Bearer pc_live_…" \
  -H "Content-Type: application/json" \
  -H "Expected-Revision: 7" \
  -d '{ "amount": 1800 }'
  • The check happens inside the run's lock, after the row is locked and before anything is evaluated, so two racing writers cannot both pass.
  • A match applies the write and answers the updated Run; its revision is the value to send on your next write.
  • A mismatch is refused with 409 revision_mismatch. The problem body names the run's current revision in currentRevision (and repeats it in detail):
{
  "type": "https://developers.processcart.com/errors#revision_mismatch",
  "title": "Revision mismatch",
  "status": 409,
  "detail": "The run changed; its current revision is 9. Read it again, merge, and retry with Expected-Revision: 9.",
  "code": "revision_mismatch",
  "requestId": "req_…",
  "currentRevision": 9
}

Read the run again, merge, and retry with Expected-Revision: <currentRevision>. A 409 is never stored for idempotent replay, so a retry with the same Idempotency-Key and a fresh Expected-Revision re-executes.

Accepted forms: a bare integer (Expected-Revision: 7, the canonical form) or the same in double quotes ("7"); * matches any revision, the same as omitting the header. Anything else — an empty value, a list, a weak W/"7", a negative number, a fraction, text — is a 422 validation_failed with errors[].field: "Expected-Revision".

Why not If-Match

CDN layers evaluate If-Match against the response and turn a successful write into a 412. A hosting edge treats If-Match as a standard HTTP precondition and compares it with the ETag of the response the API is about to send — so a write that succeeded, and moved the revision, would reach you as 412 Precondition Failed. A header of the API's own is passed through untouched.

So the API does not use If-Match at all: a request that carries it is refused with 422 validation_failed (errors[].field: "If-Match", message "This API does not use If-Match; send Expected-Revision: instead"), on every authenticated operation, reads included — never silently honored or ignored. A client that sends If-Match sees 412 Precondition Failed from the hosting layer on some deployments and the 422 from the API on others; both mean "stop sending If-Match" — send Expected-Revision instead. If-None-Match is not supported either: a GET always returns the current document.

Without Expected-Revision

Omit the header and the write is applied to whatever the run holds at that moment, last writer wins per field. That is fine for appending a comment or setting a field nobody else edits; use Expected-Revision whenever you read, compute and write back.

Lock semantics

Every run write runs under a row lock: the run is locked, Expected-Revision is checked, the engine re-evaluates formulas, visibility and locks from the persisted values, the gates are applied, and only then is anything written — in one transaction. Concurrent writes to one run therefore serialize; writes to different runs do not contend. The 600-requests-per-minute limit is the only concurrency limit per key.