Skip to content

7. Webhook (Order, Subscription, and Card Webhooks)

Infini sends HTTP POST requests to the merchant's pre-configured Webhook address when an order, subscription, or card changes, or when a card authorization challenge is generated. These notifications provide the latest state needed for fulfillment, service activation, card operations, reconciliation, and customer support.

7.1 Subscribable Events and Event Types

Subscription types can be configured in the merchant backend, with different subscription types corresponding to the following events:

Order events:

  • Subscribe to order.create → will receive order.create
  • Subscribe to granular order update events as needed:
    • order.processing
    • order.completed
    • order.expired
    • order.late_payment
  • Existing endpoints that still subscribe to legacy order.update continue to receive the granular order update events above (and payment.failed where applicable). New subscriptions should use the granular event names.

Order Event Type Description:

  • order.create: Order created successfully, entering awaiting payment status.
  • order.processing: Order entering processing (received partial payment or pending on-chain transaction confirmations).
  • order.completed: Order amount satisfied, order status is paid.
  • order.expired: Order not completed within validity period, expired.
  • order.late_payment: Payment received after order expiration.

Automatic refund events:

  • Subscribe to auto_refund.completed → will receive auto_refund.completed
  • Subscribe to auto_refund.unclaimed → will receive auto_refund.unclaimed

Automatic Refund Event Type Description:

  • auto_refund.completed: An automatic refund was successfully sent on-chain.
  • auto_refund.unclaimed: The payer did not submit a refund address before the confirmation deadline; the amount is settled to the merchant instead.

See Automatic Refund for when these events are produced.

Subscription events:

  • Subscribe to subscription.update → will receive:
    • subscription.update
  • Subscribe to subscription.cancel → will receive:
    • subscription.cancel

Subscription Event Type Description:

  • subscription.update: Subscription status updated (e.g. activated after first payment, billing period renewed after successful renewal payment).
  • subscription.cancel: Subscription canceled (by merchant API, user unsubscribe, or system due to non-payment).

Card events:

  • Subscribe to card.status_change → will receive:
    • card.status_change
  • Subscribe to card.transaction → will receive:
    • card.transaction
  • Subscribe to card.challenge → will receive:
    • card.challenge

Card Event Type Description:

  • card.status_change: Current card lifecycle snapshot, including successful activation, suspension, reactivation, and final deletion.
  • card.transaction: Card transaction update, including card application, top-up, redeem, consume, refund, reversal, clearing, and the latest transaction status.
  • card.challenge: One-time authorization code challenge for a card transaction. This event must be subscribed explicitly and cannot be manually resent from the developer console after terminal delivery.

Note:

  • New webhook configurations should subscribe using exact event names. Wildcard subscriptions are not supported by the current webhook management API.
  • Card webhooks are not emitted for personal card activity.
  • card.challenge is delivered only before expires_at; handle it immediately and return HTTP 200 only after recording it safely.

7.1.1 Testing Tool

To inspect incoming webhook requests, you can use Webhook Cool, which provides a unique HTTPS URL for receiving events.

To generate real card transaction events in the Sandbox environment, use the Card Simulator. Enter a card UUID and select one of the supported scenarios:

  • Consumption authorization
  • Authorization failure
  • Full or partial settlement
  • Authorization reversal
  • Refund of a settled transaction

Before running a simulation, configure a Sandbox webhook endpoint and explicitly subscribe it to card.transaction. The simulator triggers the real Sandbox transaction-processing flow; it does not display the merchant callback and does not simulate card.status_change or card.challenge.

7.2 Webhook Payload Field Description

Order Webhook Payload Fields

Webhook request body is in JSON format, including fields:

  • event: Event type (e.g. order.create)
  • order_id: Unique order identifier
  • client_reference: Merchant-side order number (i.e. client_reference)
  • amount: Order payable amount (fiat amount)
  • currency: Order currency (e.g. USD)
  • status: Order status:
    • pending
    • processing
    • paid
    • partial_paid
    • expired
  • amount_confirming: Confirming amount (on-chain transaction exists but has not reached confirmation requirement)
  • amount_confirmed: Confirmed amount (on-chain confirmation completed)
  • created_at: Order creation time (Unix timestamp, seconds)
  • updated_at: Order last update time (Unix timestamp, seconds)
  • exception_tags (if any): Order exception tag array (e.g. ["underpaid", "late"]), see "Core Business Concepts" chapter for details

