The request
{
"name": "wever_retry-safety-check",
"arguments": {
"paths": [
{
"name": "Refund after support approval",
"operation": "refund",
"timeout": "10s",
"upstream_completes_within": "30s",
"retry_on": [
"timeout"
],
"max_retries": 3,
"idempotency_key": {
"present": true,
"scope": "per_attempt"
}
},
{
"name": "Card charge at checkout",
"operation": "charge",
"timeout": "15s",
"retry_on": [
"timeout",
"server_error"
],
"max_retries": 2,
"idempotency_key": {
"present": true,
"scope": "per_operation",
"source": "order id"
}
}
]
}
}What the report returns
Verdict: double_effect_possible.
| Refund after support approval | A retry after timeout can produce a second refund. The idempotency key is created per attempt, so each retry looks like a new request. |
|---|
| Card charge at checkout | A retry cannot produce a second charge: idempotency key holds across every retry. |
|---|
| Fix, Refund after support approval | Create the idempotency key once per refund, before the first attempt, and reuse it on every retry. |
|---|
| Fix, Refund after support approval | Before retrying a timed-out refund, look it up by reference and retry only when no refund exists. |
|---|
1 path where a retry can produce a second charge, refund or settlement (Refund after support approval); 1 protected.
The refund path gives up after 10 seconds while the provider can take 30, so a retry starts while the first refund is still completing. Its idempotency key is new on every attempt, so the provider reads each retry as a fresh refund. The checkout charge reuses one key per order, so its retries settle once. One key per refund, and a status lookup before each retry, closes the refund path.