Webhook Payloads
Every webhook delivery is a POST request with a JSON body containing the event details and enriched resource data.
Request headers
| Header | Description | Example |
|---|---|---|
Content-Type | Always application/json | application/json |
X-Webhook-ID | Unique delivery ID (changes on each retry) | f47ac10b-58cc-4372-a567-0e02b2c3d479 |
X-Webhook-Event-ID | Unique event ID (same across retries, use for deduplication) | evt_80e0a4bcb0534c21967f50c54602433e |
X-Webhook-Timestamp | Unix timestamp in seconds | 1712000382 |
X-Webhook-Signature | HMAC-SHA256 signature (hex-encoded) | a1b2c3d4e5f6... |
X-Webhook-Event | Event type | invoice.paid |
User-Agent | Identifies the sender | OnlineInvoiceMaker-Webhooks/1.0 |
Payload structure
All events share the same top-level structure:
{
"id": "evt_80e0a4bcb0534c21967f50c54602433e",
"type": "invoice.created",
"created_at": "2026-04-01T10:30:00.000Z",
"business_id": "cb21efb1-fa40-434f-a1d3-e17c0bdb9aa6",
"data": {
// Event-specific data (see below)
}
}
| Field | Type | Description |
|---|---|---|
id | string | Unique event ID prefixed with evt_. Use for deduplication |
type | string | The event type |
created_at | string (ISO 8601) | When the event occurred |
business_id | string (UUID) | The business that owns the event |
data | object | Full enriched data for the resource |
Invoice event payload
Events: invoice.created, invoice.updated, invoice.sent, invoice.paid, invoice.overdue, invoice.cancelled, invoice.deleted
{
"id": "evt_80e0a4bcb0534c21967f50c54602433e",
"type": "invoice.paid",
"created_at": "2026-04-01T12:00:00.000Z",
"business_id": "cb21efb1-fa40-434f-a1d3-e17c0bdb9aa6",
"data": {
"id": "dad9a150-8bb6-4764-9a22-ac8b83734e24",
"invoice_number": "INV-00001",
"total": 3109.88,
"status": "paid",
"previous_status": "sent",
"invoice": {
"id": "dad9a150-8bb6-4764-9a22-ac8b83734e24",
"invoice_number": "INV-00001",
"status": "paid",
"issue_date": "2026-04-01",
"due_date": "2026-05-01",
"currency": "USD",
"subtotal": 2859.88,
"tax_amount": 250.00,
"discount_amount": 0,
"total": 3109.88,
"amount_paid": 3109.88,
"notes": "Thank you for your business!",
"terms": null,
"template_type": "modern",
"created_at": "2026-04-01T10:30:00.000Z",
"updated_at": "2026-04-01T12:00:00.000Z"
},
"items": [
{
"id": "a1b2c3d4-...",
"description": "Website Design",
"quantity": 1,
"unit_price": 2500,
"tax_rate": 10,
"tax_amount": 250.00,
"amount": 2750.00,
"product_id": null
},
{
"id": "e5f6a7b8-...",
"description": "Hosting (12 months)",
"quantity": 12,
"unit_price": 29.99,
"tax_rate": 0,
"tax_amount": 0,
"amount": 359.88,
"product_id": null
}
],
"customer": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Acme Corp",
"email": "billing@acme.com",
"phone": "+1-555-0100",
"address": "123 Business Ave",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US",
"tax_id": "US-12345678"
},
"business": {
"id": "cb21efb1-fa40-434f-a1d3-e17c0bdb9aa6",
"name": "My Company LLC",
"business_type": "LLC",
"phone": "+1-555-0200",
"address": "456 Commerce St",
"country": "US",
"currency": "USD",
"tax_id": "EIN-98765432",
"logo_url": "https://...",
"website": "https://mycompany.com"
}
}
}
Quotation event payload
Events: quotation.created, quotation.updated, quotation.sent, quotation.accepted, quotation.rejected, quotation.expired, quotation.converted
{
"id": "evt_...",
"type": "quotation.accepted",
"created_at": "2026-04-01T14:00:00.000Z",
"business_id": "cb21efb1-...",
"data": {
"id": "7a8b9c0d-...",
"quotation_number": "QT-00001",
"total": 5900.00,
"status": "accepted",
"quotation": {
"id": "7a8b9c0d-...",
"quotation_number": "QT-00001",
"status": "accepted",
"issue_date": "2026-04-01",
"valid_until": "2026-05-01",
"currency": "USD",
"subtotal": 5400.00,
"tax_amount": 500.00,
"discount_amount": 100.00,
"total": 5900.00,
"notes": null,
"terms": null,
"template_type": "modern",
"created_at": "2026-04-01T10:30:00.000Z",
"updated_at": "2026-04-01T14:00:00.000Z"
},
"items": [
{
"id": "...",
"description": "Full Brand Identity Package",
"quantity": 1,
"unit_price": 5000,
"tax_rate": 10,
"tax_amount": 500.00,
"amount": 5500.00,
"product_id": null
}
],
"customer": { ... },
"business": { ... }
}
}
Customer event payload
Events: customer.created, customer.updated, customer.deleted
{
"id": "evt_...",
"type": "customer.created",
"created_at": "2026-04-01T10:00:00.000Z",
"business_id": "cb21efb1-...",
"data": {
"id": "550e8400-...",
"name": "Acme Corp",
"email": "billing@acme.com",
"customer": {
"id": "550e8400-...",
"name": "Acme Corp",
"email": "billing@acme.com",
"phone": "+1-555-0100",
"address": "123 Business Ave",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US",
"tax_id": "US-12345678",
"notes": null
},
"business": { ... }
}
}
Recurring invoice event payload
Events: recurring_invoice.generated, recurring_invoice.failed, recurring_invoice.completed
{
"id": "evt_...",
"type": "recurring_invoice.generated",
"created_at": "2026-04-01T00:00:00.000Z",
"business_id": "cb21efb1-...",
"data": {
"recurring_invoice_id": "...",
"invoice_id": "...",
"invoice_number": "INV-00042",
"total": 1500.00,
"occurrence_number": 3,
"recurring_invoice": {
"id": "...",
"name": "Monthly Retainer - Acme",
"frequency": "monthly",
"status": "active",
"start_date": "2026-01-01",
"next_run_date": "2026-05-01",
"end_date": null,
"occurrences_count": 3,
"max_occurrences": 12,
"currency": "USD",
"auto_send": true,
"created_at": "2025-12-15T10:00:00.000Z"
},
"generated_invoice": {
"id": "...",
"invoice_number": "INV-00042",
"status": "sent",
"issue_date": "2026-04-01",
"due_date": "2026-05-01",
"currency": "USD",
"subtotal": 1500.00,
"tax_amount": 0,
"total": 1500.00,
"items": [
{
"description": "Monthly Retainer",
"quantity": 1,
"unit_price": 1500
}
]
},
"customer": { ... },
"business": { ... }
}
}
Deduplication
Use the id field (event ID) to deduplicate events. The same event may be delivered more than once due to retries. The event ID remains the same across all delivery attempts, while the X-Webhook-ID header changes per attempt.
const processedEvents = new Set();
app.post('/webhooks', (req, res) => {
const eventId = req.body.id;
if (processedEvents.has(eventId)) {
return res.status(200).send('Already processed');
}
processedEvents.add(eventId);
// Process the event...
res.status(200).send('OK');
});
In production, store processed event IDs in your database (e.g., a processed_webhook_events table) instead of an in-memory Set.