Note:

  • amount_confirmed + amount_confirming reflects the total payment amount identified on-chain.
  • Order status combined with expiration status, confirmed/confirming amounts constitute the current order semantics.

Automatic Refund Webhook Payload Fields

Automatic refund webhook bodies include the order context plus an auto_refund object:

  • event: auto_refund.completed or auto_refund.unclaimed
  • event_id: Stable event identifier for idempotency
  • order_id / client_reference / amount / currency / status
  • amount_confirmed / amount_confirming
  • refunded_amount: Present on auto_refund.completed (refund principal in original token)
  • payments: Source payments included in this refund group
  • auto_refund:
    • id: Automatic refund ID
    • original_payment_ids: Source payment IDs
    • reason: underpayment / overpayment / late_payment
    • status: completed or unclaimed
    • currency / network
    • refund_amount: Planned refund principal in original token
    • gas_fee: Gas deducted from the refund principal
    • received_amount: Amount the payer receives after gas
    • refund_address / tx_hash: Present when the refund was completed on-chain; null on unclaimed
    • claim_period_days / claim_deadline: Present on auto_refund.unclaimed
    • created_at / completed_at: Present on auto_refund.completed

Subscription Webhook Payload Fields

Subscription webhook request body is in JSON format, including fields:

  • event: Event type (subscription.update or subscription.cancel)
  • subscription_id: System-generated subscription ID
  • merchant_sub_id: Merchant-defined subscription ID
  • plan_name: Subscription plan name
  • trigger_method: Billing trigger method (e.g. invoice)
  • status: Subscription status:
    • pending
    • active
    • canceled
  • currency: Subscription currency (e.g. USD)
  • amount: Recurring amount per billing period
  • interval_unit: Billing interval unit (DAY or MONTH)
  • interval_count: Number of intervals per billing cycle
  • payer_email: Payer email address
  • current_period_start: Current billing period start time (Unix timestamp, seconds)
  • current_period_end: Current billing period end time (Unix timestamp, seconds)
  • subscription_end_at: Subscription termination time (Unix timestamp, seconds)
  • next_invoice_at: Next invoice send time (Unix timestamp, seconds)
  • cancel_reason: Cancel reason (included only when subscription is canceled)
  • canceled_at: Cancel time (Unix timestamp, seconds; included only when subscription is canceled)
  • created_at: Subscription creation time (Unix timestamp, seconds)
  • updated_at: Subscription last update time (Unix timestamp, seconds)

Card Webhook Envelope

Card webhooks use a versioned public envelope:

  • id: Webhook delivery ID, also carried in X-Webhook-Event-Id
  • event: Event type such as card.status_change
  • version: Current public envelope version, currently 1
  • occurred_at: Event occurrence time (Unix timestamp, seconds)
  • data: Event-specific payload

Order and subscription webhooks keep the existing legacy JSON payload shape without the version / data wrapper.

Card Status Change Payload (card.status_change)

The data object includes:

  • card.card_id
  • card.alias (if configured)
  • card.last_four (if available)
  • card.status
  • card.currency

Note:

  • card.status can be init, pending, active, suspend, or deleted.
  • Successful card issuance is represented by card.status_change with card.status: active; there is no separate card.activated event.
  • pending_delete is an internal deletion transition and is not emitted as a standalone card.status_change webhook. During delete flows, wait for deleted or query GET /v2/cards/status.

Example:

{
  "id": "b7ef2c62-6177-4ea8-84ec-3080f2db58f0",
  "event": "card.status_change",
  "version": 1,
  "occurred_at": 1763513200,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    }
  }
}

Card Transaction Payload (card.transaction)

The data object includes:

  • card
  • transaction_id
  • related_transaction_id (for refunds / reversals when available)
  • type: apply_card, top_up, redeem, consume, refund, reversal, or clearing
  • status: pending, authorized, completed, or failed
  • amount
  • fee
  • currency
  • direction: debit, credit, or none
  • transaction_amount and transaction_currency (when the provider supplies the original transaction amount and currency)
  • merchant (if provider merchant information is available)
  • transaction_at
  • settled_at
  • failure (only for failed transactions)
  • auth_settle_adjustment (for a completed consume event when authorization context is available)

