Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.lux-core.io/llms.txt

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

Webhooks Guide

Webhooks allow you to receive real-time HTTP notifications when events occur in your LuxCore account, such as when a payment completes or fails.

How Webhooks Work

1

Event Occurs

A payment status changes (e.g., completed, failed)
2

LuxCore Sends Notification

We send an HTTP POST request to your configured endpoint
3

You Process the Event

Your server processes the event and returns a 2xx response
4

Retry if Needed

If delivery fails, we retry with exponential backoff

Setting Up Webhooks

Create a Webhook Endpoint

curl -X POST "https://api.lux-core.io/api/v1/webhooks" \
  -H "X-API-Key: qp_test_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/luxcore",
    "events": ["payment.completed", "payment.failed"],
    "signature_algorithm": "hmac_sha256"
  }'

Response

{
  "success": true,
  "data": {
    "id": "wh_abc123",
    "url": "https://your-server.com/webhooks/luxcore",
    "events": ["payment.completed", "payment.failed"],
    "status": "active",
    "signature_algorithm": "hmac_sha256",
    "secret": "whsec_a1b2c3d4e5f6g7h8i9j0...",
    "created_at": "2025-01-21T10:30:00Z"
  }
}
The webhook secret is only shown once when creating the webhook. Store it securely!

Webhook Events

EventDescription
payment.createdPayment was initialized
payment.completedPayment completed successfully
payment.failedPayment failed
payment.cancelledPayment was cancelled
payment.refundedPayment was refunded
balance.updatedMerchant balance was updated
merchant.updatedMerchant configuration was updated
ticket.createdA new support ticket was created
ticket.updatedSupport ticket status changed (e.g., reopened)
ticket.comment_addedSupport team replied to a ticket
ticket.resolvedSupport ticket was resolved (reserved)
ticket.closedSupport ticket was closed
Withdrawal-type payments (withdrawal, withdrawal_pp) use the same payment.* events as deposits. The payout.* events from the initial release have been merged into the payment.* event namespace.

Webhook Payload

All webhook payloads follow this envelope structure:
{
  "id": "evt_abc123def456",
  "created_at": "2025-01-21T10:35:00Z",
  "merchant_id": 123,
  "data": {
    "payment": {
      "id": "pay_1234567890_abcdefgh",
      "status": "completed",
      "amount": 100050,
      "currency": "ARS",
      "type": "deposit",
      "merchant_id": 123,
      "merchant_reference": "order_123456",
      "method": "bank_transfer",
      "fee": 4500,
      "net_amount": 95550,
      "created_at": "2025-01-21T10:30:00Z",
      "confirmed_at": null,
      "completed_at": "2025-01-21T10:35:00Z",
      "failed_at": null,
      "failure_reason": null,
      "customer_data": {
        "note": "Customer order note",
        "expires_at": "2025-01-21T10:45:00Z",
        "requested_payment_type": "deposit",
        "commission": {
          "currency": "ARS",
          "total_minor": 4500
        }
      },
      "processing_time_ms": 300000
    }
  }
}
Field name differences: The webhook payment object uses different field names than the payment creation API response. Key differences:
  • Webhook: id → API response: transaction_id
  • Webhook: fee → API response: fee_amount
  • Webhook: net_amount → present in both (same name)
Make sure your webhook handler uses the correct field names from the webhook payload.

Payment Fields Reference

FieldTypeDescription
idstringUnique payment identifier
statusstringCurrent payment status (completed, failed, cancelled, expired, refunded, partial_refund)
amountintegerTotal payment amount in minor units (centavos). Example: 100050 = 1000.50 ARS
currencystringISO 4217 currency code (ARS, AUD, MXN, UYU)
typestringPayment type: deposit, withdrawal, deposit_pp, or withdrawal_pp
merchant_idintegerYour merchant ID
merchant_referencestringYour unique reference passed when creating the payment
methodstring | nullPayment method code (bank_transfer, spei, crypto, payid). Nullable if method not yet assigned
feeintegerCommission amount in minor units. Already calculated based on your merchant fee rate
net_amountintegerAmount you receive (deposit) or total debited (withdrawal), in minor units
created_atstringPayment creation timestamp (ISO 8601)
confirmed_atstring | nullWhen payment was confirmed by the customer
completed_atstring | nullWhen payment reached terminal completed status
failed_atstring | nullWhen payment failed (if applicable)
failure_reasonstring | nullHuman-readable failure description
customer_dataobjectWhitelisted metadata (see below)
processing_time_msintegerTime from creation to completion in milliseconds. Only present for completed events
refundobject | nullRefund details. Only present for payment.refunded events

Amount, Fee, and Net Amount

All monetary values are integers in minor units (centavos). The fee is automatically calculated based on your merchant commission rate.
Formula: net_amount = amount - feeDeposit example (customer pays 1000.50 ARS, merchant fee 4.5%):
  • amount: 100050 — the amount the customer paid
  • fee: 4502 — commission charged (100050 × 4.5%)
  • net_amount: 95548 — amount credited to your balance
