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)
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);
  }
}

2. Configure Webhooks in GetBill

  1. Log in to your GetBill dashboard
  2. Navigate to SettingsExternal Services
  3. Scroll down to the Webhook Endpoints section
  4. Click Create New Webhook Endpoint
  5. 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)
  6. Click Create - a secret key will be automatically generated
  7. 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:
  1. GetBill generates a signature: Using HMAC-SHA256 with your webhook secret key and the raw request body
  2. Signature is sent in header: The signature is included in the X-GetBill-Signature header
  3. You compute the same signature: Using the same algorithm, your secret key, and the raw request body
  4. Compare signatures: Use a timing-safe comparison function to verify they match

Signature Details

Algorithm
HMAC-SHA256
The cryptographic hash algorithm used to generate signatures
Header Name
X-GetBill-Signature
The HTTP header containing the signature (lowercase hex string)
Signed Data
Raw Request Body
The exact raw JSON payload as received (before parsing)
Secret Key
Webhook Secret
The secret key generated when you created the webhook endpoint

Verification Steps

  1. Extract the signature from the X-GetBill-Signature header
  2. Get the raw request body (before JSON parsing)
  3. Compute HMAC-SHA256 hash of the raw body using your secret key
  4. Compare using a timing-safe comparison function (prevents timing attacks)
  5. 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"
  }
}
id
string
Unique identifier for the webhook event (format: evt_[32 hex characters])
type
string
The type of event that occurred (e.g., debt.created, followup.completed)
created_at
string
When the event occurred (ISO 8601 format with timezone)
data
object
The actual data for the event (structure varies by event type - see examples below)
metadata
object
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):
StatusCategoryDescriptionWhen Used
status.default.pendingInitialInitial state, awaiting processingDebt just created
status.default.in_progressActiveActive collection in progressCollection process started
status.default.processingActiveCurrently being processedIn active workflow
status.default.settledResolvedDebt settled through payment planPayment plan completed
status.default.paidResolvedFully paid offFull payment received
status.default.partialActivePartially paidSome payment received
status.default.failedClosedCollection failedCannot collect (various reasons)
status.default.archivedClosedArchived/closedManually archived
status.default.invalid_phoneClosedInvalid contact informationPhone number invalid/blocked
status.default.on_holdPausedOn hold (typically due to dispute)Collection paused (dispute active)
status.default.no_answerActiveVoicemail/no answerMultiple failed contact attempts
status.default.refusedClosedDebtor refused to payExplicit 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):
StatusTypeDescriptionUse Case
on_holdPendingFollowup is queued and waiting to be processedInitial state before execution
scheduledPendingFollowup is scheduled for a future timePlanned for specific datetime
in_progressActiveFollowup is currently being processedCall in progress
sentSuccessMessage (email/SMS/WhatsApp) has been sentEmail/SMS successfully sent
deliveredSuccessMessage was successfully deliveredConfirmed delivery
deal_foundSuccessCustomer agreed to a payment arrangementPositive outcome from call
no_answerRetryCall was not answeredCustomer didn’t pick up (voicemail)
deal_not_foundCompletedCustomer refused or no agreement reachedNegative outcome from call
bad_behaviorFailedNegative behavior detected (abusive, threats, etc.)Customer was inappropriate
failedFailedFollowup permanently failedTechnical failure or error
cancelledCancelledFollowup was manually cancelledCancelled 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 SettingsExternal ServicesWebhook EndpointsDeliveries.
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:
  1. Go to SettingsExternal Services
  2. Find your webhook endpoint
  3. 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.