Skip to content

Webhook Integration

Receive real-time notifications when billing events occur — invoices created, payments received, subscriptions changed, and more.

How It Works

  1. You register a webhook endpoint (a URL on your server)
  2. Invora sends HTTP POST requests to your URL when events occur
  3. Each request includes a signature for verification
  4. Your server processes the event and responds with 200 OK

Setting Up a Webhook Endpoint

POST /api/billing/v2/webhook-endpoints
{
  "webhookUrl": "https://your-server.com/webhooks/invora",
  "eventTypes": ["INVOICE_CREATED", "INVOICE_PAYMENT_STATUS_UPDATED", "SUBSCRIPTION_STARTED"],
  "signatureAlgo": "HMAC"
}

Choose which events to receive. You can subscribe to specific event types or use ALL to receive everything.

Signature algorithms: - HMAC — HMAC-SHA256 of the request body. Verify with your endpoint's signing secret. - JWT — RS256-signed JWT. Verify with Invora's public key from the JWKS endpoint.

Event Payload Format

Every webhook delivery contains:

{
  "webhookType": "invoice.created",
  "createdAt": "2026-04-28T14:30:00Z",
  "objectType": "invoice",
  "invoice": {
    "id": "inv_abc123",
    "sequentialId": "42",
    "number": "INV-2026-042",
    "issuingDate": "2026-04-28",
    "invoiceType": "INVOICE_TYPE_SUBSCRIPTION",
    "status": "INVOICE_STATUS_TYPE_FINALIZED",
    "paymentStatus": "INVOICE_PAYMENT_STATUS_TYPE_PENDING",
    "currency": "SAR",
    "totalAmountCents": {"value": "115000"},
    "customer": {
      "id": "cust_xyz",
      "externalId": "your-customer-id",
      "name": "Acme Corp"
    }
  }
}

The payload uses a oneof — exactly one of: invoice, creditNote, subscription, payment, customer, wallet, walletTransaction, fee, paymentRequest, or eventError.

Available Events

Invoice Events

Event When It Fires
INVOICE_CREATED New billing invoice generated
INVOICE_DRAFTED Invoice moved to draft state
INVOICE_GENERATED Invoice PDF ready
INVOICE_ONE_OFF_CREATED One-time invoice created
INVOICE_VOIDED Invoice cancelled
INVOICE_PAYMENT_STATUS_UPDATED Payment status changed (pending → succeeded/failed)
INVOICE_PAYMENT_FAILURE Payment attempt failed
INVOICE_PAYMENT_OVERDUE Invoice past due date
INVOICE_PAYMENT_DISPUTE_LOST Payment dispute resolved against you
INVOICE_PAID_CREDIT_ADDED Credit applied to invoice

Subscription Events

Event When It Fires
SUBSCRIPTION_STARTED New subscription activated
SUBSCRIPTION_TERMINATED Subscription ended
SUBSCRIPTION_UPDATED Plan or terms changed
SUBSCRIPTION_TRIAL_ENDED Free trial expired
SUBSCRIPTION_USAGE_THRESHOLD_REACHED Usage alert triggered

Payment Events

Event When It Fires
PAYMENT_SUCCEEDED Payment confirmed by provider
PAYMENT_REQUIRES_ACTION 3D Secure or additional authentication needed
PAYMENT_RECEIPT_CREATED Payment receipt generated
PAYMENT_REQUEST_CREATED Payment request sent to customer

Customer Events

Event When It Fires
CUSTOMER_CREATED New billing customer added
CUSTOMER_UPDATED Customer details changed
CUSTOMER_CHECKOUT_URL_GENERATED Checkout URL ready for payment setup

Credit Note & Wallet Events

Event When It Fires
CREDIT_NOTE_CREATED Credit note issued
WALLET_CREATED Prepaid wallet set up
WALLET_DEPLETED_ONGOING_BALANCE Wallet balance hit zero
WALLET_TRANSACTION_CREATED Wallet top-up or deduction

Verifying Signatures

Every webhook request includes an X-Invora-Signature header. For HMAC, compute the HMAC-SHA256 of the raw request body with your endpoint's signing secret and compare it against the header.

const crypto = require('crypto');

function verifyWebhook(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
import hmac, hashlib

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

Delivery Guarantees

At-least-once delivery

Events may be delivered more than once during retries. Your endpoint must be idempotent — use webhookType + id + createdAt as the dedup key.

No ordering guarantee

Events may arrive out of order during retries. Do not assume the delivery order matches the order in which events occurred.

A delivery must be acknowledged within a 15-second timeout — respond within 15 seconds or the delivery is marked failed.

Retry Policy

Failed deliveries (non-2xx or timeout) are retried with exponential backoff:

Attempt Delay
1 Immediate
2 30 seconds
3 2 minutes
4 15 minutes
5 1 hour
6 6 hours

After 6 failed attempts, the event is marked FAILED. Retry a specific delivery manually by referencing both the endpoint ID and the delivery ID in the path:

POST /api/billing/v2/webhook-endpoints/{webhookEndpointId}/webhooks/{id}/retry

Managing Endpoints

GET  /api/billing/v2/webhook-endpoints/{id}     — View endpoint details
POST /api/billing/v2/webhook-endpoints/list     — List all endpoints
PUT  /api/billing/v2/webhook-endpoints/{id}     — Update URL or event types
POST /api/billing/v2/webhook-endpoints/delete   — Remove endpoint

Viewing Delivery History

List recent deliveries for an endpoint:

POST /api/billing/v2/webhook-endpoints/{webhookEndpointId}/webhooks/list

Each delivery record includes: HTTP status code, response body, retry count, and timestamps.