Guides

Stablecoin API Webhooks: Signatures, Duplicates, Reconciliation

Stablecoin API webhooks are signed callbacks sent on every status change. How to verify them, survive retries and duplicates, and reconcile against the API.

Caio Barbosa

Founder & CO-CEO

Forbes Under 30. One of the leading voices in Fintech & Crypto in Brazil. Writes weekly about stablecoins, payments, and the future of financial infrastructure in Latin America.

Cover image for Lumx blog article: Stablecoin API Webhooks: Signatures, Duplicates, Reconciliation
Cover image for Lumx blog article: Stablecoin API Webhooks: Signatures, Duplicates, Reconciliation

Stablecoin API webhooks are signed HTTP requests that a payments API sends to your server whenever a transaction, customer or account changes status, so your system learns about a deposit or payout without polling. Each request carries an id, a timestamp and a signature that your code must verify against the raw body before acting.

This post covers the receiving side. What the API exposes is in what a stablecoin API is, and the order in which to build the rest is in how to integrate a stablecoin API. Everything said about Lumx below was read on docs.lumx.io on November 1, 2026.

What does a stablecoin API webhook carry?

A webhook is a POST to an endpoint you register. The body is a JSON envelope, and three headers let you prove who sent it and when.

Envelope and headers on a Lumx webhook, from the Lumx webhooks documentation.

Item

Where

What it holds

eventId

body

Unique identifier of the event (UUID)

eventType

body

The event, for example onramp.success

data

body

The resource as of the change, with a shape that varies by event type

webhook-id

header

Unique message identifier

webhook-timestamp

header

Unix timestamp, in seconds, of when the message was sent

webhook-signature

header

Base64 signature or signatures, space-delimited, each prefixed with a version

With Lumx, a project admin or owner registers the endpoint in the Dashboard under Developers > Webhooks and chooses specific event types or all of them. If the endpoint sits behind HTTP Basic authentication, put the credentials in the endpoint URL and Lumx sends them in the Authorization header on every request. Deliveries come from three published IP addresses, shared by sandbox and production, for teams that allowlist.

How do you verify a webhook signature?

Five steps, in this order:

  1. Capture the raw request body before any JSON middleware touches it.

  2. Build the signed content by joining the webhook-id header, the webhook-timestamp header and the raw body with dots.

  3. Compute HMAC-SHA256 over that string with your signing secret as the key, and base64-encode the result.

  4. Compare your value to every signature in the webhook-signature header in constant time, and accept the request if any one matches.

  5. Reject the request if the timestamp is too far from your own clock, so a captured valid call cannot be replayed at you later.

Most failures are not a wrong secret. They look like one, which is what makes them expensive.

Four causes of "signature invalid", cheapest to check first.

Symptom

Cause

Fix

Every event fails

The body was parsed and re-serialized, so the bytes differ

Capture the raw bytes before the JSON parser runs

Every event fails

The secret was used as it arrived, prefix included

Strip the prefix and base64-decode the rest before using it as the key

Fails for about a day after a rotation, only on some events

Code reads only the first signature in the header

Loop over all signatures and accept any valid one

Fails on a few valid events

Your clock is skewed against the timestamp window

Log the computed skew before you reject

Lumx follows this scheme exactly. The signing secret starts with whsec_, and you strip that prefix and base64-decode the remainder to get the HMAC key. When you rotate the secret, Lumx signs with both the old and the new one for 24 hours, so the header can read v1 followed by the old signature, a space, then v1 followed by the new one. The documented example rejects any message whose timestamp is more than five minutes from the current time, and its examples are published in TypeScript, JavaScript, Python and Go. The Integrate webhooks prompt turns the same rules into instructions for a coding agent, and the Debug webhooks prompt walks the failure causes above in order when events stop verifying.

Which events does a stablecoin API send?

One event per status change, grouped by the resource that changed. A payments API usually has a family for each thing a business wants to track: money in, money out, wallet-to-wallet movement, the customer's verification, the accounts that receive deposits, and the destinations that receive payouts. Read the status in the payload rather than inferring it from the event name, and take the list from the provider's own page, because event catalogs grow. Two families report an RFI (request for information, a compliance hold that asks for a document before something clears).

Lumx event families, from the Lumx webhooks documentation as read on November 1, 2026.

Family

Events

Statuses it reports

onramp.*

7

awaiting_funds, transferring_fiat, trading, transferring_stablecoin, success, failed, expired

offramp.*

5

transferring_stablecoin, trading, transferring_fiat, success, failed

transfer.*

3

transferring_stablecoin, success, failed

customer.*

5

created, under_verification, approved, rfi, final_rejection

customer.limit_request.*

4

created, approved, partially_approved, rejected

account.*

7

awaiting_onboarding, requested, provisioning, rfi, active, rejected, inactive

destinations.*

3

under_verification, approved, final_rejection

That is 34 event types. The RFI events are the ones a product team forgets to route, because nothing has failed yet: the customer or account is waiting on you to collect a document. A handler that only listens for success and failed leaves those users waiting with no message.