Withdrawal example (payout of 500.00 ARS, merchant fee 5%):
  • amount: 50000 — the payout amount sent to the recipient
  • fee: 2500 — commission charged (50000 × 5%)
  • net_amount: 47500 — total debited from your balance: payout amount minus fee

Withdrawal Payload Example

For withdrawals, the payload includes payout details inside customer_data:
{
  "id": "evt_def456ghi789",
  "created_at": "2025-01-21T11:05:00Z",
  "merchant_id": 123,
  "data": {
    "payment": {
      "id": "pay_1234567890_abcdefgh",
      "status": "completed",
      "amount": 50000,
      "currency": "ARS",
      "type": "withdrawal",
      "merchant_id": 123,
      "merchant_reference": "payout_789",
      "method": "bank_transfer",
      "fee": 2500,
      "net_amount": 47500,
      "created_at": "2025-01-21T11:00:00Z",
      "confirmed_at": null,
      "completed_at": "2025-01-21T11:05:00Z",
      "failed_at": null,
      "failure_reason": null,
      "customer_data": {
        "note": "Withdrawal request",
        "requested_payment_type": "withdrawal",
        "payout": {
          "bank_code": "BBVA",
          "bank_account": "0000003100010000000001",
          "recipient_name": "Juan Perez",
          "bank_name": "BBVA Argentina"
        },
        "commission": {
          "currency": "ARS",
          "total_minor": 2500
        }
      },
      "processing_time_ms": 300000
    }
  }
}

customer_data Fields

The customer_data object uses a whitelist approach — only specific fields are included:
FieldTypeDescription
notestringCustomer or merchant note
customer_emailstringCustomer email (if provided)
customer_phonestringCustomer phone (if provided)
customer_namestringCustomer name (if provided)
external_idstringExternal reference ID
referencestringPayment reference
descriptionstringPayment description
expires_atstringPayment expiration time (ISO 8601)
requested_payment_typestringOriginal requested type (deposit or withdrawal)
commissionobjectCommission details: currency, total_minor
payoutobjectOnly for withdrawals: bank_code, bank_account, recipient_name, bank_name, bsb, payid, payid_type
Internal processing data (IP addresses, internal IDs, processing metadata) is never included in webhook payloads for security reasons.

Webhook Headers

Each webhook request includes these headers:
HeaderDescription
X-Webhook-TimestampUnix timestamp when signature was created
X-Webhook-SignatureHMAC-SHA256 signature for verification
X-Webhook-IdUnique identifier for this webhook delivery
X-Quadpay-Secret-VersionSecret version used for signing
Content-TypeAlways application/json
The X-Webhook-Event and X-Webhook-Retry headers are only sent for in-request webhooks (created via the webhook_url field in payment creation). They are not included in standard admin-configured webhook deliveries.

Signature Verification

Always verify webhook signatures to ensure requests are from LuxCore.

Signature Format

X-Webhook-Signature: hmac_sha256=<hmac_signature>

Verification Algorithm

payload_to_sign = `${timestamp}.${raw_request_body}`
expected_signature = HMAC_SHA256(webhook_secret, payload_to_sign)
Important: Always compute the HMAC on the raw request body bytes exactly as received, not on re-serialized JSON. Re-serializing (e.g., JSON.stringify(parsedObject)) can change key ordering, whitespace, or Unicode escaping, causing signature mismatches.

Implementation Examples

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, timestamp, secret) {
  // Reject old timestamps (> 5 minutes)
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - timestamp) > 300) {
    throw new Error('Timestamp too old');
  }

  // Calculate expected signature using raw body (not re-serialized JSON)
  const payloadToSign = `${timestamp}.${rawBody}`;
  const expectedSignature = 'hmac_sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payloadToSign)
    .digest('hex');

  // Compare signatures
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )) {
    throw new Error('Invalid signature');
  }

  return true;
}

// Capture raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => { req.rawBody = buf.toString(); }
}));

app.post('/webhooks/luxcore', (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const timestamp = parseInt(req.headers['x-webhook-timestamp']);
  const webhookId = req.headers['x-webhook-id'];

  try {
    // Verify using raw body (not re-serialized JSON)
    verifyWebhookSignature(req.rawBody, signature, timestamp, WEBHOOK_SECRET);

    // Process the event — use envelope structure
    const event = req.headers['x-webhook-event'];
    const payment = req.body.data.payment;
    console.log(`Received ${event} for payment ${payment.id} (webhook: ${webhookId})`);

    // Handle specific events
    switch (event) {
      case 'payment.completed':
        // Update order status, send confirmation email, etc.
        break;
      case 'payment.failed':
        // Handle failed payment
        break;
    }

    res.status(200).json({ received: true });
  } catch (error) {
    console.error('Webhook error:', error.message);
    res.status(400).json({ error: error.message });
  }
});

Replay Protection

In addition to signature verification, implement these measures to prevent replay attacks:
  1. Check timestamp: Reject webhooks with timestamps older than 5 minutes (already shown in verification examples above)
  2. Store webhook IDs: Save the X-Webhook-Id header value and reject duplicates. This prevents replayed webhooks within the timestamp window
  3. Distinguish retries from replays: Legitimate retries from LuxCore will have the same X-Webhook-Id. Only process each unique webhook ID once
