Overview
Webhooks allow you to receive real-time notifications when events occur in your GetBill account. Instead of repeatedly polling the API for changes, webhooks push updates directly to your application, making your integration more efficient and responsive.
How Webhooks Work
Setting Up Webhooks
1. Create a Webhook Endpoint
Your webhook endpoint must:
Accept POST requests
Return a 2xx status code (200-299) for successful processing
Respond within 10 seconds
Verify the webhook signature (recommended)
Node.js (Express)
Python (Flask)
PHP
const express = require ( 'express' );
const crypto = require ( 'crypto' );
const app = express ();
// Middleware to capture raw body for signature verification
app . use ( '/webhooks' , express . raw ({ type: 'application/json' }));
app . post ( '/webhooks/getbill' , ( req , res ) => {
try {
// Verify webhook signature
if ( ! verifySignature ( req )) {
return res . status ( 401 ). send ( 'Invalid signature' );
}
// Parse the webhook payload
const event = JSON . parse ( req . body );
// Process the event
handleWebhookEvent ( event );
// Respond with success
res . status ( 200 ). send ( 'OK' );
} catch ( error ) {
console . error ( 'Webhook processing error:' , error );
res . status ( 500 ). send ( 'Error processing webhook' );
}
});
function verifySignature ( req ) {
const signature = req . headers [ 'x-getbill-signature' ];
const webhookSecret = process . env . GETBILL_WEBHOOK_SECRET ;
if ( ! signature || ! webhookSecret ) {
return false ;
}
const expectedSignature = crypto
. createHmac ( 'sha256' , webhookSecret )
. update ( req . body )
. digest ( 'hex' );
return crypto . timingSafeEqual (
Buffer . from ( signature , 'hex' ),
Buffer . from ( expectedSignature , 'hex' )
);
}
function handleWebhookEvent ( event ) {
console . log ( 'Received webhook:' , event . type );
switch ( event . type ) {
case 'debt.created' :
handleDebtCreated ( event . data );
break ;
case 'debt.updated' :
handleDebtUpdated ( event . data );
break ;
case 'followup.completed' :
handleFollowupCompleted ( event . data );
break ;
default :
console . log ( 'Unknown event type:' , event . type );
}
}
from flask import Flask, request, abort
import hashlib
import hmac
import json
import os
app = Flask( __name__ )
@app.route ( '/webhooks/getbill' , methods = [ 'POST' ])
def handle_webhook ():
try :
# Verify webhook signature
if not verify_signature(request):
abort( 401 )
# Parse the webhook payload
event = request.get_json()
# Process the event
handle_webhook_event(event)
return 'OK' , 200
except Exception as e:
print ( f 'Webhook processing error: { e } ' )
return 'Error processing webhook' , 500
def verify_signature ( request ):
signature = request.headers.get( 'X-GetBill-Signature' )
webhook_secret = os.environ.get( 'GETBILL_WEBHOOK_SECRET' )
if not signature or not webhook_secret:
return False
expected_signature = hmac.new(
webhook_secret.encode( 'utf-8' ),
request.get_data(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
def handle_webhook_event ( event ):
print ( f 'Received webhook: { event[ "type" ] } ' )
event_handlers = {
'debt.created' : handle_debt_created,
'debt.updated' : handle_debt_updated,
'followup.completed' : handle_followup_completed,
}
handler = event_handlers.get(event[ 'type' ])
if handler:
handler(event[ 'data' ])
else :
print ( f 'Unknown event type: { event[ "type" ] } ' )
def handle_debt_created ( data ):
print ( f 'New debt created: { data[ "id" ] } ' )
# Update your local database, send notifications, etc.
def handle_debt_updated ( data ):
print ( f 'Debt updated: { data[ "id" ] } ' )
# Sync changes to your local database
def handle_followup_completed ( data ):
print ( f 'Followup completed: { data[ "id" ] } ' )
# Update statistics, trigger next actions, etc.
<? php
// webhook.php
// Get the raw POST data
$payload = file_get_contents ( 'php://input' );
$headers = getallheaders ();
// Verify webhook signature
if ( ! verifySignature ( $payload , $headers )) {
http_response_code ( 401 );
exit ( 'Invalid signature' );
}
try {
// Parse the webhook payload
$event = json_decode ( $payload , true );
// Process the event
handleWebhookEvent ( $event );
// Respond with success
http_response_code ( 200 );
echo 'OK' ;
} catch ( Exception $e ) {
error_log ( 'Webhook processing error: ' . $e -> getMessage ());
http_response_code ( 500 );
echo 'Error processing webhook' ;
}
function verifySignature ( $payload , $headers ) {
$signature = $headers [ 'X-GetBill-Signature' ] ?? '' ;
$webhookSecret = $_ENV [ 'GETBILL_WEBHOOK_SECRET' ] ?? '' ;
if ( empty ( $signature ) || empty ( $webhookSecret )) {
return false ;
}
$expectedSignature = hash_hmac ( 'sha256' , $payload , $webhookSecret );
return hash_equals ( $signature , $expectedSignature );
}
function handleWebhookEvent ( $event ) {
error_log ( 'Received webhook: ' . $event [ 'type' ]);
switch ( $event [ 'type' ]) {
case 'debt.created' :
handleDebtCreated ( $event [ 'data' ]);
break ;
case 'debt.updated' :
handleDebtUpdated ( $event [ 'data' ]);
break ;
case 'followup.completed' :
handleFollowupCompleted ( $event [ 'data' ]);
break ;
default :
error_log ( 'Unknown event type: ' . $event [ 'type' ]);
}
}
function handleDebtCreated ( $data ) {
error_log ( 'New debt created: ' . $data [ 'id' ]);
// Update your local database, send notifications, etc.
}
function handleDebtUpdated ( $data ) {
error_log ( 'Debt updated: ' . $data [ 'id' ]);
// Sync changes to your local database
}
function handleFollowupCompleted ( $data ) {
error_log ( 'Followup completed: ' . $data [ 'id' ]);
// Update statistics, trigger next actions, etc.
}
?>
Log in to your GetBill dashboard
Navigate to Settings → External Services
Scroll down to the Webhook Endpoints section
Click Create New Webhook Endpoint
Configure your webhook:
Name : A descriptive name (e.g., “CRM Integration”)
URL : Your webhook endpoint URL (e.g., https://yourapp.com/webhooks/getbill)
Description : Optional description of what this webhook does
Events : Select which events to receive (check the boxes for the event types you want)
Click Create - a secret key will be automatically generated
Important : Copy and save the secret key securely - you’ll need it for signature verification
Verifying Webhook Signatures
GetBill signs all webhook requests to ensure they’re authentic and haven’t been tampered with. You should always verify the signature before processing webhook events to protect your application from malicious requests.
How It Works
When GetBill sends a webhook to your endpoint, it includes a cryptographic signature in the request headers. Here’s the process:
GetBill generates a signature : Using HMAC-SHA256 with your webhook secret key and the raw request body
Signature is sent in header : The signature is included in the X-GetBill-Signature header
You compute the same signature : Using the same algorithm, your secret key, and the raw request body
Compare signatures : Use a timing-safe comparison function to verify they match
Signature Details
The cryptographic hash algorithm used to generate signatures
The HTTP header containing the signature (lowercase hex string)
The exact raw JSON payload as received (before parsing)
The secret key generated when you created the webhook endpoint
Verification Steps
Extract the signature from the X-GetBill-Signature header
Get the raw request body (before JSON parsing)
Compute HMAC-SHA256 hash of the raw body using your secret key
Compare using a timing-safe comparison function (prevents timing attacks)
Reject the request if signatures don’t match
Security Critical : Always use a constant-time comparison function (like crypto.timingSafeEqual() in Node.js, hmac.compare_digest() in Python, or hash_equals() in PHP) to prevent timing attacks. Never use simple string equality (== or ===).
Implementation Examples
The code examples in the sections above demonstrate proper signature verification. Key points:
Node.js : Use crypto.timingSafeEqual() with Buffer.from()
Python : Use hmac.compare_digest()
PHP : Use hash_equals()
Testing Signature Verification
You can test your signature verification with this example:
# Example values
SECRET_KEY = "your_webhook_secret_here"
PAYLOAD = '{"id":"evt_test123","type":"debt.created","data":{}}'
# Generate signature (in bash)
echo -n " $PAYLOAD " | openssl dgst -sha256 -hmac " $SECRET_KEY " -hex
This will output the expected signature that should match what your endpoint computes.
Store your webhook secret securely (environment variable or secrets manager) and never commit it to version control . Treat it like a password!
Webhook Events
Available Event Types
debt.created - A new debt was created
debt.updated - A debt was modified
debt.deleted - A debt was deleted
debt.status_changed - Debt status was updated
followup.created - A new followup was created
followup.updated - A followup was modified
followup.completed - A followup was marked as completed
followup.failed - A followup attempt failed
payment.received - A payment was recorded
payment.failed - A payment attempt failed
payment_plan.created - A payment plan was set up
payment_plan.completed - All payments in a plan were completed
company.credits_low - Credit balance is running low
report.generated - A report was generated and is ready for download
integration.error - An integration error occurred
Event Payload Structure
All webhook events follow this structure:
{
"id" : "evt_abc123def456" ,
"type" : "debt.created" ,
"created_at" : "2024-01-22T10:30:00Z" ,
"data" : {
"id" : "d4b7e9f2a1c8" ,
"firstname" : "John" ,
"lastname" : "Doe" ,
"phone" : "+33612345678" ,
"email" : "john.doe@example.com" ,
"amount" : 1250.00 ,
"currency" : "EUR" ,
"status" : "status.default.in_progress" ,
"internal_id" : "INV-2024-001" ,
"due_date" : "2024-02-22" ,
"invoice_date" : "2024-01-22" ,
"created_at" : "2024-01-22T10:30:00Z" ,
"company_id" : "c5a8f3e1b9d2" ,
"metadata" : { "crm_id" : "CRM-123" , "source" : "website" }
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2" ,
"debt_id" : "d4b7e9f2a1c8"
}
}
Unique identifier for the webhook event (format: evt_[32 hex characters])
The type of event that occurred (e.g., debt.created, followup.completed)
When the event occurred (ISO 8601 format with timezone)
The actual data for the event (structure varies by event type - see examples below)
Additional context about the event including:
source: Always “getbill”
version: API version (currently “1.0”)
company_id: Your company ID
Additional fields depending on event type (e.g., debt_id, followup_id, payment_id)
Event Type Examples
Debt Events
debt.created
{
"id" : "evt_a1b2c3d4..." ,
"type" : "debt.created" ,
"created_at" : "2024-01-22T10:30:00+00:00" ,
"data" : {
"id" : "d4b7e9f2a1c8" ,
"firstname" : "John" ,
"lastname" : "Doe" ,
"phone" : "+33612345678" ,
"email" : "john.doe@example.com" ,
"amount" : 1250.00 ,
"currency" : "EUR" ,
"status" : "status.default.in_progress" ,
"internal_id" : "INV-2024-001" ,
"due_date" : "2024-02-22" ,
"invoice_date" : "2024-01-22" ,
"created_at" : "2024-01-22T10:30:00+00:00" ,
"company_id" : "c5a8f3e1b9d2" ,
"metadata" : { "crm_id" : "CRM-123" , "source" : "website" }
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2" ,
"debt_id" : "d4b7e9f2a1c8"
}
}
debt.status_changed
{
"id" : "evt_e5f6g7h8..." ,
"type" : "debt.status_changed" ,
"created_at" : "2024-01-25T14:20:00+00:00" ,
"data" : {
"id" : "d4b7e9f2a1c8" ,
"firstname" : "John" ,
"lastname" : "Doe" ,
"phone" : "+33612345678" ,
"email" : "john.doe@example.com" ,
"amount" : 1250.00 ,
"currency" : "EUR" ,
"status" : "status.default.paid" ,
"previous_status" : "status.default.in_progress" ,
"new_status" : "status.default.paid" ,
"internal_id" : "INV-2024-001" ,
"due_date" : "2024-02-22" ,
"invoice_date" : "2024-01-22" ,
"created_at" : "2024-01-22T10:30:00+00:00" ,
"company_id" : "c5a8f3e1b9d2" ,
"metadata" : { "crm_id" : "CRM-123" , "source" : "website" }
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2" ,
"debt_id" : "d4b7e9f2a1c8" ,
"previous_status" : "status.default.in_progress" ,
"new_status" : "status.default.paid"
}
}
The status field uses translation key format status.default.* (not simple strings): Status Category Description When Used status.default.pendingInitial Initial state, awaiting processing Debt just created status.default.in_progressActive Active collection in progress Collection process started status.default.processingActive Currently being processed In active workflow status.default.settledResolved Debt settled through payment plan Payment plan completed status.default.paidResolved Fully paid off Full payment received status.default.partialActive Partially paid Some payment received status.default.failedClosed Collection failed Cannot collect (various reasons) status.default.archivedClosed Archived/closed Manually archived status.default.invalid_phoneClosed Invalid contact information Phone number invalid/blocked status.default.on_holdPaused On hold (typically due to dispute) Collection paused (dispute active) status.default.no_answerActive Voicemail/no answer Multiple failed contact attempts status.default.refusedClosed Debtor refused to pay Explicit refusal to pay
Important Notes :
Format is always status.default.{name}, never just the name (e.g., "status.default.paid" not "paid")
Companies can create custom statuses with the status.custom.* prefix
When filtering or comparing status values, always use the full format
debt.deleted
{
"id" : "evt_i9j0k1l2..." ,
"type" : "debt.deleted" ,
"created_at" : "2024-01-26T09:15:00+00:00" ,
"data" : {
"id" : "d4b7e9f2a1c8" ,
"deleted_at" : "2024-01-26T09:15:00+00:00"
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2" ,
"debt_id" : "d4b7e9f2a1c8"
}
}
Followup Events
followup.completed
{
"id" : "evt_m3n4o5p6..." ,
"type" : "followup.completed" ,
"created_at" : "2024-01-23T15:45:00+00:00" ,
"data" : {
"id" : "f8a3c7e2d9b1" ,
"debt_id" : "d4b7e9f2a1c8" ,
"type" : "CALL" ,
"status" : "deal_found" ,
"summary" : "Customer agreed to payment plan" ,
"call_date" : "2024-01-23T15:30:00+00:00" ,
"scheduled_at" : "2024-01-23T15:00:00+00:00" ,
"processed_at" : "2024-01-23T15:45:00+00:00" ,
"duration_ms" : 180000 ,
"provider" : "retell" ,
"audio_url" : "https://d1234567abcdef.cloudfront.net/audio/recordings/call_abc123.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=..." ,
"pdf_url" : null ,
"media_urls_expire_at" : "2024-01-23T16:45:00+00:00" ,
"company_id" : "c5a8f3e1b9d2"
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2" ,
"followup_id" : "f8a3c7e2d9b1" ,
"debt_id" : "d4b7e9f2a1c8" ,
"status" : "deal_found"
}
}
Media URLs : The audio_url and pdf_url fields contain presigned CloudFront URLs that expire after 1 hour (indicated by media_urls_expire_at). These URLs provide temporary access to call recordings and PDF documents. Important : Cache these URLs but refresh them before the expiration time if you need continued access.
The status field uses lowercase underscore format (e.g., deal_found, not DEAL_FOUND): Status Type Description Use Case on_holdPending Followup is queued and waiting to be processed Initial state before execution scheduledPending Followup is scheduled for a future time Planned for specific datetime in_progressActive Followup is currently being processed Call in progress sentSuccess Message (email/SMS/WhatsApp) has been sent Email/SMS successfully sent deliveredSuccess Message was successfully delivered Confirmed delivery deal_foundSuccess Customer agreed to a payment arrangement Positive outcome from call no_answerRetry Call was not answered Customer didn’t pick up (voicemail) deal_not_foundCompleted Customer refused or no agreement reached Negative outcome from call bad_behaviorFailed Negative behavior detected (abusive, threats, etc.) Customer was inappropriate failedFailed Followup permanently failed Technical failure or error cancelledCancelled Followup was manually cancelled Cancelled by user
Note : Always use lowercase format (e.g., "deal_found" not "DEAL_FOUND").
followup.failed
{
"id" : "evt_q7r8s9t0..." ,
"type" : "followup.failed" ,
"created_at" : "2024-01-23T16:00:00+00:00" ,
"data" : {
"id" : "f2b5d8a1c7e3" ,
"debt_id" : "d9e1a3f7b2c5" ,
"type" : "CALL" ,
"status" : "bad_behavior" ,
"summary" : "Call could not be completed" ,
"failure_reason" : "Phone number disconnected" ,
"scheduled_at" : "2024-01-23T15:30:00+00:00" ,
"processed_at" : "2024-01-23T16:00:00+00:00" ,
"audio_url" : null ,
"pdf_url" : null ,
"company_id" : "c5a8f3e1b9d2"
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2" ,
"followup_id" : "f2b5d8a1c7e3" ,
"debt_id" : "d9e1a3f7b2c5"
}
}
Payment Events
payment.received
{
"id" : "evt_u1v2w3x4..." ,
"type" : "payment.received" ,
"created_at" : "2024-01-24T11:20:00+00:00" ,
"data" : {
"id" : "p7c3e9a2f1d8" ,
"debt_id" : "d4b7e9f2a1c8" ,
"amount" : 1250.00 ,
"payment_date" : "2024-01-24" ,
"status" : "paid" ,
"paid_at" : "2024-01-24T11:20:00+00:00" ,
"created_at" : "2024-01-22T10:30:00+00:00"
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2" ,
"payment_id" : "p7c3e9a2f1d8" ,
"debt_id" : "d4b7e9f2a1c8"
}
}
payment_plan.created
{
"id" : "evt_y5z6a7b8..." ,
"type" : "payment_plan.created" ,
"created_at" : "2024-01-23T14:00:00+00:00" ,
"data" : {
"debt_id" : "d4b7e9f2a1c8" ,
"plan_months" : 6 ,
"plan_start_date" : "2024-02-01" ,
"first_payment_amount" : 250.00 ,
"installments" : [
{
"id" : "p1a2b3c4d5e6" ,
"debt_id" : "d4b7e9f2a1c8" ,
"amount" : 250.00 ,
"payment_date" : "2024-02-01" ,
"status" : "pending" ,
"paid_at" : null ,
"created_at" : "2024-01-23T14:00:00+00:00"
},
{
"id" : "p2b3c4d5e6f7" ,
"debt_id" : "d4b7e9f2a1c8" ,
"amount" : 250.00 ,
"payment_date" : "2024-03-01" ,
"status" : "pending" ,
"paid_at" : null ,
"created_at" : "2024-01-23T14:00:00+00:00"
}
]
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2" ,
"debt_id" : "d4b7e9f2a1c8"
}
}
payment_plan.completed
{
"id" : "evt_z6a7b8c9..." ,
"type" : "payment_plan.completed" ,
"created_at" : "2024-07-01T10:00:00+00:00" ,
"data" : {
"debt_id" : "d4b7e9f2a1c8" ,
"completed_at" : "2024-07-01T10:00:00+00:00"
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2" ,
"debt_id" : "d4b7e9f2a1c8"
}
}
System Events
company.credits_low
{
"id" : "evt_c9d0e1f2..." ,
"type" : "company.credits_low" ,
"created_at" : "2024-01-25T08:00:00+00:00" ,
"data" : {
"company_id" : "c5a8f3e1b9d2" ,
"current_credits" : 15.50 ,
"threshold" : 20.00
},
"metadata" : {
"source" : "getbill" ,
"version" : "1.0" ,
"company_id" : "c5a8f3e1b9d2"
}
}
Handling Specific Events
Debt Events
function handleDebtCreated ( data ) {
console . log ( `New debt created: ${ data . id } ` );
// Update local database
updateLocalDebt ({
external_id: data . id ,
firstname: data . firstname ,
lastname: data . lastname ,
amount: data . amount ,
currency: data . currency ,
status: data . status ,
created_at: data . created_at
});
// Send notification to relevant team members
notifyTeam ( 'new_debt' , {
debtor: ` ${ data . firstname } ${ data . lastname } ` ,
amount: ` ${ data . amount } ${ data . currency } `
});
// Trigger automated followup workflow
scheduleInitialFollowup ( data . id );
}
function handleDebtStatusChanged ( data ) {
console . log ( `Debt ${ data . id } status changed to: ${ data . status } ` );
// Update local records
updateLocalDebtStatus ( data . id , data . status );
// Handle status-specific logic
switch ( data . status ) {
case 'status.default.paid' :
handleDebtPaid ( data );
break ;
case 'status.default.on_hold' :
handleDebtOnHold ( data );
break ;
case 'status.default.failed' :
handleDebtFailed ( data );
break ;
}
}
function handleDebtPaid ( data ) {
// Update accounting system
recordPayment ( data . id , data . amount );
// Stop all followup activities
cancelFollowups ( data . id );
// Send confirmation to debtor
sendPaymentConfirmation ( data . email );
}
Followup Events
function handleFollowupCompleted ( data ) {
console . log ( `Followup completed: ${ data . id } ` );
// Update local records
updateLocalFollowup ( data . id , {
status: data . status ,
processed_at: data . processed_at ,
summary: data . summary ,
duration_ms: data . duration_ms
});
// Download and store call recording if available
if ( data . audio_url ) {
downloadAndStoreAudio ( data . id , data . audio_url , data . media_urls_expire_at );
}
// Download and store PDF letter if available
if ( data . pdf_url ) {
downloadAndStorePDF ( data . id , data . pdf_url , data . media_urls_expire_at );
}
// Analyze outcome and schedule next action
switch ( data . status ) {
case 'deal_found' :
// Customer agreed to payment plan
handlePaymentAgreement ( data . debt_id );
break ;
case 'deal_not_found' :
// Customer refused payment plan
scheduleFollowupCall ( data . debt_id , 7 ); // Try again in 7 days
break ;
case 'sent' :
// Email/SMS sent successfully
trackCommunicationSent ( data . debt_id , data . type );
break ;
default :
console . log ( `Followup completed with status: ${ data . status } ` );
}
// Update debt priority based on response
updateDebtPriority ( data . debt_id , data . status );
}
async function downloadAndStoreAudio ( followupId , audioUrl , expiresAt ) {
try {
// Presigned URLs expire after 1 hour - download immediately
const response = await fetch ( audioUrl );
if ( ! response . ok ) {
throw new Error ( `Failed to download audio: ${ response . status } ` );
}
// Store audio file in your system
const audioBuffer = await response . arrayBuffer ();
await saveAudioToStorage ( followupId , audioBuffer );
console . log ( `Audio recording stored for followup ${ followupId } ` );
} catch ( error ) {
console . error ( `Failed to download audio for followup ${ followupId } :` , error );
// Schedule retry or alert admins
}
}
function handleFollowupFailed ( data ) {
console . log ( `Followup failed: ${ data . id } ` );
console . log ( `Reason: ${ data . failure_reason } ` );
// Log the failure
logFollowupFailure ( data . id , data . failure_reason );
// Handle based on failure type
if ( data . status === 'bad_behavior' ) {
// Mark debt for manual review
escalateToManualReview ( data . debt_id );
} else {
// Schedule retry with different contact method
scheduleAlternativeContact ( data . debt_id );
}
}
Error Handling & Reliability
Automatic Retry Behavior
GetBill automatically handles webhook delivery retries for you. If your endpoint fails to respond with a 2xx status code or times out, GetBill will automatically retry with exponential backoff:
1st retry : After 1 minute
2nd retry : After 5 minutes
3rd retry : After 30 minutes
After 3 failed attempts, the webhook delivery is marked as permanently failed. You can view the full delivery history and error details in your dashboard under Settings → External Services → Webhook Endpoints → Deliveries .
Important : Make sure your endpoint returns a 2xx status code (like 200 or 204) quickly, even if you queue the actual processing for later. GetBill waits up to 10 seconds for a response before timing out.
Monitoring Failed Deliveries
Monitor webhook delivery failures through your dashboard:
Go to Settings → External Services
Find your webhook endpoint
Click Deliveries to see:
Delivery status for each event
HTTP status codes returned
Error messages for failures
Retry attempts and timing
Full request/response payloads
This helps you quickly diagnose and fix integration issues.
Idempotency
Ensure your webhook handlers are idempotent to handle duplicate deliveries:
const processedEvents = new Set ();
function handleWebhookEvent ( event ) {
// Check if we've already processed this event
if ( processedEvents . has ( event . id )) {
console . log ( `Event ${ event . id } already processed, skipping` );
return ;
}
try {
// Process the event
switch ( event . type ) {
case 'debt.created' :
handleDebtCreated ( event . data );
break ;
// ... other handlers
}
// Mark as processed
processedEvents . add ( event . id );
// Clean up old entries (keep last 1000)
if ( processedEvents . size > 1000 ) {
const entries = Array . from ( processedEvents );
processedEvents . clear ();
entries . slice ( - 500 ). forEach ( id => processedEvents . add ( id ));
}
} catch ( error ) {
console . error ( `Error processing event ${ event . id } :` , error );
throw error ;
}
}
Testing Webhooks
Local Development with ngrok
For local testing, use ngrok to expose your local server:
# Install ngrok
npm install -g ngrok
# Start your local server
node server.js
# In another terminal, expose port 3000
ngrok http 3000
# Use the ngrok URL in your webhook configuration
# https://abc123.ngrok.io/webhooks/getbill
Testing Webhook Endpoints
// test-webhook.js
const crypto = require ( 'crypto' );
function createTestWebhook ( type , data , companyId = 123 ) {
return {
id: 'evt_' + crypto . randomBytes ( 16 ). toString ( 'hex' ),
type ,
created_at: new Date (). toISOString (),
data ,
metadata: {
source: 'getbill' ,
version: '1.0' ,
company_id: companyId ,
debt_id: data . id || null
}
};
}
function signPayload ( payload , secret ) {
return crypto
. createHmac ( 'sha256' , secret )
. update ( payload )
. digest ( 'hex' );
}
async function testWebhook () {
const webhook = createTestWebhook ( 'debt.created' , {
id: 789 ,
firstname: 'John' ,
lastname: 'Doe' ,
phone: '+33612345678' ,
email: 'john.doe@example.com' ,
amount: 1000.00 ,
currency: 'EUR' ,
status: 'active' ,
internal_id: 'INV-TEST-001' ,
due_date: '2024-02-22' ,
invoice_date: '2024-01-22' ,
created_at: new Date (). toISOString (),
company_id: 123
});
const payload = JSON . stringify ( webhook );
const signature = signPayload ( payload , 'your_webhook_secret' );
const response = await fetch ( 'http://localhost:3000/webhooks/getbill' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'X-GetBill-Signature' : signature
},
body: payload
});
console . log ( 'Webhook test response:' , response . status );
if ( response . ok ) {
console . log ( '✓ Webhook delivered successfully' );
} else {
console . error ( '✗ Webhook delivery failed' );
}
}
testWebhook ();
Monitoring & Debugging
Monitoring Webhooks
Monitor webhook deliveries through your GetBill dashboard:
Delivery Status : Check webhook delivery success/failure rates
Event History : Review recent webhook events and their outcomes
Error Analysis : Investigate failed deliveries and common issues
Performance Metrics : Track response times and delivery patterns
For programmatic monitoring, you can implement your own logging and alerting based on webhook responses.
Security Best Practices
Verify Signatures Always verify webhook signatures to ensure requests come from GetBill
Use HTTPS Only accept webhooks over HTTPS to protect data in transit
Validate Payloads Validate webhook payloads before processing to prevent malicious input
Rate Limiting Implement rate limiting on your webhook endpoints to prevent abuse
Webhooks are a powerful way to keep your application in sync with GetBill in real-time. By following these guidelines and implementing proper error handling, you’ll have a robust integration that scales with your business needs.