Webhooks
Know the moment money moves
Real-time notifications for pay-ins, payouts, virtual accounts, subscriptions, currency exchange, Wire In and Wire Out, each signed with HMAC-SHA256 so you can verify every request before you act on it.
- HMAC-SHA256 signatures
- Retries with backoff
- Scoped by country and account
- 09:41:02.11409:41:02PayIn100 INPROGRESS20084 ms
- 09:41:05.90209:41:05PayIn200 COMPLETED20091 ms
- 09:42:18.33009:42:18PayOut100 INPROGRESS20077 ms
- 09:42:40.01809:42:40PayOut200 COMPLETED20088 ms
- 09:43:00.55109:43:00VirtualAccount200 COMPLETED20080 ms
POST https://example-merchant.com/webhooks/localpayment
x-Signature: HMAC-SHA256 3f9a1c0e…b47e2b
{ "transactionType": "PayIn", "transactionFlow": "DIRECT",
"data": { "externalId": "order_8F2K-1043",
"status": { "code": "200", "description": "COMPLETED" } } }Configuration
Set up notifications in the Business Portal
No code needed to register an endpoint. Each entry can be scoped by country and account.
- 01
Open notifications
In the Business Portal, go to Configurations → Clients → Notifications.
- 02
Add your HTTPS URL
Register the public HTTPS endpoint on your servers that will receive events.
- 03
Scope the entry
Choose a country, or All, and the account the notifications belong to.
- 04
Add headers and test
Optionally add custom headers for your gateway, then run a test transaction on Stage.
Payload
One envelope for every event
Every notification shares the same root fields, so a single handler can route all of them. The body is JSON, delivered with Content-Type text/plain.
- transactionType: PayIn, PayOut, VirtualAccount, Subscription, CurrencyExchange, WireIn or WireOut
- transactionFlow: the processing flow, useful for routing
- data: the transaction itself, with externalId, internalId and status
Events
What you will be notified about
Card pay-in
- transactionType
- PayIn
- What it tells you
- Approved, completed, rejected, refunded or charged back
Bank transfer and local-method pay-in
- transactionType
- PayIn
- What it tells you
- Final status after the payer confirms in their bank or wallet app
Virtual account
- transactionType
- VirtualAccount
- What it tells you
- Account created, and every payment received
Subscription
- transactionType
- Subscription
- What it tells you
- Subscription lifecycle; each charge also arrives as a PayIn event
Payout
- transactionType
- PayOut
- What it tells you
- Status changes along the payout flow, through to completion
Currency exchange
- transactionType
- CurrencyExchange
- What it tells you
- Result of a conversion between your balances
Wire In
- transactionType
- WireIn
- What it tells you
- Funds credited to your account, in fiat or USDC/USDT
Wire Out
- transactionType
- WireOut
- What it tells you
- Withdrawal sent; crypto Wire Outs include the blockchain transaction hash
Full schemas and status codes for each event are in the webhooks reference at docs.localpayment.com.
Security
Verify every signature
Each request carries x-Signature: HMAC-SHA256 followed by a hex digest of the raw body, computed with your webhook secret. Recompute it and compare before you trust anything in the payload.
- Read the raw body: it is JSON sent as Content-Type text/plain
- Strip the “HMAC-SHA256 ” prefix, then compare with a timing-safe function
- Reject mismatches with 401 and never log the full payload
// Express receiver for Localpayment webhooks (Node.js 18+).
import crypto from 'node:crypto';
import express from 'express';
import { jobQueue } from './queue.js'; // your own async job queue
const app = express();
// Load the webhook secret from your secrets manager at boot. Never hardcode it.
const WEBHOOK_SECRET = process.env.LP_WEBHOOK_SECRET;
if (!WEBHOOK_SECRET) throw new Error('LP_WEBHOOK_SECRET is not configured');
function hasValidSignature(rawBody, header) {
if (typeof rawBody !== 'string') return false;
// Header format: "x-Signature: HMAC-SHA256 <hex>". Strip the prefix first.
const received = String(header ?? '').replace(/^HMAC-SHA256\s+/i, '').trim();
if (!/^[0-9a-f]{64}$/i.test(received)) return false;
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody, 'utf8').digest();
return crypto.timingSafeEqual(Buffer.from(received, 'hex'), expected);
}
// The body is JSON sent as Content-Type text/plain: keep the raw text for the HMAC.
app.post('/webhooks/localpayment', express.text({ type: '*/*', limit: '1mb' }), (req, res) => {
if (!hasValidSignature(req.body, req.get('x-signature'))) return res.sendStatus(401);
let event;
try {
event = JSON.parse(req.body);
} catch {
return res.sendStatus(400);
}
res.sendStatus(200); // acknowledge fast, before any heavy work
// Process asynchronously. Deduplicate on internalId/externalId + status
// so a retried delivery is applied only once. Never log the full payload.
jobQueue.enqueue(event);
});Reliable processing
Built for retries and real-world networks
Acknowledge fast
Return 200 as soon as the signature checks out, and do the heavy work in a background job.
Expect retries
If we do not get a 200, we retry with exponential backoff. Your handler must be safe to run more than once.
Deduplicate
Key processing on internalId or externalId plus status, so a retried delivery is applied only once.
Do not assume order
Compare the incoming status with what you have stored before applying it, and never move a transaction backwards.
Confirm with the API
When in doubt, query GET /api/transactions/{externalId}/status for the current state of a transaction.
Log safely
Log event types, IDs and statuses, never full payloads, tokens or personal data.
Operations
See every delivery in the Business Portal
Notification reports show what was sent to your endpoints, so support and engineering can resolve issues without digging through server logs.
- Delivery log of the notifications sent to your endpoints
- Transaction search and detailed activity reports
- Webhooks, IP allow-list and users managed in one place
Overview
USD
1,284,560.00
+4.2%
BRL
3,420,118.40
+12.8%
MXN
8,905,332.10
+6.1%
- Pay-in · Pixorder_8F2K-1043+ R$ 1,250.00
- Payout · SPEIseller_0921− MX$ 18,400.00
- FX · BRL → USDfx_3b7q250,000.00 USD
- Pay-in · Bre-Border_9A1C-2210+ COP 890,000
Webhooks FAQ
How do I know a webhook really came from Localpayment?
Verify the x-Signature header: recompute the HMAC-SHA256 of the raw body with your webhook secret and compare them with a timing-safe function. You can also add custom headers to each notification entry for your gateway.
How often are failed deliveries retried?
Deliveries that do not receive a 200 are retried with exponential backoff. Design your handler for several attempts and check the notification reports in the Business Portal for delivery history.
Why is the Content-Type text/plain?
The body is JSON sent as text/plain. Read it as raw text, verify the signature on those exact bytes, then parse it.
Can I send different countries to different endpoints?
Yes. Each notification entry is scoped by country (or All) and account, so you can route markets or business units to separate URLs.
What should my endpoint return?
A 200 status, as quickly as possible and before any long-running work. Without a 200, the delivery is treated as failed and retried.
Wire up your first webhook
Configure a Stage endpoint, trigger a test pay-in and verify the signature end to end.

HMAC-SHA256 signatures