Idempotency
Write requests require a stable Idempotency-Key so retrying callers do not create duplicates.
POST /notes and POST /contacts 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.
<source-system>:<source-event-id>:<target-type>:<target-id>:<action>
Examples:
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
Do not use timestamps or random ids unless the source event is genuinely unique. A random key per retry disables replay protection.
Note replay
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:
{
"data": {
"id": "note-id",
"type": "note",
"idempotencyStatus": "created"
}
}
Same key and same body:
{
"data": {
"id": "note-id",
"type": "note",
"idempotencyStatus": "replayed"
}
}
Same key and different body:
{
"error": {
"type": "conflict_error",
"code": "idempotency_key_reused",
"message": "The Idempotency-Key was reused with a different request body.",
"requestId": "request-id"
}
}
Contact replay
POST /contacts follows the same rule. A duplicate created through a matching idempotency key replays. A different key whose name or email matches an existing contact under the same parent can return 409 duplicate_contact.
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 approved source event and choose a new stable key.
