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:
- Contract v1 signs the optional idempotency header so the request cannot be changed without invalidating its signature.
- Email and SMS routes provide Gateman-managed idempotency records.
- Credential-disclosure routes use a separate single-use request identifier.
- Provider relays can forward provider-native idempotency mechanisms.
- 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.
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/sesPOST /send/postmarkPOST /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/sendPOST /telnyx/sms/sendPOST /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_progressclaim 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
unknownresults and SMSindeterminateresults remain protected; they are not physically reclaimed merely because the replay window elapsed. - SMS protects an unresolved
in_progressclaim; 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:
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, Square, PayPal, AWS, and Cloudflare 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 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_indeterminateorsms_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/getand/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-Afterheaders 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.
