> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pivotal.app/llms.txt.
> For full documentation content, see https://docs.pivotal.app/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pivotal.app/_mcp/server.

# Idempotency

Pivotal's `POST` endpoints accept an `Idempotency-Key` header. Send the same key with the same body twice and you get the same response twice — the second request doesn't create a duplicate resource.

This matters because networks fail. If a `POST /customers` request times out, you can't tell whether the customer was created or not. With an idempotency key you can retry safely.

```http
POST /api/v1/customers HTTP/1.1
Authorization: Bearer pivotal_…
Content-Type: application/json
Idempotency-Key: 9c4d…-aurora-2026-05-26

{ "name": "Aurora Outfitters", "slug": "aurora", "status": "onboarding" }
```

## Key rules

* **Format:** any opaque string up to 255 bytes. UUIDs work; so do app-specific strings like `customer-create-aurora-2026-05-26`. Whatever you pick has to be unique per logical operation.
* **Scope:** keys are scoped to one workspace and one endpoint. Two different workspaces can use the same key without colliding. Two different endpoints with the same key behave independently.
* **TTL:** Pivotal retains idempotency results for **24 hours**. After that, the same key is treated as fresh and will create a new resource.
* **Header is optional.** `POST` without an `Idempotency-Key` works exactly as before — at-most-once becomes at-least-once on retry.

## What we cache

When you send `Idempotency-Key` and the request succeeds, Pivotal stashes:

* The HTTP status code.
* The full response body.
* A hash of the request body.

A second request with the same key returns the cached response, byte-for-byte, with the same `id` and `display_id`.

## Body must match

If you send the same `Idempotency-Key` with a *different* body, Pivotal returns `409 Conflict`:

```json
{
  "error": {
    "type": "conflict",
    "code": "idempotency_key_reused",
    "message": "Idempotency-Key was used with a different request body."
  }
}
```

This catches a common bug: the same key getting reused across two unrelated operations because someone hardcoded `Idempotency-Key: 1`.

## In-flight collision

If two requests with the same key arrive concurrently, the second one returns `409`:

```json
{
  "error": {
    "type": "conflict",
    "code": "idempotency_key_in_flight",
    "message": "Idempotency-Key request is already in flight. Retry shortly."
  }
}
```

Retry after a short delay and you'll get the result of the first one (assuming it succeeded).

## When you need a key

Use idempotency keys for:

* **Customer creation** — the main one. Don't double-create from a "Sign up" button.
* **Onboarding creation** from a webhook (e.g. HubSpot closed-won) that may redeliver.
* **Contact creation** from any source that retries.

You don't need one for:

* `GET` requests — already idempotent by definition.
* `DELETE` — soft delete twice on the same record is fine; the second call returns the same body.
* `PATCH` — same effect both times unless your patch is non-idempotent (rare with PATCH).

## Picking keys

Three patterns work well, in order of preference:

1. **Caller-side UUIDv4.** Generate a fresh UUID per logical operation, store it on your side, retry with the same UUID.
2. **Natural key plus context.** `customer-create-{external-id}-{epoch-day}` is fine if you trust your `external-id`. Avoid plain `{external-id}` — you may legitimately want to recreate after a delete.
3. **Request-body hash plus timestamp bucket.** `sha256(body) + ":" + floor(now / 60s)`. Self-contained but slightly more code.

What doesn't work: a global counter (collides on cold starts), the current Unix epoch (collides under concurrency), the user's email (the same user might legitimately create multiple resources).

## Response headers

Idempotent responses carry `Idempotent-Replay: true` on the second (cached) response:

```http
HTTP/1.1 201 Created
Idempotent-Replay: true
Content-Type: application/json
```

Log that header to confirm retries are doing what you expect.