# Idempotency

Idempotency controls whether repeating a logical request can cause another
provider-side attempt or can return a retained result. Gateman does not make
every authenticated HTTP request idempotent, does not guarantee exactly-once
execution or delivery, and does not provide one universal duplicate ledger.

Gateman has five separate concerns:

1. Contract v1 signs the optional idempotency header so the request cannot be
   changed without invalidating its signature.
2. Email and SMS routes provide Gateman-managed idempotency records.
3. Credential-disclosure routes use a separate single-use request identifier.
4. Provider relays can forward provider-native idempotency mechanisms.
5. Operation accounting determines whether an accepted billable invocation
   consumes an operation. Accounting is not universal request deduplication.

## Signed idempotency keys and Contract v1

`X-GM-Idempotency-Key` is optional. Its normalized value is included in the
Contract v1 signing input for both HMAC-SHA256 and Ed25519 requests. Before
signing, Contract v1 trims leading and trailing ASCII spaces (`0x20`) and tabs
(`0x09`). A missing header, an empty value, or a value containing only those
characters contributes an empty signing component. The exact signing
construction, raw-body rules, timestamp, and encoding are defined in
[Contract v1](https://docs.getgateman.com/contract-v1/).

Signing this header does not create a server-side idempotency record. It only
binds the normalized value to the authenticated request. Duplicate suppression
exists only where the selected capability implements it.

Email and SMS validate a supplied non-empty key in their own route-specific
idempotency stores. Their current rules trim the key, reject an empty key,
reject keys over 255 bytes, and reject ASCII control characters. These rules
are capability rules, not a universal Contract v1 validation rule. Do not rely
on duplicate physical HTTP headers; send one canonical header value.

## Capability coverage

| Mechanism | Current coverage | What it controls |
| --- | --- | --- |
| Contract v1 idempotency header | Any authenticated request that sends it | Signed request integrity only |
| Gateman-managed idempotency | `POST /send/ses`, `/send/postmark`, `/send/resend`; `POST /twilio/sms/send`, `/telnyx/sms/send`, `/aws/sms/send` | Claiming, duplicate suppression, and retained send results |
| Credential request claim | `POST /secrets/get`, `POST /db/get` | Single-use credential-disclosure request identifiers |
| Provider-native idempotency | Selected Stripe, Square, PayPal, AWS, and other provider operations | Provider-defined duplicate handling |
| Operation accounting | Explicitly billable capabilities | Usage consumption after acceptance |

## Email idempotency

The email routes are:

- `POST /send/ses`
- `POST /send/postmark`
- `POST /send/resend`

Email idempotency is optional. When supplied, a key is scoped by the
application and email provider. The request fingerprint includes the selected
environment and normalized provider payload. Environment therefore
participates in conflict detection but does not create separate email key
namespaces: reusing a key for the same application and provider in another
environment can produce `409` with
`idempotency_key_payload_conflict`.

The normal outcomes are:

| Request state | Result |
| --- | --- |
| First request with the key | The request claims the key and proceeds through provider or capture admission. |
| Exact duplicate after success | The retained result is returned as HTTP `200` without another email attempt or Gateman operation. |
| Exact duplicate after definite failure | The retained result is returned as HTTP `409` without another email attempt or Gateman operation. A replayed result does not reproduce the original HTTP response headers. |
| Duplicate while the first request is unresolved | `409` with `mail_send_indeterminate`. Treat the original outcome as unresolved. |
| Same key with a changed payload or environment | `409` with `idempotency_key_payload_conflict`. |
| Provider request may have started | The claim remains protected; do not send the same logical message under a new key merely because the response was uncertain. |

Completed successful and definite failed results are retained for 15 minutes.
Email idempotency replays the stored Gateman JSON result, not the original HTTP
response status or headers. Successful results replay with HTTP `200`, while
retained failures replay with HTTP `409`. Original response headers, including
`Retry-After`, are not preserved.

Email admission is provider-aware. For production sends, provider-bound
concurrency is admitted before operation accounting. A definitive provider
concurrency rejection therefore consumes no operation and releases the owned
claim for a legitimate retry. A monthly allowance rejection or accounting
failure also occurs before provider execution, consumes no operation, and
releases the owned claim. Staging and development capture bypass provider
concurrency and reserve an operation immediately before capture; an accepted
capture consumes one operation.

After admission, an accepted provider or capture operation remains billable
even when the provider rejects it, times out, or its outcome becomes
indeterminate. Provider-started or uncertain claims remain protected. Gateman
does not automatically retry email sends. SES, Postmark, and Resend retain
their distinct request formats and provider error classifications; SES also
uses Gateman-controlled sender identity as a security boundary. SES SDK
automatic retries are disabled so Gateman's idempotency and uncertainty model
is not hidden behind an additional SDK retry.

## SMS idempotency

The SMS routes are:

- `POST /twilio/sms/send`
- `POST /telnyx/sms/send`
- `POST /aws/sms/send`

SMS idempotency is optional. A key is scoped by application, provider, and
environment. The fingerprint includes the provider, environment, and request
payload. Unlike email, environments are independent SMS idempotency
namespaces: the same key can be used separately for production, staging, and
development, subject to each route's request requirements.

Successful results and definite provider failures are retained and replayed
for the active retention period. SMS replays the stored HTTP status, body, and
selected response headers. A changed payload under the same scoped key
returns `409` with `idempotency_key_payload_conflict`. A duplicate whose first
request is still `in_progress`, or whose provider outcome is uncertain,
returns `409` with `sms_send_indeterminate`.

Provider-bound concurrency is checked before SMS operation accounting. A
definitive concurrency rejection returns the route's `429` or `503` response,
consumes no operation, and releases the current claim so the same key can be
retried. If accounting rejects the request because the monthly allowance is
exhausted, or accounting is unavailable, no provider request has started, no
operation is consumed, and the current claim is released. A legitimate retry
with the same key can then claim it again.

Once an SMS operation is accepted and provider execution may have started, the
claim remains protected. Provider timeouts, transport failures, and uncertain
provider results do not prove that no SMS was sent. Do not switch to a new key
unless the application deliberately accepts the possibility of a duplicate.

Staging and development SMS requests are captured locally rather than sent to
Twilio, Telnyx, or AWS. An accepted capture consumes one operation and its
successful result can be replayed through the SMS idempotency record. The
provider-native response contracts are otherwise unchanged.

## Retention, unresolved operations and recovery

The documented retention period for supported email and SMS send results is
15 minutes.

Terminal results can expire. After expiry, the same key may be claimed for a
new operation, and that new operation can consume another operation allowance.
Expiry is not a guarantee that every key is reusable:

- Email can recover an expired `in_progress` claim only when its execution
  state shows that provider execution was not started. A provider-admitted or
  provider-started claim remains protected when its outcome is unresolved.
- Email `unknown` results and SMS `indeterminate` results remain protected;
  they are not physically reclaimed merely because the replay window elapsed.
- SMS protects an unresolved `in_progress` claim; provider-started or
  indeterminate outcomes must be treated as potentially executed.

Operation-accounting recovery does not reconcile provider execution or
email/SMS idempotency records. A timeout is not evidence that the provider
rolled back a mutation or message.

## Credential-request replay protection

`POST /secrets/get` and `POST /db/get` require a separate header:

```http
X-GM-Credential-Request-ID: 0123456789abcdef0123456789abcdef
```

The value must be exactly 32 lowercase hexadecimal characters. It is included
in Contract v1 signing input and is claimed once for the authenticated
application across both credential-retrieval routes. Claims are retained for
11 minutes. Reuse returns `409`; a legitimate subsequent attempt requires a
fresh identifier and a fresh signature.

Credential responses are never replayed from an idempotency cache. A request
identifier may remain consumed even if a later stage of the request fails.
This protection is distinct from `X-GM-Idempotency-Key`, which is optional and
does not make ordinary authenticated requests single-use. Do not put
credential values or storage details in application logs.

## Provider-native idempotency

Provider relays do not share a universal Gateman duplicate ledger. Where a
relay contract supports a provider-native mechanism, Gateman validates and
forwards it; the provider defines its meaning, scope, retention, and replay
behavior.

| Relay | Supported provider-native mechanism |
| --- | --- |
| Stripe | `Idempotency-Key` header |
| Square | Idempotency fields in supported request bodies |
| PayPal | `PayPal-Request-Id` header |
| AWS general relay | AWS operation-specific identifiers where the selected operation supports and receives them |
| Cloudflare general relay | Endpoint-specific semantics; no universal Gateman or Cloudflare relay idempotency facility |

An accepted Gateman relay invocation ordinarily consumes its own operation,
even when the external provider recognizes the forwarded request as a
duplicate. Gateman does not automatically deduplicate relay calls and does
not automatically retry provider mutations. See the individual [Stripe](https://docs.getgateman.com/capabilities/stripe.use/),
[Square](https://docs.getgateman.com/capabilities/square.use/),
[PayPal](https://docs.getgateman.com/capabilities/paypal.use/),
[AWS](https://docs.getgateman.com/capabilities/aws.use/), and
[Cloudflare](https://docs.getgateman.com/capabilities/cloudflare.use/)
references for request-specific rules.

## Idempotency and operation accounting

Idempotency and accounting answer different questions: idempotency controls
whether a logical send is attempted again, while accounting controls whether
an accepted billable invocation consumes an operation.

| Situation | Email/SMS managed idempotency | Operation accounting |
| --- | --- | --- |
| First accepted send | Claims the key and executes or captures | Consumes one operation |
| Exact retained duplicate | Replays the retained result | Consumes no additional operation |
| Conflicting duplicate | `409` payload conflict | Consumes no operation |
| In-progress or indeterminate duplicate | `409` indeterminate error | Consumes no additional operation |
| Terminal record expired and request accepted again | Creates a new claim | Consumes one new operation |
| Gateman concurrency rejection before reservation | Claim is released where applicable | Consumes no operation |
| Monthly allowance rejection before reservation | Claim is released where applicable | Consumes no operation |
| Accepted provider rejection, timeout, or uncertain result | Terminal failure or protected indeterminate state | Operation remains consumed |
| Separate accepted provider-relay request | No Gateman duplicate suppression | Ordinarily consumes one operation, even if provider-native idempotency deduplicates it |

Rejected authentication, validation, credential-request replay, and
pre-reservation admission requests do not consume a billable operation. Once a
billable operation is accepted, provider failure does not make it free. See
[Operation Accounting](https://docs.getgateman.com/operation-accounting/) for
the complete allowance and enforcement contract.

## Retry guidance

- Retry a definitive pre-provider rejection when the response permits it. For
  supported email and SMS sends, the same key can be retried after Gateman
  releases the rejected claim.
- Replay an exact retained terminal result rather than starting a new logical
  send.
- Do not blindly retry `mail_send_indeterminate` or
  `sms_send_indeterminate`. A provider may already have accepted the message.
- A new idempotency key means a new logical send and can duplicate an earlier
  uncertain send.
- After terminal retention expires, decide explicitly whether a new attempt
  is safe; do not assume the previous provider execution did not occur.
- For `/secrets/get` and `/db/get`, generate a new credential request ID and
  signature for every legitimate retry.
- For provider relays, use the provider-native identifier required by the
  selected provider operation when available. Gateman will not add one or
  retry the mutation for you.
- A transport timeout proves only that the client did not receive a completed
  response. It does not prove provider failure. Respect applicable
  `Retry-After` headers for rate limits, but do not convert a provider rate
  limit or timeout into a blind duplicate mutation.

The official PHP, Node.js, Python, and Go clients do not automatically retry
Gateman requests.

## Related references

- [Contract v1](https://docs.getgateman.com/contract-v1/)
- [Authentication and signing](https://docs.getgateman.com/authentication/)
- [Errors and responses](https://docs.getgateman.com/errors-and-responses/)
- [Operation Accounting](https://docs.getgateman.com/operation-accounting/)
- [Rate limits and concurrency](https://docs.getgateman.com/rate-limits-and-concurrency/)
- [Environments](https://docs.getgateman.com/environments/)
- [Send email](https://docs.getgateman.com/capabilities/email.send/)
- [Send SMS](https://docs.getgateman.com/capabilities/sms.send/)
