Integrations

Webhooks

·3 min read

Webhooks

Webhooks are HTTP POST requests HandyBook sends to your URL whenever something happens (invoice paid, job created, customer added, etc.). Useful for real-time integrations without polling.

How they work

  1. You set up a webhook endpoint — a URL on your server that accepts POSTs
  2. You register the URL with HandyBook and pick events to subscribe to
  3. When subscribed events fire, HandyBook POSTs a JSON payload to your URL
  4. Your server processes it and responds with HTTP 200 OK

Setting up a webhook

  1. Settings → Developer → Webhooks → + New Webhook
  2. Enter your endpoint URL (must be HTTPS, must respond within 5 seconds)
  3. Pick events to subscribe to:
    • customer.created
    • customer.updated
    • job.created
    • job.status_changed
    • quote.sent
    • quote.signed
    • invoice.sent
    • invoice.paid
    • invoice.overdue
    • payment.created
    • payment.refunded
    • form.submitted
    • expense.created
    • And ~20 more — see docs
  4. Save — HandyBook sends a test event to verify the URL responds

Payload format

Standard envelope with event metadata + the resource data:

{
  "id": "evt_abc123",
  "type": "invoice.paid",
  "created_at": "2026-05-03T14:23:11Z",
  "data": {
    "id": "inv_xyz789",
    "number": "INV-1234",
    "customer_id": "cust_456",
    "amount": 28500,
    "currency": "USD",
    "paid_at": "2026-05-03T14:23:09Z",
    ...
  }
}

Amounts are in cents/smallest currency unit.

Signature verification

Each webhook request includes a signature header:

HandyBook-Signature: t=1683123456,v1=abc123def...

Verify by:

  1. Computing HMAC-SHA256 of {timestamp}.{payload} with your webhook secret
  2. Comparing to v1 value
  3. Rejecting if mismatch (likely forged)
  4. Rejecting if t is more than 5 minutes old (replay attack)

Sample code in JavaScript / Python in the docs.

Retries

If your endpoint returns non-200 or doesn't respond within 5 seconds, HandyBook retries:

  • After 30 seconds
  • Then 1 minute
  • Then 5 minutes
  • Then 30 minutes
  • Then 1 hour
  • Then every 6 hours, up to 3 days
  • Then gives up

View delivery attempts in Settings → Developer → Webhooks → [endpoint] → Logs.

Idempotency

Because of retries, your endpoint may receive the same event multiple times. Make your processing idempotent — store event.id and skip if already processed.

Common patterns

Update an external CRM

  • Listen for customer.updated
  • Push relevant fields to your other CRM

Trigger a Slack notification

  • Listen for invoice.paid with amount > $5,000
  • Post to Slack via Incoming Webhook

Sync to a data warehouse

  • Listen for any event
  • Append to a BigQuery / Snowflake table
  • Build custom analytics

Trigger a custom workflow

  • Listen for job.status_changed to complete
  • Trigger your custom post-job process (insurance updates, certifications, etc.)

Testing locally

For local development:

  • Use a tunneling tool like ngrok or Cloudflare Tunnel to expose your local endpoint
  • Or use a tool like Webhook.site to inspect payloads first

Pausing / disabling

Toggle a webhook Paused while debugging. Events queue up and resume when you un-pause (within 24 hours).

Best practices

  • Process asynchronously. Receive the webhook → enqueue → respond 200 immediately. Process in a background worker.
  • Verify signatures. Always.
  • Handle retries gracefully. Idempotent processing.
  • Log everything. Webhooks fail silently otherwise.
  • Use ngrok or similar during development.

FAQ

Are webhooks free? Yes — included on all tiers. Volume-related limits are very high; not something most users hit.

Can I send the webhook to multiple URLs? Yes — register multiple endpoints, each with its own event subscription.

What if my server is down — do I lose events? Events are retried over 3 days. As long as your server is back within that window, you'll receive them.

Is the payload encrypted? TLS (HTTPS) protects the connection. The payload itself isn't separately encrypted — but the signature ensures authenticity. Don't put sensitive secrets in your URL path; everything's logged.

Can I get historical events? Yes — via API: GET /v1/events?since=.... Up to 30 days of event history.

Was this article helpful?

We're not tracking clicks here — if anything's missing, just email us and we'll fix the doc.

Send feedback

More in Integrations