Skip to content

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

Configuration

Set up notifications in the Business Portal

No code needed to register an endpoint. Each entry can be scoped by country and account.

  1. 01

    Open notifications

    In the Business Portal, go to Configurations → Clients → Notifications.

  2. 02

    Add your HTTPS URL

    Register the public HTTPS endpoint on your servers that will receive events.

  3. 03

    Scope the entry

    Choose a country, or All, and the account the notifications belong to.

  4. 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
webhook.js
// 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

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.

How can we help?

Download your copy

Leave your details and get the full edition instantly.