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
}
}
| Field | Type | Description |
|---|---|---|
eventId | string | Unique identifier for the event (UUID) |
eventType | string | The event type (e.g., onramp.success, customer.approved) |
data | object | The event payload, which varies by event type |
Available events
On-ramp events
On-ramp events
| Event type | Triggered when |
|---|---|
onramp.awaiting_funds | On-ramp is waiting for the customer to send funds |
onramp.transferring_fiat | Fiat transfer is in progress |
onramp.trading | Trade is being executed |
onramp.transferring_stablecoin | Stablecoin transfer to the customer is in progress |
onramp.success | On-ramp completed successfully |
onramp.failed | On-ramp failed |
onramp.expired | On-ramp expired before payment was received |
Off-ramp events
Off-ramp events
| Event type | Triggered when |
|---|---|
offramp.transferring_stablecoin | Stablecoin transfer from the customer is in progress |
offramp.trading | Trade is being executed |
offramp.transferring_fiat | Fiat transfer to the destination is in progress |
offramp.success | Off-ramp completed successfully |
offramp.failed | Off-ramp failed |
Transfer events
Transfer events
| Event type | Triggered when |
|---|---|
transfer.transferring_stablecoin | Stablecoin transfer is in progress |
transfer.success | Transfer completed successfully |
transfer.failed | Transfer failed |
Customer events
Customer events
| Event type | Triggered when |
|---|---|
customer.created | Customer was created |
customer.under_verification | Customer verification is in progress |
customer.approved | Customer was approved |
customer.rfi | Customer requires additional information (can resubmit) |
customer.final_rejection | Customer was permanently rejected |
Account events
Account events
| Event type | Triggered when |
|---|---|
account.provisioning | Account is being provisioned and verified |
account.rfi | Additional information is required to continue verification |
account.active | Account is verified and can receive funds |
account.closed | Account was permanently closed |
Destination events
Destination events
| Event type | Triggered when |
|---|---|
destinations.under_verification | Destination verification is in progress |
destinations.approved | Destination was approved |
destinations.final_rejection | Destination was permanently rejected |
Payload examples
- On-ramp
- Off-ramp
- Transfer
- Customer
- Account
- Destination
- awaiting_funds
- transferring_fiat
- trading
- transferring_stablecoin
- success
- failed
- expired
{
"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"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "onramp.transferring_fiat",
"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": "TRANSFERRING_FIAT",
"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"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "onramp.trading",
"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": "TRADING",
"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"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "onramp.transferring_stablecoin",
"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": "TRANSFERRING_STABLECOIN",
"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"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "onramp.success",
"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": "SUCCESS",
"payment": {
"rail": "PIX",
"brCode": "00020126580014br.gov.bcb.pix0136123e4567-e89b-12d3-a456-4266141740005204000053039865802BR5910Lumx Test6009Sao Paulo62070503***6304ABCD"
},
"receipt": {
"sourceCurrency": "BRL",
"sourceAmount": "10000.00",
"targetCurrency": "USDC",
"baseExchangeRate": "5.25",
"baseTargetAmount": "1904.76",
"fees": {
"lumx": {
"rate": "0.005",
"flatAmount": "0.00",
"totalAmount": "9.52",
"currency": "USDC"
},
"partner": {
"rate": "0.002",
"flatAmount": "0.00",
"totalAmount": "3.81",
"currency": "USDC"
}
},
"finalTargetAmount": "1891.43",
"finalExchangeRate": "5.29",
"transactionHash": "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1",
"blockExplorerUrl": "https://polygonscan.com/tx/0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1"
}
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "onramp.failed",
"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": "FAILED",
"payment": {
"rail": "PIX",
"brCode": "00020126580014br.gov.bcb.pix0136123e4567-e89b-12d3-a456-4266141740005204000053039865802BR5910Lumx Test6009Sao Paulo62070503***6304ABCD"
},
"receipt": {
"error": {
"message": "Your transaction has failed. Please contact support."
}
}
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "onramp.expired",
"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": "EXPIRED",
"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"
}
}
- transferring_stablecoin
- trading
- transferring_fiat
- success
- failed
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "offramp.transferring_stablecoin",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"destinationId": "ba123456-e89b-12d3-a456-426614174000",
"type": "OFF_RAMP",
"request": {
"destinationId": "e80d3137-eddd-4791-b9b8-6e36b289f284",
"sourceCurrency": "USDC",
"sourceAmount": "1000.00",
"purpose": "PERSONAL_ACCOUNT"
},
"state": {
"status": "TRANSFERRING_STABLECOIN"
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "offramp.trading",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"destinationId": "ba123456-e89b-12d3-a456-426614174000",
"type": "OFF_RAMP",
"request": {
"destinationId": "e80d3137-eddd-4791-b9b8-6e36b289f284",
"sourceCurrency": "USDC",
"sourceAmount": "1000.00",
"purpose": "PERSONAL_ACCOUNT"
},
"state": {
"status": "TRADING"
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "offramp.transferring_fiat",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"destinationId": "ba123456-e89b-12d3-a456-426614174000",
"type": "OFF_RAMP",
"request": {
"destinationId": "e80d3137-eddd-4791-b9b8-6e36b289f284",
"sourceCurrency": "USDC",
"sourceAmount": "1000.00",
"purpose": "PERSONAL_ACCOUNT"
},
"state": {
"status": "TRANSFERRING_FIAT"
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "offramp.success",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"destinationId": "ba123456-e89b-12d3-a456-426614174000",
"type": "OFF_RAMP",
"request": {
"destinationId": "e80d3137-eddd-4791-b9b8-6e36b289f284",
"sourceCurrency": "USDC",
"sourceAmount": "1000.00",
"purpose": "PERSONAL_ACCOUNT"
},
"state": {
"status": "SUCCESS",
"receipt": {
"sourceCurrency": "USDC",
"sourceAmount": "1904.76",
"targetCurrency": "BRL",
"baseExchangeRate": "5.25",
"baseTargetAmount": "10000.00",
"fees": {
"lumx": {
"rate": "0.005",
"flatAmount": "0.00",
"totalAmount": "9.52",
"currency": "USDC"
},
"partner": {
"rate": "0.002",
"flatAmount": "0.00",
"totalAmount": "3.81",
"currency": "USDC"
}
},
"finalTargetAmount": "9900.00",
"finalExchangeRate": "5.20",
"transactionHash": "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1",
"blockExplorerUrl": "https://polygonscan.com/tx/0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1"
}
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "offramp.failed",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"destinationId": "ba123456-e89b-12d3-a456-426614174000",
"type": "OFF_RAMP",
"request": {
"destinationId": "e80d3137-eddd-4791-b9b8-6e36b289f284",
"sourceCurrency": "USDC",
"sourceAmount": "1000.00",
"purpose": "PERSONAL_ACCOUNT"
},
"state": {
"status": "FAILED",
"receipt": {
"error": {
"message": "Your transaction has failed. Please contact support."
}
}
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
- transferring_stablecoin
- success
- failed
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "transfer.transferring_stablecoin",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "TRANSFER",
"request": {
"currency": "USDC",
"from": "123e4567-e89b-12d3-a456-426614174001",
"to": "123e4567-e89b-12d3-a456-426614174002",
"amount": "1000.000000"
},
"state": {
"status": "TRANSFERRING_STABLECOIN"
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "transfer.success",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "TRANSFER",
"request": {
"currency": "USDC",
"from": "123e4567-e89b-12d3-a456-426614174001",
"to": "123e4567-e89b-12d3-a456-426614174002",
"amount": "1000.000000"
},
"state": {
"status": "SUCCESS",
"receipt": {
"transactionHash": "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1",
"blockExplorerUrl": "https://polygonscan.com/tx/0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1"
}
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "transfer.failed",
"data": {
"id": "123e4567-e89b-12d3-a456-426614174000",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"type": "TRANSFER",
"request": {
"currency": "USDC",
"from": "123e4567-e89b-12d3-a456-426614174001",
"to": "123e4567-e89b-12d3-a456-426614174002",
"amount": "1000.000000"
},
"state": {
"status": "FAILED",
"receipt": {
"error": {
"message": "Your transaction has failed. Please contact support."
}
}
},
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
- created
- under_verification
- approved
- rfi
- final_rejection
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "customer.created",
"data": {
"id": "7f3e1a2b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "created",
"level": "BASIC",
"associatedParties": [
{
"id": "f0e9d8c7-b6a5-4f3e-2d1c-0b9a8f7e6d5c",
"associatedPartyId": "c1d2e3f4-a5b6-7c8d-9e0f-a1b2c3d4e5f6",
"roles": ["SHAREHOLDER"],
"status": "approved",
"level": "BASIC"
}
],
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "customer.under_verification",
"data": {
"id": "7f3e1a2b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "under_verification",
"level": "BASIC",
"associatedParties": [
{
"id": "f0e9d8c7-b6a5-4f3e-2d1c-0b9a8f7e6d5c",
"associatedPartyId": "c1d2e3f4-a5b6-7c8d-9e0f-a1b2c3d4e5f6",
"roles": ["SHAREHOLDER"],
"status": "approved",
"level": "BASIC"
}
],
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "customer.approved",
"data": {
"id": "7f3e1a2b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "approved",
"level": "BASIC",
"associatedParties": [
{
"id": "f0e9d8c7-b6a5-4f3e-2d1c-0b9a8f7e6d5c",
"associatedPartyId": "c1d2e3f4-a5b6-7c8d-9e0f-a1b2c3d4e5f6",
"roles": ["SHAREHOLDER"],
"status": "approved",
"level": "BASIC"
}
],
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "customer.rfi",
"data": {
"id": "7f3e1a2b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "rfi",
"level": "BASIC",
"statusReason": {
"rejectLabels": ["DOCUMENT_QUALITY"],
"rejectSubLabels": ["DOCUMENT_BLURRY"],
"documents": [
{
"type": "PASSPORT",
"status": "DECLINED",
"comment": "Document is blurry and illegible",
"rejectLabels": ["DOCUMENT_QUALITY"]
}
]
},
"associatedParties": [
{
"id": "f0e9d8c7-b6a5-4f3e-2d1c-0b9a8f7e6d5c",
"associatedPartyId": "c1d2e3f4-a5b6-7c8d-9e0f-a1b2c3d4e5f6",
"roles": ["SHAREHOLDER"],
"status": "rfi",
"level": "BASIC",
"statusReason": {
"rejectLabels": ["DOCUMENT_QUALITY"],
"rejectSubLabels": ["DOCUMENT_BLURRY"]
}
}
],
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "customer.final_rejection",
"data": {
"id": "7f3e1a2b-4c5d-6e7f-8a9b-0c1d2e3f4a5b",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "final_rejection",
"level": "BASIC",
"statusReason": {
"rejectLabels": ["FRAUDULENT_PATTERNS"],
"rejectSubLabels": ["MULTIPLE_SUBMISSIONS"]
},
"associatedParties": [
{
"id": "f0e9d8c7-b6a5-4f3e-2d1c-0b9a8f7e6d5c",
"associatedPartyId": "c1d2e3f4-a5b6-7c8d-9e0f-a1b2c3d4e5f6",
"roles": ["SHAREHOLDER"],
"status": "rfi",
"level": "BASIC",
"statusReason": {
"rejectLabels": ["DOCUMENT_QUALITY"],
"rejectSubLabels": ["DOCUMENT_BLURRY"]
}
}
],
"metadata": {},
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
- provisioning
- rfi
- active
- closed
{
"eventId": "d26c479f-f786-4be8-a694-c4da0e8393e7",
"eventType": "account.provisioning",
"data": {
"id": "ce4c89f3-60f2-45a3-9de7-415a462dd95c",
"customerId": "96fec1e2-78d9-4978-813d-674bba64020d",
"status": "PROVISIONING",
"currency": "USD",
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "account.rfi",
"data": {
"id": "ce4c89f3-60f2-45a3-9de7-415a462dd95c",
"customerId": "96fec1e2-78d9-4978-813d-674bba64020d",
"status": "RFI",
"currency": "USD",
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "81d783f1-3675-4f3a-92d1-e4f5af66cca3",
"eventType": "account.active",
"data": {
"id": "d06d342c-629a-4d99-a26f-34e8ea528403",
"customerId": "768d6aac-627e-44e8-aabd-d51d10f59c02",
"status": "ACTIVE",
"currency": "EUR",
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "account.closed",
"data": {
"id": "d06d342c-629a-4d99-a26f-34e8ea528403",
"customerId": "768d6aac-627e-44e8-aabd-d51d10f59c02",
"status": "CLOSED",
"currency": "EUR",
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
- under_verification
- approved
- final_rejection
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "destinations.under_verification",
"data": {
"id": "ba123456-e89b-12d3-a456-426614174000",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "UNDER_VERIFICATION",
"level": "BASIC",
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "destinations.approved",
"data": {
"id": "ba123456-e89b-12d3-a456-426614174000",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "APPROVED",
"level": "BASIC",
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
{
"eventId": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "destinations.final_rejection",
"data": {
"id": "ba123456-e89b-12d3-a456-426614174000",
"customerId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"status": "FINAL_REJECTION",
"level": "BASIC",
"createdAt": "2024-03-20T15:30:00Z",
"updatedAt": "2024-03-20T15:30:05Z"
}
}
Verifying webhook signatures
Each webhook request includes three headers for signature verification:| Header | Description |
|---|---|
webhook-id | Unique message identifier |
webhook-timestamp | Unix timestamp (seconds) of when the message was sent |
webhook-signature | Base64-encoded signature(s), space-delimited and prefixed with version |
webhook-id, webhook-timestamp, and request body, separated by dots:
signed_content = "${webhook_id}.${webhook_timestamp}.${body}"
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.- TypeScript
- JavaScript
- Python
- Go
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))
);
}
const { createHmac, timingSafeEqual } = require("crypto");
function verifyWebhook(body, headers, secret) {
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))
);
}
import base64
import hashlib
import hmac
import time
def verify_webhook(
body: str, headers: dict[str, str], secret: str
) -> bool:
msg_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signature = headers.get("webhook-signature")
if not msg_id or not timestamp or not signature:
return False
# Reject messages older than 5 minutes to prevent replay attacks
now = int(time.time())
if abs(now - int(timestamp)) > 300:
return False
# Strip the whsec_ prefix and decode the secret
secret_bytes = base64.b64decode(secret.removeprefix("whsec_"))
# Compute the expected signature
signed_content = f"{msg_id}.{timestamp}.{body}"
expected_signature = base64.b64encode(
hmac.new(
secret_bytes, signed_content.encode(), hashlib.sha256
).digest()
).decode()
# Compare against all provided signatures (there may be multiple)
signatures_v1 = [
s.removeprefix("v1,")
for s in signature.split(" ")
if s.startswith("v1,")
]
return any(
hmac.compare_digest(sig, expected_signature)
for sig in signatures_v1
)
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
)
func VerifyWebhook(body string, headers http.Header, secret string) bool {
msgID := headers.Get("webhook-id")
timestamp := headers.Get("webhook-timestamp")
signature := headers.Get("webhook-signature")
if msgID == "" || timestamp == "" || signature == "" {
return false
}
// Reject messages older than 5 minutes to prevent replay attacks
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
now := time.Now().Unix()
if math.Abs(float64(now-ts)) > 300 {
return false
}
// Strip the whsec_ prefix and decode the secret
secretBytes, err := base64.StdEncoding.DecodeString(
strings.TrimPrefix(secret, "whsec_"),
)
if err != nil {
return false
}
// Compute the expected signature
signedContent := fmt.Sprintf("%s.%s.%s", msgID, timestamp, body)
mac := hmac.New(sha256.New, secretBytes)
mac.Write([]byte(signedContent))
expectedSignature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
// Compare against all provided signatures (there may be multiple)
for _, sig := range strings.Split(signature, " ") {
if strings.HasPrefix(sig, "v1,") {
if hmac.Equal(
[]byte(strings.TrimPrefix(sig, "v1,")),
[]byte(expectedSignature),
) {
return true
}
}
}
return false
}
Retries
If your endpoint does not return a2xx response, Lumx retries the delivery with exponential backoff:
| 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 |
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
Authorization header on every webhook request:
Authorization: Basic <base64(username:password)>