Invoicing webhooks provide your app with real-time invoice status updates. By subscribing your webhook endpoint you will receive a notification every time an invoice status changes.
Setup
Prerequisites
You will need:
- Your CDP API Key ID and secret
- A webhook notification HTTPS URL
- Install the CDP CLI and run
cdp env live --key-file ./cdp_api_key.json once to configure credentials.
Create a webhook subscription
- Review the configuration you’ll submit:
Important configuration notes:
-
labels is a required field, but can be an empty object {}
-
target.url should be your webhook endpoint that will receive the events
-
You can also set a
headers object in target if your url requires specific headers. Pass it inline with 'target.headers:={"custom-header":"value"}'.
-
All Invoicing event types should be included to ensure you receive notifications for every invoice state change:
| Event type | Description |
|---|
invoicing.invoice.scheduled | Invoice scheduled to be sent |
invoicing.invoice.open | Invoice sent and awaiting payment |
invoicing.invoice.paid | Invoice paid in full |
invoicing.invoice.overdue | Invoice past due date |
invoicing.invoice.void | Invoice voided |
invoicing.invoice.deleted | Invoice deleted |
- Create the webhook subscription with the CDP CLI:
cdp data webhooks subscriptions create \
description="Invoice status webhook" \
'eventTypes:=["invoicing.invoice.scheduled","invoicing.invoice.open","invoicing.invoice.paid","invoicing.invoice.overdue","invoicing.invoice.void","invoicing.invoice.deleted"]' \
target.url=https://your-webhook-url.com \
target.method=POST \
'labels:={}' \
isEnabled:=true
Response:
201 Created
{
"createdAt": "2025-09-10T13:58:38.681893Z",
"description": "Invoice status webhook",
"eventTypes": [
"invoicing.invoice.scheduled",
"invoicing.invoice.open",
"invoicing.invoice.paid",
"invoicing.invoice.overdue",
"invoicing.invoice.void",
"invoicing.invoice.deleted"
],
"isEnabled": true,
"labelKey": "user_uuid",
"labelValue": "<YOUR_USER_UUID>",
"labels": {
"user_uuid": "<YOUR_USER_UUID>"
},
"metadata": {
"secret": "<SECRET_FOR_WEBHOOK_VERIFICATION>"
},
"subscriptionId": "<YOUR_SUBSCRIPTION_ID>",
"target": {
"url": "https://your-webhook-url.com"
}
}
Manage subscriptions
Use the subscriptionId from the response to view, update, or delete the subscription.
List all subscriptions
cdp data webhooks subscriptions list
View specified subscription details by subscription ID
cdp data webhooks subscriptions get <SUBSCRIPTION_ID>
Update subscription
update is a full replace — pass every field, including ones you aren’t changing:
cdp data webhooks subscriptions update <SUBSCRIPTION_ID> \
description="Updated: Invoice status webhook" \
'eventTypes:=["invoicing.invoice.scheduled","invoicing.invoice.open","invoicing.invoice.paid","invoicing.invoice.overdue","invoicing.invoice.void","invoicing.invoice.deleted"]' \
target.url=https://your-new-webhook-url.com \
target.method=POST \
'labels:={}' \
isEnabled:=true
Delete subscription
cdp data webhooks subscriptions delete <SUBSCRIPTION_ID>
List subscription events
View delivery attempts for a subscription, including delivery status, retry count, and HTTP response details:
cdp data webhooks subscriptions events <SUBSCRIPTION_ID>
You can narrow results with the following optional query parameters:
| Parameter | Description |
|---|
eventId | Filter by a specific event ID |
minCreatedAt | Only include events created at or after this timestamp (RFC 3339, e.g. 2025-01-15T00:00:00Z) |
maxCreatedAt | Only include events created at or before this timestamp (RFC 3339) |
eventTypeNames | Filter by event type names, comma-separated (e.g. invoicing.invoice.paid,invoicing.invoice.open) |
Results are limited to the 50 most recent events, returned newest first. Example with filters:
cdp data webhooks subscriptions events <SUBSCRIPTION_ID> minCreatedAt==2025-01-15T00:00:00Z eventTypeNames==invoicing.invoice.paid,invoicing.invoice.overdue
Webhook signature verification
Verify webhook signatures to ensure that requests are authentic. This protects your application from forged webhooks and potential security threats.
How it works
When you create a webhook subscription, the response includes a secret. Use this value to verify that incoming webhooks are authentic.
Each webhook request includes an X-Hook0-Signature header containing:
t field - the timestamp
h field - list of headers included in the signature
v1 field - the signature
Implementation
Here’s an example of how to verify webhook signatures:
const crypto = require('crypto');
/**
* Verify webhook signature and timestamp
* @param {string} payload - Raw request body as string
* @param {string} signatureHeader - X-Hook0-Signature header value
* @param {string} secret - Webhook secret from the subscription response
* @param {Object} headers - HTTP headers from webhook request
* @param {number} maxAgeMinutes - Max age for webhook (default: 5 minutes)
* @returns {boolean} true if webhook is authentic and within allowed time window
*/
function verifyWebhookSignature(payload, signatureHeader, secret, headers, maxAgeMinutes = 5) {
try {
// Parse signature header: t=timestamp,h=headers,v1=signature
const elements = signatureHeader.split(',');
const timestamp = elements.find(e => e.startsWith('t=')).split('=')[1];
const headerNames = elements.find(e => e.startsWith('h=')).split('=')[1];
const providedSignature = elements.find(e => e.startsWith('v1=')).split('=')[1];
// Build header values string
const headerNameList = headerNames.split(' ');
const headerValues = headerNameList.map(name => headers[name] || '').join('.');
// Build signed payload
const signedPayload = `${timestamp}.${headerNames}.${headerValues}.${payload}`;
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
// Compare signatures securely
const signaturesMatch = crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(providedSignature, 'hex')
);
// Verify timestamp to prevent replay attacks
const webhookTime = parseInt(timestamp) * 1000; // Convert to milliseconds
const currentTime = Date.now();
const ageMinutes = (currentTime - webhookTime) / (1000 * 60);
if (ageMinutes > maxAgeMinutes) {
console.error(`Webhook timestamp exceeds maximum age: ${ageMinutes.toFixed(1)} minutes > ${maxAgeMinutes} minutes`);
return false;
}
return signaturesMatch;
} catch (error) {
console.error('Webhook verification error:', error);
return false;
}
}
And in your application:
const express = require("express");
const app = express();
// Important: Get raw body for signature verification
app.use(express.raw({ type: "application/json" }));
app.post("/webhook", (req, res) => {
const payload = req.body.toString(); // Raw string needed for signature
const signature = req.headers["x-hook0-signature"];
const secret = process.env.WEBHOOK_SECRET; // Store securely from the subscription response
if (verifyWebhookSignature(payload, signature, secret, req.headers)) {
console.log("✅ Authentic webhook");
// Parse and process the event
const event = JSON.parse(payload);
// Handle your invoice event here
res.status(200).send("OK");
} else {
console.log("❌ Invalid webhook - rejected");
res.status(400).send("Invalid signature");
}
});
Next steps
Once your subscription is created, your endpoint will begin receiving webhook events for invoices!
Sample invoice event payload
{
"id": "68f7a946db0529ea9b6d3a12",
"invoiceNumber": "INV-1234",
"contactName": "John Doe",
"contactEmail": "john.doe@example.com",
"contactAddress": {
"addressLine1": "123 Main Street",
"city": "San Francisco",
"state": "CA",
"country": "US",
"postalCode": "94103"
},
"dueDate": "2025-01-15T00:00:00Z",
"sendDate": "2025-01-01T09:00:00Z",
"invoiceDate": "2025-01-01T00:00:00Z",
"lineItems": [
{
"itemName": "Web Development Services",
"quantity": 10,
"unitPrice": {
"value": "100.00",
"currency": "USDC"
}
}
],
"totalAmountDue": {
"value": "1000.00",
"currency": "USDC"
},
"status": "PAID",
"eventType": "invoicing.invoice.paid",
"createdAt": "2025-01-01T08:30:00Z",
"updatedAt": "2025-01-02T14:22:00Z",
"createdBy": "750a84dd-2460-504c-8bb9-f6fa731a2361",
"lastUpdatedBy": "750a84dd-2460-504c-8bb9-f6fa731a2361",
"entityName": "Acme Corporation",
"entityAddress": {
"addressLine1": "456 Business Ave",
"city": "San Francisco",
"state": "CA",
"country": "US",
"postalCode": "94105"
},
"paymentMethod": {
"crypto": {
"paymentLinkUrl": "https://pay.coinbase.com/pl_01h8441j23abcd1234567890ef",
"paymentLinkId": "68f7a946db0529ea9b6d3a12",
"transactionHash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
}
}
}