All monetary values are decimal strings. Optional fields are omitted when the provider does not supply them.

Transaction types:

typeMeaning
apply_cardCard application or issuance ledger transaction
top_upFunds loaded to the card
redeemFunds redeemed from the card
consumeMerchant purchase authorization or settlement
refundFunds returned for a settled purchase
reversalAuthorization canceled and reserved funds released
clearingProvider clearing transaction or adjustment

Transaction statuses:

statusMeaning
pendingTransaction has been accepted but is not yet authorized or completed
authorizedConsumption authorization succeeded and funds are reserved
completedTransaction reached its final successful state
failedTransaction failed; direction is none and failure is included

One transaction can produce multiple card.transaction webhooks. For example, a consumption normally moves from authorized to completed. Each delivery has a different top-level id and X-Webhook-Event-Id; correlate the updates using data.transaction_id. For refunds and reversals, related_transaction_id identifies the original consumption transaction when it can be resolved.

For a completed consumption, auth_settle_adjustment describes any difference between the amount reserved during authorization and the final settlement:

authorized_total = authorized_amount + authorized_fee
settled_total    = settled_amount + settled_fee
signed_delta     = authorized_total - settled_total
balance_delta    = balance_after - balance_before = signed_delta
  • direction: release: the authorization total was greater than the settlement total, so funds were released.
  • direction: additional_debit: the settlement total was greater than the authorization total, so additional funds were debited.
  • direction: none: the authorization and settlement totals were equal.

Consumption authorized:

{
  "id": "f72f6cd7-1f38-5e96-b60f-f54e56c8db63",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417200,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "8c1db7df-3d4e-44de-b6b3-5ed451809d79",
    "type": "consume",
    "status": "authorized",
    "amount": "10",
    "fee": "0",
    "transaction_amount": "10",
    "currency": "USD",
    "transaction_currency": "USD",
    "direction": "debit",
    "merchant": {
      "name": "Amazon"
    },
    "transaction_at": 1786417200
  }
}

Consumption completed with a lower settlement amount:

{
  "id": "54b1090a-307e-5ec6-9adb-e877415222fd",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417260,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "8c1db7df-3d4e-44de-b6b3-5ed451809d79",
    "type": "consume",
    "status": "completed",
    "amount": "8",
    "fee": "0",
    "transaction_amount": "8",
    "currency": "USD",
    "transaction_currency": "USD",
    "direction": "debit",
    "merchant": {
      "name": "Amazon"
    },
    "transaction_at": 1786417200,
    "settled_at": 1786417260,
    "auth_settle_adjustment": {
      "authorized_amount": "10",
      "authorized_fee": "0",
      "settled_amount": "8",
      "settled_fee": "0",
      "signed_delta": "2",
      "direction": "release",
      "amount": "2",
      "balance_delta": "2",
      "balance_before": "90",
      "balance_after": "92"
    }
  }
}

Authorization failed:

{
  "id": "1ae7c21c-71b4-5fd8-9f12-2a44f94bab41",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417320,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "f14c387d-02d1-41b8-aebb-1c9fda914385",
    "type": "consume",
    "status": "failed",
    "amount": "1000",
    "fee": "0",
    "transaction_amount": "1000",
    "currency": "USD",
    "transaction_currency": "USD",
    "direction": "none",
    "merchant": {
      "name": "Amazon"
    },
    "transaction_at": 1786417320,
    "failure": {
      "reason": "Insufficient balance"
    }
  }
}

Authorization reversed:

{
  "id": "b3ab57ae-18f9-5ab7-8338-1202ea946994",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417380,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "7b9bb337-27f6-4d2a-b474-f91cc26a5e4c",
    "related_transaction_id": "8c1db7df-3d4e-44de-b6b3-5ed451809d79",
    "type": "reversal",
    "status": "completed",
    "amount": "10",
    "fee": "0",
    "currency": "USD",
    "direction": "credit",
    "transaction_at": 1786417380,
    "settled_at": 1786417380
  }
}

Settled transaction refunded:

{
  "id": "ebc55ba6-5607-516e-902c-9400ec44c5bf",
  "event": "card.transaction",
  "version": 1,
  "occurred_at": 1786417440,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "transaction_id": "27dfc395-f703-4b16-90ca-f1cb9d3f1ec8",
    "related_transaction_id": "8c1db7df-3d4e-44de-b6b3-5ed451809d79",
    "type": "refund",
    "status": "completed",
    "amount": "8",
    "fee": "0",
    "currency": "USD",
    "direction": "credit",
    "transaction_at": 1786417440,
    "settled_at": 1786417440
  }
}

Card Challenge Payload (card.challenge)

The data object includes:

  • card
  • challenge_id
  • challenge_type (currently authorization_code)
  • challenge
  • expires_at

Provider-only fields such as provider webhook IDs, provider transaction IDs, merchant routing IDs, encrypted ciphertext, PAN, and CVV are not exposed in the public payload.

Example:

{
  "id": "12e7c9c8-598b-4dc5-b0c2-d7b6b8c3d8a4",
  "event": "card.challenge",
  "version": 1,
  "occurred_at": 1763513400,
  "data": {
    "card": {
      "card_id": "a441831c-a5c7-4bed-8f61-793738afd5bc",
      "alias": "Travel card",
      "last_four": "1234",
      "status": "active",
      "currency": "USD"
    },
    "challenge_id": "challenge-1",
    "challenge_type": "authorization_code",
    "challenge": "123456",
    "expires_at": 1763513700
  }
}

7.3 Webhook Request Headers

When sending Webhooks, Infini includes the following HTTP Headers for security verification and idempotent processing:

  • Content-Type: application/json
  • X-Webhook-Timestamp: Unix timestamp (seconds)
  • X-Webhook-Event-Id: Unique event identifier, used for idempotent deduplication
  • X-Webhook-Signature-Version: Signature version. The current value is v1.
  • X-Webhook-Signature: HMAC-SHA256 signature, used for merchant-side signature verification

It is recommended that merchants use X-Webhook-Event-Id for idempotent processing to avoid duplicate consumption of the same event.

Card webhooks are always signed and require a usable Webhook Secret. Verify the signature against the exact raw request body before parsing JSON.

7.4 Webhook Example Payloads

The following examples show Webhook content in typical scenarios.

7.4.1 Scenario 1: Order Created (order.created)

After order creation, status is pending, waiting for user payment.

{
  "event": "order.create",
  "order_id": "10290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "pending",
  "amount_confirmed": "0",
  "amount_confirming": "0",
  "created_at": 1763512195,
  "updated_at": 1763512195
}

7.4.2 Scenario 2: Order Processing (Payment Received, Confirming, order.processing)

Payment received, but transaction still confirming on blockchain.

{
  "event": "order.processing",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "processing",
  "amount_confirmed": "0",
  "amount_confirming": "0.5",
  "created_at": 1763512349,
  "updated_at": 1763512403
}

7.4.3 Scenario 3: Order Processing (Partial Payment Confirmed, order.processing)

Partial payment has been confirmed on blockchain.

{
  "event": "order.processing",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "processing",
  "amount_confirmed": "0.5",
  "amount_confirming": "0",
  "created_at": 1763512349,
  "updated_at": 1763512453
}

7.4.4 Scenario 4: Order Completed (Full Payment Received, order.completed)

Full payment received and confirmed, order completed.

{
  "event": "order.completed",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "paid",
  "amount_confirmed": "1",
  "amount_confirming": "0",
  "created_at": 1763512349,
  "updated_at": 1763512573,
  "payments": [
    {
      "id": "pay_9c1e2f3a4b5c",
      "method": "crypto_transfer",
      "status": "paid",
      "currency": "USD",
      "amount": "1",
      "network": "ETH",
      "tx_hash": "0xabc123def456...",
      "paid_amount": "1.000000"
    }
  ]
}

7.4.5 Scenario 5: Order Expired (No Payment, order.expired)

Order timeout without receiving any payment.

{
  "event": "order.expired",
  "order_id": "10290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "expired",
  "amount_confirmed": "0",
  "amount_confirming": "0",
  "created_at": 1763512195,
  "updated_at": 1763512255
}

7.4.6 Scenario 6: Order Expired (Partial Payment Received, order.expired + partial_paid)

Order timeout but received partial payment, not reaching order amount.

