> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alvys.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Updating Records Safely

> ETags and the If-Match header on the Alvys Public API — how PATCH preconditions work, what 412 and 428 mean, and how to handle a concurrent edit.

Every `PATCH` on the Alvys Public API is guarded by an **ETag**. This is the general mechanism, not a per-endpoint quirk: if a surface supports `PATCH`, it expects a precondition.

The problem it solves is the one blind updates create. You read a record, decide a change, and send it. Between your read and your write, someone else changed the same record — a dispatcher in the UI, another integration, a second worker of your own. Without a precondition your write lands on top of theirs, and their edit is gone with no error and no trace.

## The loop

**1. Read, and keep the `ETag`.** Every write returns one, and so does a read on the surfaces that support updates:

```http theme={null}
HTTP/1.1 200 OK
ETag: "0f8c1a3e-7b21-4d19-9d7a-2c5b8e40a1c6"
```

**2. Send it back as `If-Match`:**

```http theme={null}
PATCH /api/p/v1/credits/customer/{accessorialId}
If-Match: "0f8c1a3e-7b21-4d19-9d7a-2c5b8e40a1c6"
Content-Type: application/json
```

**3. Keep the new `ETag` from the response.** Each successful update issues a new one, and the next update needs it. A flow that updates twice can carry the value forward instead of re-reading in between.

## What the answers mean

| Status                      | Meaning                                                      | What to do                                   |
| --------------------------- | ------------------------------------------------------------ | -------------------------------------------- |
| `200 OK`                    | Applied. A new `ETag` is on the response.                    | Keep the new validator.                      |
| `428 Precondition Required` | No `If-Match` header.                                        | Read the record, then retry with its `ETag`. |
| `412 Precondition Failed`   | Your validator is stale — the record changed.                | **Re-read, re-decide, retry.**               |
| `409 Conflict`              | The record cannot be changed at all — money already settled. | Do not retry. Post a correction instead.     |

`412` and `409` are different failures and want different handling. A `412` says *try again with fresh information*; a `409` says *this record is closed, stop asking*.

<Warning>
  **Do not drop the header to get past a `412`.** The header is not the obstacle — the concurrent edit is. Removing it does not make your change correct; it makes it overwrite whatever you did not see, which is the exact outcome the precondition exists to prevent.
</Warning>

## Handling a `412` properly

A `412` is not a retry-the-same-bytes situation. The record changed, so re-send only after deciding your change still applies:

1. Re-read the record and take the new `ETag`.
2. Compare it against what you expected. Did the other writer change the same field you are changing, or a different one?
3. If your change still makes sense, apply it to the **new** state and send with the new validator.
4. If it does not — someone already corrected the amount you were about to correct — stop. Do not re-apply it.

Retrying a `412` in a loop without step 2 is a blind update with extra steps.

## Replacement versus merge

Read the endpoint before assuming which one you are calling, because the two behave differently on an omitted field:

* **Replacement** — the update writes all the mutable fields. **Omitting one clears it.** Send the complete set you want the record to end up with, not only what you are changing.
* **Merge** — the update writes only what you send, and leaves the rest alone. Send at least one field.

On the accessorial and credit surfaces, the customer, carrier, and driver *accessorial* and *credit* updates are replacements; the **driver credit** update is a merge.

## What updates never change

Structural fields are fixed when a record is created and are not accepted on an update:

* An accessorial or credit cannot move to a different load, trip, stop, or accessorial type.
* A driver credit cannot move to a different driver or owner-operator.

Moving money to a different parent is a delete and a create, not an edit — the new parent settles differently, and rewriting the pointer would leave the original statement wrong.

## This is not idempotency

An ETag protects a record that **already exists** from a concurrent edit. An idempotency key protects a record that **may not exist yet** from being created twice. They solve different problems and are not interchangeable — a create takes `ExternalId`, an update takes `If-Match`.

See [Idempotency](/en/api/guides/idempotency) for retrying creates safely.
