Skip to content

Webhooks

Configure a webhook URL in the dashboard to receive real-time HTTP POST notifications when invoice status changes. The API settings page lets you turn individual webhook events on or off.

Events

EventTrigger
invoice_createdInvoice was created
invoice_partially_paidSome payment was detected, but total detected funds are below the required amount
invoice_payment_detectedSufficient payment was detected, but observed funds have not reached the configured confirmation threshold
invoice_confirmedSufficient detected funds reached the configured confirmation threshold
invoice_expiredFinal payable deadline elapsed before sufficient payment was detected
invoice_voidedInvoice was voided before payment was detected
invoice_payment_recordedExternal payment was recorded for the invoice
invoice_exception_openedAn exception was opened for merchant review
invoice_exception_closedAn open exception was closed by the merchant

Payload

All webhook payloads have the same shape. The invoice object contains the latest invoice state at the time the event is sent.

json
{
  "event": "invoice_payment_detected",
  "invoice": {
    "id": "a1b2c3d4-...",
    "amountUsd": 49.99,
    "status": "payment_detected",
    "paymentCoverage": "exact_payment",
    "receivedAmountUsd": 49.99,
    "confirmedAmountUsd": 0,
    "confirmations": 0,
    "paymentDetectedAt": "2026-04-29T10:05:00.000Z",
    "confirmedAt": null,
    "paidOutOfBandAt": null,
    "paidOutOfBandNote": null,
    "defaultPaymentOptionId": "payment-option-default",
    "lastPaymentOptionId": "payment-option-1",
    "paymentSummary": {
      "paymentCoverage": "exact_payment",
      "receivedAmountUsd": 49.99,
      "confirmedAmountUsd": 0,
      "remainingAmountUsd": 0,
      "overpaymentAmountUsd": 0
    },
    "lastTransactionHash": "def456...",
    "exceptionType": null,
    "exceptionStatus": null,
    "exceptionAction": null,
    "exceptionNote": null,
    "exceptionClosedAt": null,
    "metadata": { "orderId": "order_123" }
  }
}

Webhook payloads do not include full payment instructions. Use lastPaymentOptionId to identify the latest observed payment option for the event, and fetch the invoice from your server if you need the latest full invoice detail.

Processing Webhooks

Use webhooks as a notification that invoice state changed. After verifying the signature, fetch the latest invoice from your server before making fulfillment decisions:

bash
curl https://fincobra.com/api/checkout/invoices/a1b2c3d4-1111-4222-8333-abcdefabcdef \
  -H "X-Api-Key: fc_live_..."

This keeps your order logic correct if events are retried, delayed, or delivered close together.

Webhook delivery is at-least-once. Your endpoint may receive the same event more than once. Events for the same invoice are delivered independently, so do not rely on receiving every intermediate status before a later status.

Webhook payloads do not include a separate webhook event ID or event timestamp. For idempotency, use the invoice ID together with the event name, current invoice status, relevant invoice timestamps, and lastTransactionHash when present.

Recommended order handling:

Invoice stateRecommended action
confirmedMark the order paid if it is not already paid.
paid_out_of_bandMark the order paid and store the payment note for reconciliation.
payment_detectedKeep the order pending unless your business accepts detected-but-unconfirmed payment.
partially_paidKeep the order pending and review paymentSummary.remainingAmountUsd.
expiredStop accepting payment for this invoice and prompt the customer to create a new checkout.
voidedStop accepting payment for this invoice.

If paymentCoverage is overpayment, the invoice can still be confirmed. Overpayment does not open an exception; use paymentSummary.overpaymentAmountUsd for your own operations flow.

Exceptions are opened only for partial_payment and late_payment. A payment first observed at or before paymentTiming.payableUntilAt is on time; a payment first observed after that timestamp is late. Late payments keep the invoice expired or voided until the merchant accepts the late payment. Closing an exception records exceptionStatus: "closed", exceptionAction, exceptionNote, and exceptionClosedAt on merchant-facing invoice and webhook payloads.

confirmedAmountUsd is the USD amount that has reached the confirmation threshold. Use payments[] on the invoice detail response when you need payment-level asset amounts, USD credit, transaction hash, and confirmation details.

Signature verification

Create a webhook signing secret in the dashboard before relying on webhook delivery. Every webhook request includes an X-Checkout-Signature header containing an HMAC-SHA256 hex digest of the raw JSON body, signed with your webhook secret. Copy the current webhook secret from the dashboard when it is created or rotated.

To verify:

javascript
import crypto from 'node:crypto';

function verifySignature(body, signature, secret) {
  if (typeof signature !== 'string') {
    return false;
  }

  if (!/^[0-9a-f]{64}$/i.test(signature)) {
    return false;
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  const signatureBuffer = Buffer.from(signature, 'hex');
  const expectedBuffer = Buffer.from(expected, 'hex');

  return (
    signatureBuffer.length === expectedBuffer.length &&
    crypto.timingSafeEqual(signatureBuffer, expectedBuffer)
  );
}

// In your webhook handler:
const body = req.rawBody; // raw JSON string
const signature = req.headers['x-checkout-signature'];
const webhookSecret = 'your-webhook-secret'; // store the dashboard secret server-side

if (!verifySignature(body, signature, webhookSecret)) {
  return res.status(401).send('Invalid signature');
}
python
import hmac
import hashlib

def verify_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

TIP

Your webhook signing secret is only shown when it is created or rotated. Store it securely and never expose it publicly.

Retry policy

If your endpoint returns a non-2xx status or the request times out (10 seconds), the webhook is retried:

AttemptDelay after failure
1st retry1 minute
2nd retry5 minutes
3rd retry15 minutes

After 3 failed attempts, the webhook is permanently marked as failed and not retried again. Each invoice event is delivered independently.

Use the dashboard Webhooks page to search delivery records by delivery ID, invoice ID, event, URL, or HTTP response code. Filter by status or event when reviewing failed deliveries.

Best practices

  • Respond quickly — return a 2xx status within 10 seconds. Process the event asynchronously if needed.
  • Verify signatures — always validate the X-Checkout-Signature header before processing.
  • Fetch current state — after verification, read GET /api/checkout/invoices/:id from your server and make decisions from the current invoice.
  • Handle duplicates — make order updates idempotent. Store processed transitions using the invoice ID, event name, status, relevant timestamps, and transaction hash when present.
  • Use HTTPS — your webhook URL should use HTTPS in production.