{
  "event": "order.expired",
  "order_id": "60290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "partial_paid",
  "amount_confirmed": "0.5",
  "amount_confirming": "0",
  "created_at": 1763514565,
  "updated_at": 1763514765
}

7.4.7 Scenario 7: Payment After Order Expiration (Late Payment, order.late_payment)

Payment received within 24 hours after order expiration.

{
  "event": "order.late_payment",
  "order_id": "30290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "",
  "amount": "1",
  "currency": "USD",
  "status": "expired",
  "amount_confirmed": "1",
  "amount_confirming": "0",
  "created_at": 1763512622,
  "updated_at": 1763512815
}

Tips:

  • In late_payment scenarios, order status remains expired, but amount_confirmed has reached the order amount. Merchants can decide whether to ship or refund based on business strategy.
  • It is recommended to make business decisions combined with exception tags (such as late, underpaid, overpaid).
  • If automatic refund is enabled, eligible late / under / over payments may also produce auto_refund.* events later. See Automatic Refund.

7.4.8 Scenario 8: Automatic Refund Completed (auto_refund.completed)

{
  "event": "auto_refund.completed",
  "event_id": "evt_xxx",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "ORDER-10001",
  "amount": "100",
  "currency": "USD",
  "status": "partial_paid",
  "amount_confirmed": "50",
  "amount_confirming": "0",
  "refunded_amount": "50",
  "created_at": 1763512349,
  "updated_at": 1763513573,
  "payments": [
    {
      "id": "pay_8b9c0d1e2f3a",
      "method": "Crypto Transfer",
      "status": "paid",
      "currency": "USD",
      "amount": "30",
      "paid_currency": "USDT",
      "paid_amount": "30",
      "network": "TRON",
      "tx_hash": "4e82a9b1...",
      "created_at": 1763512400,
      "completed_at": 1763512500
    }
  ],
  "auto_refund": {
    "id": "ref_7f8e9d0c1b2a",
    "original_payment_ids": ["pay_8b9c0d1e2f3a"],
    "reason": "underpayment",
    "status": "completed",
    "currency": "USDT",
    "refund_amount": "50",
    "gas_fee": "1",
    "received_amount": "49",
    "network": "TRON",
    "refund_address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "tx_hash": "f91d38ab...",
    "created_at": 1763513400,
    "completed_at": 1763513573
  }
}

7.4.9 Scenario 9: Automatic Refund Unclaimed (auto_refund.unclaimed)

{
  "event": "auto_refund.unclaimed",
  "event_id": "evt_yyy",
  "order_id": "20290d05-8f5c-4ecb-84f0-f78d6f30557f",
  "client_reference": "ORDER-10001",
  "amount": "100",
  "currency": "USD",
  "status": "partial_paid",
  "amount_confirmed": "50",
  "amount_confirming": "0",
  "created_at": 1763512349,
  "updated_at": 1766100000,
  "payments": [
    {
      "id": "pay_8b9c0d1e2f3a",
      "method": "Crypto Transfer",
      "status": "paid",
      "currency": "USD",
      "amount": "50",
      "paid_currency": "USDT",
      "paid_amount": "50",
      "network": "TRON",
      "tx_hash": "4e82a9b1...",
      "created_at": 1763512400,
      "completed_at": 1763512500
    }
  ],
  "auto_refund": {
    "id": "ref_7f8e9d0c1b2a",
    "original_payment_ids": ["pay_8b9c0d1e2f3a"],
    "reason": "underpayment",
    "status": "unclaimed",
    "currency": "USDT",
    "refund_amount": "50",
    "gas_fee": "1",
    "received_amount": "49",
    "network": "TRON",
    "refund_address": null,
    "tx_hash": null,
    "claim_period_days": 30,
    "claim_deadline": 1766100000
  }
}

7.4.10 Scenario 10: Subscription Activated (subscription.update)

After first payment is completed, the subscription transitions from pending to active.

{
  "event": "subscription.update",
  "subscription_id": "sub-9f3c1f2e",
  "merchant_sub_id": "msub_001",
  "plan_name": "Monthly Plan",
  "trigger_method": "invoice",
  "status": "active",
  "currency": "USD",
  "amount": "9.99",
  "interval_unit": "MONTH",
  "interval_count": 1,
  "payer_email": "user@example.com",
  "current_period_start": 1740000000,
  "current_period_end": 1742678400,
  "next_invoice_at": 1742592000,
  "created_at": 1740000000,
  "updated_at": 1740000100
}