The combination of timestamp validation and webhook ID deduplication provides strong replay protection.

Retry Policy

If webhook delivery fails (non-2xx response or timeout), we retry with exponential backoff and full jitter:
  • Default retries: 3 attempts after the initial delivery (configurable per webhook)
  • Backoff formula: Random delay between 0 and 2^attempt x base_delay, with a maximum cap of 24 hours
  • Timeout: Each delivery attempt times out after 30 seconds
AttemptMax Delay
InitialImmediate
Retry 1~2 minutes
Retry 2~4 minutes
Retry 3~8 minutes
Actual delays are randomized (full jitter) to prevent thundering herd. The values above are maximums.
After all retry attempts are exhausted, the webhook event is marked as failed. You can manually retry failed events via the API.

Testing Webhooks

Send a test event to verify your endpoint:
curl -X POST "https://api.lux-core.io/api/v1/webhooks/wh_abc123/test" \
  -H "X-API-Key: qp_test_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "payment.completed"
  }'

Managing Webhooks

List Webhooks

curl -X GET "https://api.lux-core.io/api/v1/webhooks" \
  -H "X-API-Key: qp_test_sk_your_key"

Update Webhook

curl -X PUT "https://api.lux-core.io/api/v1/webhooks/wh_abc123" \
  -H "X-API-Key: qp_test_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["payment.completed", "payment.failed", "payment.cancelled"]
  }'

Delete Webhook

curl -X DELETE "https://api.lux-core.io/api/v1/webhooks/wh_abc123" \
  -H "X-API-Key: qp_test_sk_your_key"

In-Request Webhooks

Instead of pre-configuring webhooks via the API, you can pass a webhook_url directly in the payment creation request. This is useful when you want per-payment notification routing or a simpler integration without managing webhook endpoints.

How It Works

  1. Include webhook_url in your POST /payments request
  2. LuxCore automatically creates (or reuses) a webhook endpoint for your merchant
  3. Events are delivered to this URL in addition to any admin-configured webhooks
  4. The webhook is signed using your merchant’s default webhook secret

Example

curl -X POST "https://api.lux-core.io/api/v1/payments" \
  -H "X-API-Key: qp_test_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 150000,
    "currency": "ARS",
    "method": "bank_transfer",
    "type": "deposit",
    "merchant_reference": "order_abc123",
    "customer": {
      "name": "Maria Garcia",
      "email": "maria@example.com"
    },
    "webhook_url": "https://your-server.com/webhooks/order_abc123",
    "webhook_events": ["payment.completed", "payment.failed"]
  }'

Default Events

If webhook_events is not provided, the following events are subscribed by default:
  • payment.created
  • payment.completed
  • payment.failed
  • payment.refunded

Webhook Secret

In-request webhooks are signed using your merchant’s default webhook secret. This secret is:
  • Automatically generated the first time you use webhook_url
  • Shared across all in-request webhooks for your merchant
  • Visible in the merchant settings panel of the backoffice
  • Used for HMAC-SHA256 signature verification (same algorithm as admin webhooks)
Use the same signature verification logic for in-request webhooks as for admin webhooks. The only difference is the signing secret — in-request webhooks use your merchant’s default secret instead of the per-webhook secret.

Deduplication

If you send the same webhook_url across multiple payments, the system automatically reuses the existing webhook configuration. This means:
  • No duplicate webhook endpoints are created
  • Event subscriptions from the first request are preserved
  • The same secret key is used for all deliveries to that URL

Differences from Admin Webhooks

FeatureAdmin WebhooksIn-Request Webhooks
Created viaPOST /webhooks APIwebhook_url field in POST /payments
Secret keyPer-webhook (custom or generated)Merchant default secret (shared)
Listed in GET /webhooksYes (by default)No (use ?source=in_request to view)
EventsConfigurable per-webhookConfigurable per-request (or defaults)
DeduplicationManual (create once, reuse)Automatic (same URL = same webhook)

Best Practices

Always Verify Signatures

Never process webhooks without verifying the signature first

Respond Quickly

Return 200 immediately, process events asynchronously

Handle Duplicates

Use payment ID + status_version for deduplication and ordering

Log Everything

Log webhook payloads for debugging and audit trails

Ordering and Deduplication

Webhooks are delivered at-least-once and may arrive out of order. Each webhook payload includes a status_version field that increments with every payment status change. Recommended approach:
  1. Store the last processed status_version per payment ID
  2. Ignore webhooks where status_version <= stored_version (stale or duplicate)
  3. Process only webhooks with a higher status_version
{
  "id": "evt_abc123",
  "data": {
    "payment": {
      "id": "pay_xyz789",
      "status": "completed",
      "status_version": 3,
      "amount": 500.00,
      "currency": "MXN"
    }
  }
}
Webhook endpoints must be publicly accessible HTTPS URLs. Self-signed certificates are not supported.