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

# Idempotency

> Idempotency-Key contract for the Brew Public API v1 — 24h replay window, 409 IDEMPOTENCY_CONFLICT semantics, recommended key patterns.

`POST` endpoints support idempotency via the `Idempotency-Key` HTTP header. Same key + same body within 24 hours returns the **cached original response** instead of doing the work twice. Same key + a **different** body returns `409 IDEMPOTENCY_CONFLICT`. Set this on every retried `POST`.

## Contract

| Property                  | Value                                                                         |
| ------------------------- | ----------------------------------------------------------------------------- |
| Header                    | `Idempotency-Key: <≤ 100 chars>`                                              |
| Methods                   | `POST` only. (`PATCH` is naturally idempotent at the resource level.)         |
| Window                    | 24 hours from the first request.                                              |
| Same key + same body      | Returns the original cached response (same status, same body, same headers).  |
| Same key + different body | `409 IDEMPOTENCY_CONFLICT`.                                                   |
| Different key             | Treated as a fresh request.                                                   |
| Scope                     | Namespaced per-organization (one tenant's key cannot collide with another's). |

## Why every retry needs it

Network errors, timeouts, and re-delivered webhooks all cause your code to call the same API twice. Without idempotency that means duplicate workflow runs, duplicate emails sent, duplicate contact rows created. With `Idempotency-Key`, the retry replays the original outcome without doing the work again.

The endpoints that need it most:

* `POST /v1/automations/triggers/{triggerEventId}/fire` — a doubled fire = doubled welcome / drip / receipt emails.
* `POST /v1/sends` — `EMAIL_ALREADY_SENT` (409) blocks a same-email retry; an idempotency key lets a retry return the original `sendId` / `runId` cleanly.
* `POST /v1/emails` — generate is expensive (\~30–90s). Idempotency makes a flaky-network retry free.
* `POST /v1/contacts` (single OR batch) — the underlying upsert is naturally idempotent on `email`, but `Idempotency-Key` saves you the second batch run.

## Body-field fallback on the fire branch

`POST /v1/automations/triggers/{triggerEventId}/fire` additionally honors a body `idempotencyKey` field for back-compat. Prefer the `Idempotency-Key` header for new code:

```json theme={null}
{
  "payload": { "email": "jane@example.com", "firstName": "Jane" },
  "idempotencyKey": "signup-jane@example.com-1779292800"
}
```

The HTTP header is preferred for new code; the body field stays for parity. If both are set, the HTTP header wins.

## Recommended key patterns

The key MUST be stable per logical operation. Common recipes:

| Operation                         | Recommended key                                                             |
| --------------------------------- | --------------------------------------------------------------------------- |
| Trigger fire from a webhook       | `<eventName>-<userId>-<eventTimestamp>` (e.g. `signup-user_123-1779292800`) |
| Trigger fire from an internal job | `<jobName>-<runId>-<targetUserId>`                                          |
| Campaign send                     | `send-<emailId>-<audienceId>-<scheduledAt or "now">`                        |
| Email generate from a chat        | `email-gen-<conversationId>-<turnId>`                                       |
| Batch contact upsert              | `contacts-batch-<importId>`                                                 |
| Cron-driven daily blast           | `daily-promo-<YYYY-MM-DD>`                                                  |

Anti-patterns to avoid:

* `<uuid()>` per call — defeats the purpose; every retry gets a fresh key.
* `Date.now()` per call — same as above.
* `<sha256(body)>` — vulnerable to partial replays where body bytes change.

## Worked example — fire a trigger with retry

```ts theme={null}
const idempotencyKey = `signup-${userId}-${eventTimestamp}`

async function fireWithRetry() {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const res = await fetch('https://brew.new/api/v1/automations/triggers/tri_signup/fire', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.BREW_API_KEY!}`,
        'Content-Type': 'application/json',
        // Same key on every attempt — first one wins, the rest replay
        'Idempotency-Key': idempotencyKey,
      },
      body: JSON.stringify({
        payload: { email: 'jane@example.com', firstName: 'Jane' },
      }),
    })
    if (res.ok) return res.json()
    if (res.status === 409) {
      const body = await res.json()
      if (body.error?.code === 'IDEMPOTENCY_CONFLICT') {
        // You changed the body between retries. Fix the body OR mint a fresh key.
        throw new Error('Idempotency conflict — fix the body or use a new key')
      }
    }
    if (res.status === 429) {
      const wait = Number(res.headers.get('Retry-After') ?? '1') * 1000
      await new Promise((r) => setTimeout(r, wait))
      continue
    }
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`)
  }
  throw new Error('Exhausted retries')
}
```

