Idempotency
Every write operation carries an externalId: an identifier you generate, unique per
logical operation, up to 100 characters. It is what allows LB Pay to recognise that two
requests are the same request, and to settle the money only once.
Getting this wrong is the single most expensive mistake in a payments integration, because the failure mode is a duplicate payment — and duplicates are discovered by reconciliation, hours later, after the money is gone.
The rule
externalId, reused on every retryGenerate the UUID once per operation, persist it before sending the request, and
send that same externalId on every retry of that operation.
A withdrawal of R$ 100 to a given player is one operation. It has one externalId for its
entire lifetime, no matter how many times the network fails while you try to submit it. A
different withdrawal — even same player, same amount, five minutes later — is a different
operation, and gets a new UUID.
Why reusing the key is the safe behaviour
When a request times out, you do not know whether the server processed it. There are only two possible strategies, and they are not equivalent:
Reusing the key is what makes the retry safe. Generating a new UUID on retry discards the protection entirely and converts an ordinary network blip into a double payment.
This matches how idempotency works across the payments industry — the key identifies the intent, and repeating the intent must not repeat the effect.
Implementation
1. Persist before you send. Write the operation to your database with its externalId
and a PENDING state first, then call the API. If your process dies mid-request, the
record tells you which externalId to reuse when you recover.
2. Never derive the key from mutable data. A key built from
player_id + amount + date collides the moment a player legitimately withdraws the same
amount twice in a day. Use a random UUID v4.
3. Never reuse a key for a different operation. The key is bound to the operation it was created for. Reusing it elsewhere means the second operation silently returns the first one's result and never executes.
4. Do not put sensitive data in the key. It appears in logs and support tickets. A CPF or an email address does not belong there.
When you are unsure what happened
If a request fails without a clear answer — timeout, connection reset, 503 — do not
guess and do not roll back. Query the operation by its externalId:
curl --request GET \
--url "https://api.sdb.lbpay.com.br/v1/pix/withdraw/3f2b1c4e-9a7d-4e21-b8f0-5c6d7e8a9b01" \
--header "Authorization: Bearer $TOKEN"
The full decision procedure is in Retries and timeouts.
Credit exactly once
The same rule applies in the other direction. LB Pay retries webhook delivery when it does
not receive a timely 2xx, which is what stops an outage on your side from losing a payment
notification — and it means the same event will sometimes arrive twice.
Deduplicate on endToEndId, and enforce it as a uniqueness constraint in your database,
not as a check in application code: two concurrent deliveries both read "not processed yet"
and both credit. Let the constraint reject the second one — insert first, apply the effect
only if the insert succeeded, and commit the two together.
Related
- Retries and timeouts — what to do after a failed request
- Webhooks — delivery rules on the receiving side
- Withdrawals — where duplicates cost the most