Skip to main content

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_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_id": "123",
    "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",
    "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

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
    }
  }
}

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-EventEvent type (e.g., payment.completed)
X-Webhook-TimestampUnix timestamp when signature was created
X-Webhook-SignatureHMAC-SHA256 signature for verification
X-Webhook-IdUnique identifier for this webhook delivery
X-Webhook-RetryAlways present. true if this is a retry attempt, false otherwise
Content-TypeAlways application/json

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}.${JSON.stringify(body)}`
expected_signature = HMAC_SHA256(webhook_secret, payload_to_sign)

Implementation Examples

const crypto = require('crypto');

function verifyWebhookSignature(payload, 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
  const payloadToSign = `${timestamp}.${JSON.stringify(payload)}`;
  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;
}

// Express.js example
app.post('/webhooks/luxcore', express.json(), (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 {
    verifyWebhookSignature(req.body, 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 });
  }
});

Retry Policy

If webhook delivery fails (non-2xx response or timeout), we retry with exponential backoff using the formula min(2^n × 60s, 3600s):
AttemptDelay
1Immediate
22 minutes
34 minutes
48 minutes
516 minutes
After 5 failed attempts (maxAttempts=5), the webhook 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_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_your_key"

Update Webhook

curl -X PUT "https://api.lux-core.io/api/v1/webhooks/wh_abc123" \
  -H "X-API-Key: qp_test_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_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_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 for idempotency - you may receive the same event twice

Log Everything

Log webhook payloads for debugging and audit trails
Webhook endpoints must be publicly accessible HTTPS URLs. Self-signed certificates are not supported.