Skip to main content
Webhooks notify your application in real time whenever a status change occurs. Instead of polling the API, you receive an HTTP POST request to your endpoint with the updated data.

Setting up webhooks

To configure webhooks, go to Developers > Webhooks in the Dashboard and add your endpoint URL. You can subscribe to specific event types or receive all events.
You must be a project admin or owner to create webhooks.

Event structure

Every webhook event follows a consistent structure with three top-level fields:
{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "eventType": "onramp.success",
  "data": {
    // Event-specific payload
  }
}
FieldTypeDescription
eventIdstringUnique identifier for the event (UUID)
eventTypestringThe event type (e.g., onramp.success, customer.approved)
dataobjectThe event payload, which varies by event type

Available events

Event typeTriggered when
onramp.awaiting_fundsOn-ramp is waiting for the customer to send funds
onramp.transferring_fiatFiat transfer is in progress
onramp.tradingTrade is being executed
onramp.transferring_stablecoinStablecoin transfer to the customer is in progress
onramp.successOn-ramp completed successfully
onramp.failedOn-ramp failed
onramp.expiredOn-ramp expired before payment was received
Event typeTriggered when
offramp.transferring_stablecoinStablecoin transfer from the customer is in progress
offramp.tradingTrade is being executed
offramp.transferring_fiatFiat transfer to the bank account is in progress
offramp.successOff-ramp completed successfully
offramp.failedOff-ramp failed
Event typeTriggered when
transfer.transferring_stablecoinStablecoin transfer is in progress
transfer.successTransfer completed successfully
transfer.failedTransfer failed
Event typeTriggered when
customer.createdCustomer was created
customer.under_verificationCustomer verification is in progress
customer.approvedCustomer was approved
customer.temporary_rejectionCustomer was temporarily rejected (can resubmit)
customer.final_rejectionCustomer was permanently rejected
Event typeTriggered when
bank_account.approvedBank account was approved
bank_account.under_verificationBank account verification is in progress
bank_account.final_rejectionBank account was permanently rejected

Payload examples

{
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "eventType": "onramp.awaiting_funds",
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
    "type": "ON_RAMP",
    "state": {
      "status": "AWAITING_FUNDS",
      "payment": {
        "rail": "PIX",
        "brCode": "00020126580014br.gov.bcb.pix0136123e4567-e89b-12d3-a456-4266141740005204000053039865802BR5910Lumx Test6009Sao Paulo62070503***6304ABCD"
      }
    },
    "metadata": {}
  }
}

Verifying webhook signatures

Each webhook request includes three headers for signature verification:
HeaderDescription
webhook-idUnique message identifier
webhook-timestampUnix timestamp (seconds) of when the message was sent
webhook-signatureBase64-encoded signature(s), space-delimited and prefixed with version
To verify a webhook signature, you construct the signed content, compute the expected signature, and compare it with the header value. The signed content is created by concatenating the webhook-id, webhook-timestamp, and request body, separated by dots:
signed_content = "${webhook_id}.${webhook_timestamp}.${body}"
The signature is computed as a HMAC-SHA256 hash of the signed content using your webhook secret (base64-decoded) as the key, then base64-encoded.
Your webhook signing secret starts with whsec_. You must strip this prefix and base64-decode the remainder before using it as the HMAC key.
When you rotate your signing secret, the system continues signing messages with both the old and new secrets for 24 hours. This means the webhook-signature header may contain multiple signatures (e.g., v1,<old> v1,<new>). Your verification code should accept any valid signature from the list, which the examples below already handle.
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(
  body: string,
  headers: Record<string, string>,
  secret: string
): boolean {
  const msgId = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signature = headers["webhook-signature"];

  if (!msgId || !timestamp || !signature) {
    return false;
  }

  // Reject messages older than 5 minutes to prevent replay attacks
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }

  // Strip the whsec_ prefix and decode the secret
  const secretBytes = Buffer.from(secret.replace("whsec_", ""), "base64");

  // Compute the expected signature
  const signedContent = `${msgId}.${timestamp}.${body}`;
  const expectedSignature = createHmac("sha256", secretBytes)
    .update(signedContent)
    .digest("base64");

  // Compare against all provided signatures (there may be multiple)
  const signaturesV1 = signature
    .split(" ")
    .filter((s) => s.startsWith("v1,"))
    .map((s) => s.substring(3));

  return signaturesV1.some((sig) =>
    timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSignature))
  );
}

Retries

If your endpoint does not return a 2xx response, Lumx retries the delivery with exponential backoff:
AttemptDelay
1Immediately
25 seconds
35 minutes
430 minutes
52 hours
65 hours
710 hours
810 hours
After all retry attempts are exhausted, the message is marked as failed. You can manually retry failed messages from the Dashboard.
Use the webhook-id header to deduplicate events in case your endpoint receives the same event more than once.

IP allowlisting

If your infrastructure requires allowlisting specific IPs, add the following addresses. These IPs are shared across both sandbox and production environments:
35.231.190.127/32
44.214.29.156/32
3.82.0.0/32
100.56.2.161/32