For Lumx, the on-ramp progresses from awaiting_funds through transferring_fiat, trading and transferring_stablecoin to success. The off-ramp starts at transferring_stablecoin, because the customer's stablecoin leaves first, then trading and transferring_fiat, then success. A transfer between wallets has only two steps. Each transaction event carries the transaction id, the customer id, the type, the original request, the current state, any metadata you attached, and creation and update times.

Retries and duplicates: the eight-attempt schedule

Delivery is at least once. If your endpoint does not return a 2xx response, the provider tries again, and a handler that does the work and then times out before answering gets the same event again. A teammate who resends a failed message from the dashboard produces the same effect. None of this is a bug, and the fix is the same each time: deduplicate on the message id, which stays the same across attempts of one message.

What most guides leave out is the schedule, and the schedule decides how long an outage can last before events are lost. Lumx publishes it.

Lumx webhook delivery attempts, from the Lumx webhooks documentation.

Attempt

Delay

1

Immediately

2

5 seconds

3

5 minutes

4

30 minutes

5

2 hours

6

5 hours

7

10 hours

8

10 hours

After the eighth attempt Lumx marks the message as failed, and you can retry failed messages manually from the Dashboard. Two things follow. A deploy that returns errors for a few minutes is absorbed by attempts two and three without any action from you. An outage that outlasts the whole schedule is not, so the sweep in the reconciliation section below is what closes that gap. For deduplication, the Lumx documentation names the webhook-id header as the key to use.

Duplicates and out-of-order events

Retries mean an older status can land after a newer one. A retried onramp.trading event can reach you after onramp.success has already been processed, and a handler that writes whatever it receives will move a settled deposit back to trading. Lumx's own debugging guide treats duplicates and out-of-order delivery as expected, so the defence belongs in your code.

The rules are short. Treat each success and failure event, plus onramp.expired, as terminal and ignore any later event for the same transaction. Before you credit a customer, release goods or post to a ledger, read the transaction from the API and act on what it says rather than on arrival order. The payload tells you something changed. The API tells you what is true now.

I would build the receiver as two pieces that never share a code path: a thin endpoint that verifies the signature, stores the raw event and returns 2xx, and a worker that reads the stored events and calls the API. I prefer this because the failure you most want to survive is your own deploy, and an endpoint that does real work before answering turns every slow downstream call into a retry. I would also refuse to put the signature check behind a flag in any environment, sandbox included, because the day the flag defaults the wrong way is the day an unsigned request moves a balance.

On the write side, the same duplicate problem is handled with request keys rather than message ids, which idempotency keys on a stablecoin API covers.

Webhooks are the fast path, the API is the record

Polling every payment every minute wastes requests and still reacts late, because payments settle on their own clocks. Webhooks fix the timing. They do not fix the record, because a message can be dropped by an outage that outlasts the retry schedule, by a bug in your handler, or by a deploy that swallowed an event. So run both, with different jobs: the webhook triggers an update, and a scheduled sweep through the API confirms that your ledger agrees with the provider.

Three habits make the sweep cheap:

  • Put your own reference in the transaction metadata when you create it. The join to your books then exists before you need it, and rows created without it are the ones reconciled by hand.

  • Match on ids, never on amounts. Two payouts of the same amount to the same person on the same day are normal.

  • Read the receipt, not the request. The request is what you asked for, and the receipt is what happened, including the rate and the fees.

With Lumx, the sweep reads the list of transactions, which takes a page size of up to 100, a cursor for the next page, and a customer id. Walk the pages per customer, compare each row's status and update time with your ledger, and treat any row that differs as an event you missed. Handle a 429 TOO_MANY_REQUESTS response with exponential backoff. The on-ramp success payload carries the receipt: the base and final exchange rate, the Lumx and partner fees, the final amount and the transaction hash with a block explorer link. For an off-ramp, Lumx considers the transaction complete once the funds leave its banking partner, so a payout can read as success while the recipient's bank still holds the credit, and your ledger needs a state for sent but not yet credited. The accounting side of the same job is in reconciling stablecoin transactions in accounting, and what each status means per rail is in payout statuses and tracking by rail.

When webhooks are the wrong tool

Webhooks are the wrong tool when you need an answer inside the request that asks the question. The create call already returns the transaction, and waiting for an event to learn that it exists adds latency and a failure mode for nothing.

They are also the wrong tool when the receiving side cannot be reached from the internet or cannot answer quickly. A nightly batch job behind a firewall is better served by the list call on a schedule than by a tunnel kept open for callbacks.

And they are the wrong source of truth when money is at stake. The message is a notification. A system that credits balances from the payload alone has outsourced its ledger to a retry queue.

What a Brazil on-ramp looks like with Lumx webhooks

