Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,20 +394,22 @@ polymorfa message send text \
--body 'Your order shipped.' --idempotency-key order-123-shipped
```

For 24 hours after the first request, the API prevents the same key and request
For 1 hour after the first request, the API prevents the same key and request
from running the write again:
Comment on lines +397 to 398

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the remaining CLI help to the one-hour retry window

With the one-hour API window documented here, users following several command help descriptions are still told that retries have “24-hour retention”: this remains in src/commands/message/send/text.ts:24, the shared send flags in src/input/message-send.ts:26, the payment-order flags in src/input/payment-order.ts:15, and campaign create/launch help. A caller retrying a message after, for example, two hours can therefore follow the advertised retention period yet execute another send because the key has expired. Update the remaining Messaging idempotency help alongside this window change so it does not promise protection for an additional 23 hours.

Useful? React with 👍 / 👎.


- A recorded success returns `409 idempotency_completed` with
`Idempotent-Replayed: true`. The CLI reports an error; the response does not
contain the original message or response body.
- A recorded server error returns its status and error code with
`Idempotent-Replayed: true`. The write may have happened.
- A finished request returns its original status and body, success or error,
with `Idempotent-Replayed: true`. A send whose response was lost prints the
original message.
- A recorded error is returned again. Correct the request and use a new key.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve outcome reconciliation before recommending a new key

For a recorded server error, replaying the error does not establish that the original write had no effect; the previous guidance explicitly warned that the write may have happened. Telling callers to correct the request and use a new key for every recorded error can therefore cause a duplicate message when the original send succeeded before an error was recorded. Limit this advice to errors known to precede execution, and retain the instruction to reconcile message events or webhooks before changing keys after a potentially side-effecting failure.

Useful? React with 👍 / 👎.

- A request still running returns `409 idempotency_in_progress` with
`Retry-After`. An unrecorded outcome returns
`409 idempotency_outcome_unknown` and blocks another attempt with that key.
- Reusing a key for a changed request returns `409 idempotency_conflict`.
- If the recorded response cannot be read right now, the API returns
`503 idempotency_replay_unavailable` with `Retry-After`. Retry with the same
key; the write does not run again.
- Reusing a key for a changed request returns `422 idempotency_conflict`.

Keep the first response when you receive it. If the outcome is uncertain, check
If the outcome is uncertain, check
message events or webhooks before deciding whether to use a new key. A new key
does not resolve the uncertainty and can send a duplicate. See the
[Messaging retry contract](https://docs.polymorfa.com/guides/communicate/overview#retry-sends-safely).
Expand Down Expand Up @@ -548,8 +550,8 @@ object with `name`, `language`, `category`, and `components` from `--input`, or
from stdin when it is `-`. Template names and provider IDs remain strings.
Edits read `{ "components": [...] }` and return acceptance, not approval to send.
Deletion requires `--yes`. Create, edit and delete send one request and never
retry. Create and delete accept `--idempotency-key`; reuse the same key after an
unknown outcome and the API answers `idempotency_completed` instead of writing
retry. Create and delete accept `--idempotency-key`; reuse the same key after a
lost response and the API returns the original response instead of writing
again. List the catalog before resubmitting. The template routes need an active
Official API connection, including on a Hybrid Number; a Number with only a
Linked Devices connection is rejected.
Expand Down Expand Up @@ -1716,9 +1718,9 @@ instants. List returns one complete newest-first array with no cursor. Requeue
is synchronous and reports only the number moved.

Create, launch, pause, resume and stop accept `--idempotency-key` to deduplicate
the same request for 24 hours. A successful request repeated with the same key
returns `409 idempotency_completed`, not the original response. Changed input
returns `409 idempotency_conflict`; an in-progress or uncertain result must be
the same request for 1 hour. A request repeated with the same key returns the
original response, success or error. Changed input returns
`422 idempotency_conflict`; an in-progress or uncertain result must be
resolved before trying a new key. The SDK generates a fresh key for create and
launch when the flag is omitted. Supply an explicit key from the first attempt
to reuse it across CLI invocations with the same input, credential and API
Expand Down
4 changes: 2 additions & 2 deletions docs/command-research/campaigns.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ server deduplication window. The middleware is mounted after authentication,
rate limits and client rules, before the campaign routes under `/messaging`.
Its identity includes the credential, project, method, route, API version and
key; the fingerprint includes path parameters and the canonical request body.
A repeated successful request returns `409 idempotency_completed`, not the
original response body. Changed input returns `409 idempotency_conflict`;
A repeated request returns the original response, success or error. Changed
input returns `422 idempotency_conflict`;
in-flight requests return `idempotency_in_progress`, and uncertain outcomes
remain fenced as `idempotency_outcome_unknown`. Do not switch keys merely to
bypass an uncertain result; inspect the campaign or operation first. The SDK
Expand Down
2 changes: 1 addition & 1 deletion src/commands/campaign/pause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function lifecycleFlags() {
return {
'idempotency-key': Flags.string({
description:
'Deduplicate this request for 24 hours. A completed key returns 409 idempotency_completed.',
'Deduplicate this request for 1 hour. A repeat with the same key returns the original response.',
}),
'project-slug': Flags.string({
description: 'Project slug in the Messaging API path.',
Expand Down
2 changes: 1 addition & 1 deletion src/commands/campaign/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default class CampaignResume extends BaseCommand {
static override flags = {
'idempotency-key': Flags.string({
description:
'Deduplicate this request for 24 hours. A completed key returns 409 idempotency_completed.',
'Deduplicate this request for 1 hour. A repeat with the same key returns the original response.',
}),
'project-slug': Flags.string({
description: 'Project slug in the Messaging API path.',
Expand Down
2 changes: 1 addition & 1 deletion src/commands/campaign/stop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default class CampaignStop extends BaseCommand {
static override flags = {
'idempotency-key': Flags.string({
description:
'Deduplicate this request for 24 hours. A completed key returns 409 idempotency_completed.',
'Deduplicate this request for 1 hour. A repeat with the same key returns the original response.',
}),
'project-slug': Flags.string({
description: 'Project slug in the Messaging API path.',
Expand Down
2 changes: 1 addition & 1 deletion src/commands/channel/message/react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class ChannelMessageReact extends BaseCommand {
static override flags = {
'idempotency-key': Flags.string({
description:
'Key that makes retries of this write safe for 24 hours. Generated when omitted.',
'Key that makes retries of this write safe for 1 hour. Generated when omitted.',
}),
reaction: Flags.string({
description: 'Reaction text, at most 32 UTF-8 bytes; empty removes it.',
Expand Down
2 changes: 1 addition & 1 deletion src/commands/cloud/template/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default class CloudTemplateCreate extends BaseCommand {
}),
'idempotency-key': Flags.string({
description:
'Key that fences a manual retry: reuse it after an unknown outcome and the API returns idempotency_completed instead of writing again.',
'Key that fences a manual retry: reuse it after a lost response and the API returns the original response instead of writing again.',
}),
}
async run(): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/cloud/template/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default class CloudTemplateDelete extends BaseCommand {
}),
'idempotency-key': Flags.string({
description:
'Key that fences a manual retry: reuse it after an unknown outcome and the API returns idempotency_completed instead of writing again.',
'Key that fences a manual retry: reuse it after a lost response and the API returns the original response instead of writing again.',
}),
}
async run(): Promise<void> {
Expand Down
Loading