Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.lumx.io/llms.txt

Use this file to discover all available pages before exploring further.

Webhooks notify your application whenever a resource changes status. 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 destination 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.rfiCustomer requires additional information (can resubmit)
customer.final_rejectionCustomer was permanently rejected
Event typeTriggered when
account.provisioningAccount is being provisioned and verified
account.rfiAdditional information is required to continue verification
account.activeAccount is verified and can receive funds
account.closedAccount was permanently closed
Event typeTriggered when
destinations.under_verificationDestination verification is in progress
destinations.approvedDestination was approved
destinations.final_rejectionDestination 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",
    "request": {
      "customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
      "rail": "PIX",
      "sourceCurrency": "BRL",
      "sourceAmount": "10000.00",
      "targetCurrency": "USDC",
      "purpose": "PERSONAL_ACCOUNT"
    },
    "state": {
      "status": "AWAITING_FUNDS",
      "payment": {
        "rail": "PIX",
        "brCode": "00020126580014br.gov.bcb.pix0136123e4567-e89b-12d3-a456-4266141740005204000053039865802BR5910Lumx Test6009Sao Paulo62070503***6304ABCD"
      }
    },
    "metadata": {},
    "createdAt": "2024-03-20T15:30:00Z",
    "updatedAt": "2024-03-20T15:30:05Z"
  }
}

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, Lumx 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, Lumx marks the message 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:
44.214.29.156/32
3.82.0.0/32
100.56.2.161/32

Basic authentication

If your endpoint requires HTTP Basic authentication, include the credentials directly in the endpoint URL:
https://username:password@your-domain.com/webhook
Lumx extracts the credentials and sends them in the Authorization header on every webhook request:
Authorization: Basic <base64(username:password)>