Skip to content

Webhooks

VisiBooks fires webhook events when resources are created or changed via the API. Configure webhook endpoints in Settings > Webhooks to receive these notifications.

EventTrigger
books.contact.createdContact created
books.invoice.createdInvoice created
books.invoice.postedInvoice posted to ledger
books.invoice.voidedInvoice voided
books.invoice.paidInvoice fully paid
books.payment.createdPayment created
books.payment.appliedPayment applied to invoice(s)
books.payment.voidedPayment voided
EventTrigger
books.bill.createdBill created (direct or from commitment)
books.bill.postedBill posted to ledger
books.bill.voidedBill voided
books.bill.paidBill fully paid via vendor payment
books.vendor_payment.createdVendor payment recorded
books.vendor_payment.appliedPayment applied to bill(s)
books.vendor_payment.voidedVendor payment voided
books.commitment.matchedInvoice allocated to a commitment
books.commitment.bill_createdBill created from commitment allocations
EventTrigger
books.reconciliation.completedReconciliation completed
books.reconciliation.invalidatedReconciliation invalidated
books.period.closedAccounting period closed
books.period.reopenedAccounting period reopened

Fired when an invoice is allocated to a commitment. Includes the commitment’s external IDs so your system can correlate.

{
"event": "books.commitment.matched",
"event_id": "a1b2c3d4-...",
"occurred_at": "2026-03-12T15:30:00Z",
"data": {
"commitment_id": 1,
"commitment_number": "PO-2026-001",
"external_source": "quarry",
"external_id": "ORD-8834",
"invoice_document_id": 7,
"allocated_cents": 109500,
"remaining_cents": 0,
"status": "fully_matched"
}
}

Fired when a bill is created from approved commitment allocations.

{
"event": "books.commitment.bill_created",
"event_id": "e5f6a7b8-...",
"occurred_at": "2026-03-12T15:35:00Z",
"data": {
"bill_id": 12,
"bill_total_cents": 124000,
"invoice_document_id": 7,
"commitment_ids": [1],
"commitments": [
{
"commitment_id": 1,
"number": "PO-2026-001",
"external_source": "quarry",
"external_id": "ORD-8834",
"status": "fully_matched"
}
]
}
}

Fired when a bill’s balance reaches zero after a vendor payment is applied.

{
"event": "books.bill.paid",
"event_id": "c9d0e1f2-...",
"occurred_at": "2026-03-15T10:00:00Z",
"data": {
"bill_id": 12,
"bill_number": "BILL-00042",
"total_cents": 124000,
"contact_id": 42
}
}

Every webhook delivery includes these headers:

HeaderDescription
Content-Typeapplication/json
User-AgentVisiHub-Webhooks/1.0
X-VisiHub-EventEvent type (e.g., books.invoice.created)
X-VisiHub-SignatureHMAC-SHA256 signature of the request body

Webhook payloads include the resource ID and external ID:

{
"event": "books.invoice.created",
"data": {
"invoice_id": 123,
"external_id": "INV-001"
},
"timestamp": "2026-03-12T15:30:00Z"
}

Every webhook endpoint gets a unique secret (generated automatically when you create the endpoint). Deliveries are signed with HMAC-SHA256 using this secret.

The signature is sent in the X-VisiHub-Signature header as sha256=<hex digest>.

# Python
import hmac
import hashlib
def verify_signature(payload_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), payload_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
# Ruby
def verify_signature(payload_body, signature_header, secret)
expected = "sha256=" + OpenSSL::HMAC.hexdigest("SHA256", secret, payload_body)
Rack::Utils.secure_compare(expected, signature_header)
end
Node.js
import { createHmac, timingSafeEqual } from "crypto";
function verifySignature(payloadBody, signatureHeader, secret) {
const expected =
"sha256=" + createHmac("sha256", secret).update(payloadBody).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}

Always use constant-time comparison to prevent timing attacks.

Deliveries are retried up to 3 times with increasing backoff on failure. A delivery is considered successful if your endpoint returns a 2xx status code.

After 10 consecutive failures, the endpoint is automatically disabled. Re-enable it from Settings once the issue is resolved.

Send a test ping to any webhook endpoint from the Settings page, or via the API:

Terminal window
curl -X POST https://api.visihub.app/api/webhook_endpoints/{id}/ping \
-H "Authorization: Bearer vk_..."