# 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.

Source: https://developers.processcart.com/guides/concurrency

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.

```bash
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`):

```json
{
  "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: <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.
