Your server sends a payout. The connection drops before the response arrives. Did the payout go through? If your code simply retries, and the first attempt did go through, your customer gets paid twice and your treasury is short.
Valtu solves this with one field: externalId.
How it works
POST /v1/transfers and POST /v1/wallets are idempotent by externalId. Send the same request again with the same externalId and you get the original object back. No second wallet is created, and no second payout is sent.
If you reuse an externalId with different details, say a different amount or destination, the API answers 409 DUPLICATE instead of guessing which one you meant.
const transfer = await valtu.transfers.create({
walletId: payouts.id,
asset: "USDC-ERC20",
amount,
to: customer.address,
externalId: `withdrawal_${withdrawal.id}`, // your own id
});
The pattern
- Create your payout record first, with its id, and save it.
- Call Valtu with that id as
externalId. - On a timeout or a 5xx, retry with the same
externalId. You'll get either the payout that was already created, or a new one if the first attempt never arrived. Never both. - Track the status by webhook, and reconcile with
GET /v1/transfers?after=….
GET requests are always safe to retry. Each attempt needs a fresh timestamp and nonce, because every request is signed.
Batches
POST /v1/transfers/batch takes up to 100 payouts, each with its own required externalId. A batch isn't all-or-nothing: each payout is created or refused on its own, and the response says which. Sending the same batch again after a timeout returns the payouts already created, so the retry is safe here too.
A related safeguard
A payout that can't be sent within 30 minutes, because the wallet is short of funds or gas, is cancelled before signing. You won't find a stale payout going out hours later, when you no longer expect it.
More in the API guide: idempotency and retries and batch payouts.