7.4.11 Scenario 11: Subscription Renewed (subscription.update)

Renewal payment completed, billing period advanced to the next cycle.

{
  "event": "subscription.update",
  "subscription_id": "sub-9f3c1f2e",
  "merchant_sub_id": "msub_001",
  "plan_name": "Monthly Plan",
  "trigger_method": "invoice",
  "status": "active",
  "currency": "USD",
  "amount": "9.99",
  "interval_unit": "MONTH",
  "interval_count": 1,
  "payer_email": "user@example.com",
  "current_period_start": 1742678400,
  "current_period_end": 1745356800,
  "next_invoice_at": 1745270400,
  "created_at": 1740000000,
  "updated_at": 1742678500
}

7.4.12 Scenario 12: Subscription Canceled (subscription.cancel)

Subscription canceled by merchant API, user unsubscribe, or system due to non-payment.

{
  "event": "subscription.cancel",
  "subscription_id": "sub-9f3c1f2e",
  "merchant_sub_id": "msub_001",
  "plan_name": "Monthly Plan",
  "trigger_method": "invoice",
  "status": "canceled",
  "currency": "USD",
  "amount": "9.99",
  "interval_unit": "MONTH",
  "interval_count": 1,
  "payer_email": "user@example.com",
  "current_period_start": 1742678400,
  "current_period_end": 1745356800,
  "cancel_reason": "by_merchant_api",
  "canceled_at": 1743000000,
  "created_at": 1740000000,
  "updated_at": 1743000000
}

7.5 Receiving Webhook and Security Verification

Infini will initiate POST requests to your configured Webhook URL. It is recommended that the receiving end follows these principles:

  1. Verify that all required Headers exist:
  • X-Webhook-Signature
  • X-Webhook-Timestamp
  • X-Webhook-Event-Id
  1. Verify signature legitimacy (see next section).
  2. Implement idempotent processing based on X-Webhook-Event-Id (process only once).
  3. Business logic should be processed asynchronously, quickly return HTTP 200 to avoid timeout.

7.6 Webhook Signature Verification

Verification steps:

  1. Read from Headers:
  • X-Webhook-Signature (signature)
  • X-Webhook-Timestamp (timestamp)
  • X-Webhook-Event-Id (event ID)
  1. Get raw request body string payload.
  2. Assemble signature content string:
{timestamp}.{event_id}.{payload}
  1. Use your WEBHOOK_SECRET for HMAC-SHA256 calculation:
signed_content = f"{timestamp}.{event_id}.{payload}"
expected_sig = hmac.new(
    WEBHOOK_SECRET.encode(),
    signed_content.encode(),
    hashlib.sha256
).hexdigest()
  1. Compare expected_sig with X-Webhook-Signature for consistency.

7.6.1 Python Verification Example

@app.route('/webhook', methods=['POST'])
def webhook_verification():
    signature = request.headers.get('X-Webhook-Signature')
    timestamp = request.headers.get('X-Webhook-Timestamp')
    event_id = request.headers.get('X-Webhook-Event-Id')

    if not all([signature, timestamp, event_id]):
        return jsonify({"error": "Missing required headers"}), 400

    payload = request.get_data(as_text=True)
    signed_content = f"{timestamp}.{event_id}.{payload}"
    expected_sig = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_content.encode(),
        hashlib.sha256
    ).hexdigest()

    if expected_sig != signature:
        return jsonify({"error": "Invalid signature"}), 401

    # Process valid webhook
    return jsonify({"status": "ok"})

7.7 Webhook Retry Strategy

If the merchant does not return HTTP 200, Infini will retry the event.

  • Maximum retries: 8 times
  • First 3 retry intervals: 30 seconds
  • 4th-8th retries use incremental backoff strategy, example:
AttemptDescriptionInterval (Example)
1stFirst sendImmediate
2nd1st failed30 seconds
3rd2nd failed30 seconds
4th3rd failed30 seconds
5th4th failed60 seconds
6th5th failed120 seconds
7th6th failed240 seconds
8th7th failed480 seconds

If multiple retries still fail, the event will be marked as delivery failed. It is recommended that merchants investigate through logs and reconciliation tools.