A business that collects from customers in Brazil asks Lumx for an on-ramp over Pix (Brazil's instant payment system, run by the Central Bank), and the customer pays from their own bank app. The payment window depends on the rate type: a floating rate on Pix expires after one day, and a deposit that lands after expiry is not processed and is refunded to the sender on the same rail. Your endpoint hears about each step: onramp.awaiting_funds when the payment details are ready to show, onramp.transferring_fiat once the deposit is seen, then onramp.trading and onramp.transferring_stablecoin, and finally onramp.success with the receipt. If nobody pays in time you receive onramp.expired, and your product should release whatever it was holding for that order.

Lumx is stablecoin payments infrastructure for businesses that move money between Latin America and the rest of the world: one API to collect, hold, convert, and pay out in BRL, MXN, COP, USD, EUR, and GBP or in USDC and USDT, over local rails such as PIX, SPEI, PSE, Bre-B, ACH, FEDWIRE, SEPA, and Faster Payments, with SWIFT and on-behalf-of payments and collections (POBO and COBO) in USD, EUR, and GBP, plus named virtual accounts, custodial wallets, and KYB/KYC built in.

The same endpoint serves the rest of the integration. When the account that receives deposits is a named virtual account (a local account number issued in the customer's own name), its provisioning shows up as account. events, and a compliance hold shows up as account.rfi or customer.rfi instead of a silent delay. The stablecoin from a finished on-ramp lands in a custodial wallet, and a payout from that wallet raises offramp. events on the way to the recipient's bank. How the whole flow fits together is in global payments, and the account type behind the deposit is in named virtual accounts.

Methodology and sources

Product facts were read on November 1, 2026 from docs.lumx.io: the webhooks page (event catalog, headers, signing, rotation, retry schedule, IP allowlisting, Basic authentication), the transactions concept page (lifecycles, payment expiration, late deposits), the errors page (429 and the error shape), the payment rail cut-off page (when an off-ramp counts as complete), the environments page (sandbox), the read-all-transactions reference (page size, cursor, customer filter), and the Debug webhooks and Reconciliation ledger prompt pages. Statements about handling out-of-order events, the two-piece receiver and the daily sweep are recommendations from this post, not guarantees from Lumx.

Verified on November 1, 2026. Operational context, not legal, tax, or investment advice.

Cover photo: LEDC on Unsplash.

  • What are stablecoin API webhooks?

    They are signed HTTP requests that a payments API sends to your server whenever a transaction, customer or account changes status, so you do not have to poll. You verify each one before acting on it, and you deduplicate because the same event can arrive more than once. At Lumx you register the endpoint under Developers > Webhooks in the Dashboard and choose the event types, and every request carries the webhook-id, webhook-timestamp and webhook-signature headers.

  • How do I verify a stablecoin API webhook signature?

    Join the message id, the timestamp and the raw request body with dots, compute HMAC-SHA256 with the signing secret, base64-encode the result and compare it in constant time against every signature in the header. Use the raw bytes, never a re-serialized body. At Lumx the secret starts with whsec_, so you strip the prefix and base64-decode the rest, and during a rotation the header carries the old and the new signature for 24 hours.

  • Why do I receive the same webhook more than once?

    Delivery is at least once. If your endpoint does not return a 2xx response, the provider retries, and a message resent from a dashboard looks the same as the original. Deduplicate on the message id. Lumx makes up to eight delivery attempts, starting immediately and then after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours and two waits of 10 hours, and the Lumx documentation names the webhook-id header as the deduplication key.

  • Do I still need to poll the API if I use webhooks?

    Yes, once in a while. Webhooks are the fast path, and a scheduled sweep through the API is the safety net for events that arrived during an outage or were dropped by a handler bug. With Lumx, the sweep reads the transactions list by page and cursor for each customer, compares status and update time with your ledger, and reads a single transaction before you credit anything.

  • Can I test stablecoin API webhooks before going live?

    Yes, if the provider's sandbox sends real signed events. Lumx runs a separate sandbox at api-sandbox.lumx.io with test stablecoins and mock banking partners, no real money moves, and the same webhook scheme applies, so point an endpoint at a tunnel, run a simulated deposit and confirm that verification, retries and your terminal-state rules hold before production.

Stay up to date with what Lumx is developing.

Sign up to receive them by email.

Share on socials:

stablecoin-api-webhooks

A

stablecoin-api-webhooks

Stablecoin API Webhooks: Signatures, Duplicates, Reconciliation

Copy link

Copied!

stablecoin-api-webhooks

TALK TO OUR TEAM

Ready to transform your business with stablecoins?

Discover how our infrastructure can seamlessly integrate stablecoins into your financial operations quickly, securely, and efficiently.

Guides

In this page

©2026. All rights reserved.

Lumx US OP LLC ("Lumx") is a financial technology and payments infrastructure company. Lumx US OP LLC is a Money Service Business (MSB) registered with the Financial Crimes Enforcement Network (FinCEN) (MSB #31000316459619). Lumx is not a state-licensed money transmitter and does not, in its own capacity, engage in the provision of regulated money transmission services. All such regulated activities are conducted exclusively through, and under the licenses of, duly authorized financial-institution partners.


Lumx is not a bank, financial institution, payment institution, or custodian of client funds. Certain services made available through the Platform may be provided by duly authorized and regulated third-party partners, in accordance with applicable laws and regulations.

Please refer to Lumx’s Terms of Use and Privacy Notice for further information regarding the conditions governing the use of the Platform and the processing of your personal data.