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
| Event | Trigger |
|---|---|
invoice_created | Invoice was created |
invoice_partially_paid | Some payment was detected, but total detected funds are below the required amount |
invoice_payment_detected | Sufficient payment was detected, but observed funds have not reached the configured confirmation threshold |
invoice_confirmed | Sufficient detected funds reached the configured confirmation threshold |
invoice_expired | Final payable deadline elapsed before sufficient payment was detected |
invoice_voided | Invoice was voided before payment was detected |
invoice_payment_recorded | External payment was recorded for the invoice |
invoice_exception_opened | An exception was opened for merchant review |
invoice_exception_closed | An 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.
{
"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:
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 state | Recommended action |
|---|---|
confirmed | Mark the order paid if it is not already paid. |
paid_out_of_band | Mark the order paid and store the payment note for reconciliation. |
payment_detected | Keep the order pending unless your business accepts detected-but-unconfirmed payment. |
partially_paid | Keep the order pending and review paymentSummary.remainingAmountUsd. |
expired | Stop accepting payment for this invoice and prompt the customer to create a new checkout. |
voided | Stop 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:
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');
}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:
| Attempt | Delay after failure |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 15 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-Signatureheader before processing. - Fetch current state — after verification, read
GET /api/checkout/invoices/:idfrom 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.