---
title: "Idempotency & revisions"
description: "Why retrying after a network error is safe and how concurrent changes never overwrite each other."
lang: en
url: https://developers.bodo-app.com/en/guides/idempotency/
apiVersion: 2026-11-01
---

# Idempotency & revisions

Why retrying after a network error is safe and how concurrent changes never overwrite each other.

## One key, four outcomes

`Idempotency-Key` is required for POST and DELETE and lasts 24 hours per key, method and route. Only status and resource ids are stored, never a response body.

| Answer | Case | What happens |
| --- | --- | --- |
| 201 | First call | Bodo runs it and stores status and ids. |
| 201 · replay | Same key, same body | The answer is rendered again under your current scope, with `Idempotent-Replayed: true`. |
| [409 IDEMPOTENCY_IN_PROGRESS](/en/problems/IDEMPOTENCY_IN_PROGRESS/) | The first call is still running | Wait briefly and retry with the same key. |
| [422 IDEMPOTENCY_PAYLOAD_MISMATCH](/en/problems/IDEMPOTENCY_PAYLOAD_MISMATCH/) | Same key, different body | Bodo runs nothing. Create a new key for a new operation. |

> Answers with `5xx`, `429` and `503` are never stored. You can simply retry with the same key.

## Revisions instead of overwriting

Contacts, companies, tasks and documents carry a revision. The ETag is `"r<revision>"`, `If-Match` is optional. The comparison runs atomically in the database, not in the gateway.

**Two writers, one revision**

```http
GET /v1/contacts/con_4Nf7…        → 200, ETag: "r3"

PATCH /v1/contacts/con_4Nf7…
If-Match: "r3"
Content-Type: application/merge-patch+json
{"email": "e.mustermann@musterfirma.de"}
                                  → 200, ETag: "r4"

PATCH … If-Match: "r3"            → 412 PRECONDITION_FAILED
```

On [PRECONDITION_FAILED](/en/problems/PRECONDITION_FAILED/) read the record again, merge your change onto it and send it with the new ETag.

## PATCH as JSON Merge Patch

RFC 7396, with `Content-Type: application/merge-patch+json`.

| You send | Effect |
| --- | --- |
| a field with a value | sets the value |
| a field with `null` | clears the value |
| an array | replaces the whole array |
| a missing field | stays unchanged |