Two invariants:

1. **Reuse the same `idempotencyKey` across every retry of the same logical operation.** The first request wins; the rest return the cached response.
2. **Mint a fresh `idempotencyKey` when the body genuinely changes.** A new email recipient = a new key.

## `409 IDEMPOTENCY_CONFLICT` envelope

```json theme={null}
{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "type": "conflict",
    "message": "Idempotency-Key 'signup-jane@example.com-1779292800' was previously used with a different request body.",
    "suggestion": "Use a fresh key OR submit the exact same body as the original request.",
    "docs": "https://docs.brew.new/api-reference/api/idempotency"
  }
}
```

The cached original response can be re-fetched by replaying with the same body — there is no separate "get cached result" endpoint.

## Where it interacts with other contracts

* **Rate limits** — an idempotent replay counts against the rate-limit window. If you saw a `429` on the first attempt, the retry will hit the gate too. Honor `Retry-After`, don't burn keys.
* **Workflow runs** — `POST /v1/automations/triggers/{triggerEventId}/fire` creates rows in `automationExecutions`. An idempotent replay returns the **original** `automationRunIds[]` under `details` so polling stays consistent.
* **Sends** — `POST /v1/sends` reserves the email atomically via `reserveEmailSend`. An idempotent replay returns the original `sendId` / `runId` and short-circuits before re-reservation; a fresh key on a same-email send produces `409 EMAIL_ALREADY_SENT`.

## SDK behaviour

The official `@brew.new/sdk` ships idempotency keys automatically on every `POST` call (auto-generated UUID, scoped to the in-process operation) so you get safe-by-default retries. Override the auto-key by passing `{ idempotencyKey: 'your-key' }` in `RequestOptions`:

```ts theme={null}
await brew.automations.triggers.fire(
  {
    triggerEventId: 'tri_signup',
    payload: { email: 'jane@example.com' },
  },
  { idempotencyKey: `signup-${userId}-${eventTimestamp}` }
)
```

## See also

* [Rate limits](/api-reference/api/rate-limits) — pair with idempotency for safe retry loops.
* [Errors](/api-reference/api/errors) — full `409 IDEMPOTENCY_CONFLICT` envelope.
* [Async jobs & polling](/api-reference/api/async-jobs) — for fire / send retries you typically don't need to call again — poll the run instead.

## Need Help?

Our team is ready to support you at every step of your journey with Brew. Choose the option that works best for you:

<Tabs>
  <Tab title="Self-Service Tools">
    <CardGroup cols="2">
      <Card title="Search Documentation" icon="magnifying-glass" color="#c44925">
        Type in the "Ask any question" search bar at the top left to instantly find relevant documentation pages.
      </Card>

      <Card title="ChatGPT/Claude Integration" icon="robot" color="#c44925">
        Click "Open in ChatGPT" at the top right of any page to analyze documentation with ChatGPT or Claude for deeper insights.
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Talk to Our Team">
    <CardGroup cols="2">
      <Card title="Schedule a Call" icon="calendar" color="#c44925" href="https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ1iYoRUG1J792XQpbuQLjSRRDupr7MwraFK-HQRCtTYdBmrQi8nZu2qXfzKQigb8gbKJK3KN3-R">
        Book time with our founders for personalized guidance on strategy, best practices, or complex implementation questions.
      </Card>

      <Card title="Call Us Directly" icon="phone" color="#c44925">
        Need immediate assistance? Reach us at **+1-(332)-203-2145** for urgent issues or time-sensitive questions.
      </Card>

      <Card title="Slack Channel" icon="slack" color="#c44925">
        Our preferred support channel. You'll receive an invite after signup for direct founder support and fast responses.
      </Card>

      <Card title="Email Support" icon="envelope" color="#c44925" href="mailto:support@brew.new">
        Contact us at **[support@brew.new](mailto:support@brew.new)** for detailed inquiries or if you prefer not to use Slack.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>
