# Idempotency

Stable write keys for safe retries.

Canonical HTML: https://developer.shelfcycle.com/guides/idempotency

Write requests require a stable Idempotency-Key so retrying callers do not create duplicates.

`POST /notes`, `POST /contacts`, `POST /customers`, `POST /suppliers`, parent-scoped customer location/address creates, supplier ship-from creates, product creates, and product-family creates require `Idempotency-Key`. Repeating the same route plus the same key and same request body returns the original record. Reusing the same key with a different body returns `409 idempotency_key_reused`.

## Key format

Build the key from stable source facts.

```text
<source-system>:<source-event-id>:<target-type>:<target-id>:<action>
```

Examples:

```text
gmail:thread-18f0:customer:customer-id:create-note
gmail:thread-18f0:customer:customer-id:create-contact:jane@example.com
call:recording-77:supplier:supplier-id:create-note
crm:lead-1842:customer:create-profile
crm:site-742:customer:customer-id:create-location
intake:doc-913:create-product
intake:doc-913:create-product-family
```

> **Warning**: Do not use timestamps or random ids unless the source event is genuinely unique. A random key per retry disables replay protection.

## Note replay

```bash
curl "$SHELFCYCLE_API_BASE_URL/notes" \
  -X POST \
  -H "Authorization: Bearer $SHELFCYCLE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: gmail:thread-18f0:customer:customer-id:create-note" \
  -d '{ "primarySubject": { "type": "customer", "id": "customer-id" }, "noteType": "EMAIL", "title": "Inbound thread", "body": "Approved summary." }'
```

First successful request:

```json
{
  "data": {
    "id": "note-id",
    "type": "note",
    "idempotencyStatus": "created"
  }
}
```

Same key and same body:

```json
{
  "data": {
    "id": "note-id",
    "type": "note",
    "idempotencyStatus": "replayed"
  }
}
```

Same key and different body:

```json
{
  "error": {
    "type": "conflict_error",
    "code": "idempotency_key_reused",
    "message": "The Idempotency-Key was reused with a different request body.",
    "requestId": "request-id"
  }
}
```

## Contact, profile, child-record, and product replay

`POST /contacts`, `POST /customers`, `POST /suppliers`, parent-scoped child creates, `POST /products`, and `POST /product-families` follow the same rule. A duplicate created through a matching idempotency key replays. A different key whose identity fields match an existing record can return `409 duplicate_contact`, `409 duplicate_customer`, `409 duplicate_supplier`, `409 duplicate_customer_location`, `409 duplicate_customer_address`, `409 duplicate_supplier_ship_from_location`, `409 duplicate_product`, or `409 duplicate_family`.

## Retry rule

On network failure or `429 rate_limited`, retry the same POST with the same idempotency key and same body. If a caller needs to change the body, treat it as a new source event and choose a new stable key.
