Flominzo

Developer guide

Duplicate payouts and unknown status. Resolve by lookup, never by resending.

A payout timeout is an unknown outcome, not a failure. Learn why blind retries pay twice, how idempotency keys help, and how to resolve unknown status.

By , Founder · Last updated:

See the Payouts API

The short answer

When a payout request times out, you do not know whether money moved. Record the outcome as unknown, look the payout up, and wait for settlement evidence. Never send the instruction again as a new request. A blind retry after a timeout is the most common way payout systems pay the same recipient twice.

Idempotency keys reduce the risk, but they only protect you if the provider honours them and your system reuses the same key for the same logical payout. The safe design treats an unknown outcome as a state of its own, with a defined path to resolution.

Why is a timeout not a failure?

A request can fail at three different points, and a timeout looks the same from your side in all three:

  1. The request never reached the provider. No money moved.
  2. The provider received it and rejected it, and the rejection was lost. No money moved.
  3. The provider received it and processed it, and the success response was lost. Money moved.

From the client, all three look like a request that ended without a usable answer. Treating that as a failure assumes the first or second case. If it was the third, a retry sends a second payout.

How do blind retries pay twice?

A blind retry creates a new request, often with a new request identifier. Unless the provider can tell that the new request is the same logical payout, it treats it as a new instruction and pays it. The recipient receives the money twice, and the provider reports two successful payouts.

Recovering a duplicate payout usually means asking the recipient or their bank to return funds, which is slow and not always possible. Mobile wallets and cash pickups can be especially hard to recover once the recipient has used the money. Prevention is far cheaper than recovery.

How do idempotency keys help, and where do they stop?

HTTP defines some methods as idempotent, meaning a repeated request has the same effect as one request. POST, which most payout APIs use to create a payout, is not idempotent by definition (RFC 9110). The IETF Idempotency-Key draft describes a common pattern: the client sends a unique key with the request, and the server uses it to recognise a repeat and return the original result instead of acting again.

  • Derive the key from the instruction, not the attempt. One logical payout has one key, and every retry of it reuses that key.
  • Store the key before sending. If your process crashes mid-request, the key must survive so a later lookup or retry can use it.
  • Know the provider’s rules. Providers differ on whether they support keys, how long they remember them, and which endpoints honour them.
  • Do not rely on the key alone. A key that has expired at the provider, or an endpoint that ignores it, gives no protection. Lookup and settlement evidence are still needed.

What are the four result classes?

Classifying every provider result into one of four classes means the payout system never has to guess what a provider meant. Flominzo’s Rail Adapter Contract requires every adapter to return one of them.

SUCCESS
The provider completed the request. The payout still needs settlement evidence before it is closed.
RETRYABLE
The provider did not act, and it is safe to retry with the same idempotency key.
NON_RETRYABLE
The provider rejected the instruction. Do not send it again; correct it or fail it.
UNKNOWN_OUTCOME
The request may have been processed. Resolve it by lookup, never by sending it again.

How do you resolve an unknown outcome?

An unknown outcome is not terminal. It moves to a known state through evidence, in this order:

  1. Status lookup: query the provider by your client reference or idempotency key. Many unknowns resolve here within minutes.
  2. Webhook: wait for the provider’s callback within its declared window, and verify its signature before trusting it.
  3. Settlement evidence: the provider’s settlement file, balance statement, or your bank statement shows whether the payout was charged.
  4. A recorded human decision: if evidence stays silent, a person decides with the evidence in front of them, and the decision is recorded with its reason.

While the outcome is unknown, the money is in transit: it may have left, or it may not. If the lookup window passes without an answer, the attempt becomes an UNKNOWN_UNRESOLVED exception with an owner, so it cannot sit quietly in a queue.

A decision flow in pseudo-code

// One payout attempt. The idempotency key belongs to the instruction.
result = adapter.createPayout(instruction, instruction.idempotencyKey)

switch (result.class) {
  case SUCCESS:
    record(attempt, SUCCEEDED, result.providerReference)
    expect(settlementLine, cashMovement)       // closure still needs evidence
  case NON_RETRYABLE:
    record(attempt, FAILED, result.reason)      // do not send again
  case RETRYABLE:
    retry(instruction, instruction.idempotencyKey)  // same key, bounded attempts
  case UNKNOWN_OUTCOME:
    record(attempt, UNKNOWN)
    status = adapter.getPayoutStatus(instruction.clientReference)
    if (status.isKnown) record(attempt, status)
    else await webhook or settlement evidence within the declared window
    if (window expired) raise UNKNOWN_UNRESOLVED  // a person decides, with evidence
    // never: createPayout(instruction, newKey)
}

Illustrative pseudo-code. Names and structure are simplified and do not describe a specific API.

What if the provider reports two references?

Sometimes the duplicate is already on the provider’s side: two provider references, or two settlement lines, for one attempt. Flominzo raises this as a DUPLICATE exception.

  • Check the settlement and bank evidence to see whether the provider charged once or twice.
  • If it charged once, the second reference is a reporting duplicate and is resolved with that evidence.
  • If it charged twice, the recipient was likely paid twice. Recovery follows the provider’s return process, and a person approves each step.
  • Any ledger correction is a new, explained entry. The original records are never edited.

A checklist for your payout integration

  • Generate one idempotency key per logical payout, and persist it before the first attempt.
  • Record timeouts, connection resets, and 5xx responses after sending as unknown, not failed.
  • Implement a status lookup by your own reference before any retry logic.
  • Verify webhook signatures and freshness, and accept duplicate deliveries safely.
  • Reconcile against the provider’s settlement file and your bank statement, not only its API.
  • Alert on unknown attempts that outlive the lookup window, and give each one an owner.
  • Never refund the sender for a payout whose outcome is still unknown.

How Flominzo handles it

In Flominzo, every adapter result is classified, and UNKNOWN is never a terminal state. It is resolved by a status lookup, settlement evidence, or a recorded human decision, and never by sending the instruction again. The payment keeps the original instruction, each attempt with its idempotency key and provider reference, and the evidence that resolved it. Agents can request a status lookup through the same controlled commands a person would use, but they never resolve an unknown outcome by assumption or override idempotency.

Questions

Should we mark a timed-out payout as failed and refund the sender?

No. A timeout means the outcome is unknown. Refunding the sender while the recipient may have been paid can lose the full amount. Resolve the outcome first, then refund only if the payout is evidenced as failed or returned.

Is an idempotency key enough on its own?

No. It only helps if the provider supports it on that endpoint, still remembers the key, and receives the same key on every retry. Lookup and settlement evidence are still needed to prove what happened.

How long should we wait before escalating an unknown payout?

There is no universal number. Use each provider’s declared callback and settlement windows, and escalate when an attempt outlives them. The window belongs in provider configuration, not in code.

Can an AI agent resolve an unknown payout?

An agent can request a status lookup and summarise the evidence. In Flominzo it cannot resolve an unknown outcome by assumption, resend a payout, or close the exception; a person or an agreed rule does that.

Sources

Let’s make it specific to you.

Bring your systems, payment flows, and questions. We’ll help define the next step.

Talk to the team