Developers

Webhooks and API reference

BidTorch's integration surface is outbound webhooks. Every business event POSTs JSON to the destinations a company configures; Make, Zapier, n8n or your own code routes on the event field. There is no app to build and publish first.

Overview

Two kinds of destination. A primary webhook URL receives every event, unsigned. Any number of webhook endpoints can be added on top, each with an optional per-event filter and an HMAC signing secret. Both are configured under Settings → Integrations. Zapier subscribes through the REST-hook endpoints below automatically.

Authentication

Settings → Integrations → API key issues a per-company bearer key (btk_…). Only a hash is stored; the plaintext is shown once. Send it as Authorization: Bearer btk_… (or X-API-Key) to the REST endpoints. Regenerating the key revokes the old one.

Webhooks

POST, Content-Type: application/json, User-Agent: BidTorch/1.0. Every payload carries event, event_id (a uuid, stable across retries — the de-duplication key), sent_at, and an org block with the company's id, name, phone and email.

Durable, at-least-once delivery. Every event is persisted and attempted immediately. A delivery that fails (network, timeout, or a non-2xx response) is retried with exponential backoff — about a minute at first, capped at six hours, up to eight attempts — before being parked. Because retries mean an event can arrive more than once, consumers must de-duplicate on event_id: treat the webhook as a trigger, not a ledger. Ten-second timeout per endpoint; endpoints fire concurrently, so one slow endpoint never blocks another. Each endpoint shows its last delivery time and HTTP status in Settings.

Signing. Deliveries to an endpoint with a secret carry:

X-BidTorch-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>

Verify by recomputing the HMAC over "<t>.<raw body>" with the secret shown when the endpoint was added, and optionally rejecting a stale t. The primary webhook URL and Zapier subscriptions are unsigned; Zapier's per-Zap target URLs are unguessable, which is the standard REST-hook trust model.

Events

Every event, when it fires, and the root key of its payload.

EventFires whenPayload root
quote.sentQuote goes out to the customer (emailed in-app or marked sent)quote
quote.acceptedCustomer accepts an option on the public quote link (signature required)quote + accepted_option
quote.signedSame moment as quote.accepted — signature is required to accept, so both fire together. Subscribe to either.quote + accepted_option
quote.expiredNightly sweep: a sent quote passed its expires_at unaccepted (deadline = send time + the org's valid-days setting)quote
job.scheduledQuote converted to job / job scheduledjob
job.assignedJob assigned to a crew member (notify the tech)job + assignee
job.rescheduledA scheduled job's date/time movesjob
job.startedStatus → in progressjob
job.completedStatus → completed (the review-request trigger)job
job.cancelledStatus → cancelledjob
invoice.sentInvoice status → sent (Stripe link minted if configured)invoice + items
invoice.paidInvoice marked paidinvoice + items
lead.createdPublic lead form submissionlead
lead.status_changedLead moved on the kanbanlead
inventory.low_stockStock crosses below an item's reorder point (fires once per low-stock episode, re-armed on restock)item
task.assignedTask assigned to a teammatetask
task.completedTask marked donetask
sms.outboundA rep sends a text from the Messages inbox — carry this to Twilio (BidTorch stores the message but never sends it)message_id, to, body, customer_id, quote_id, job_id
sms.receivedAn inbound reply was posted to /api/sms/inbound (use for auto-reply / notify-the-rep)message_id, from, body, customer_id
change_order.sentChange order sent to the customerchange_order + items
change_order.approvedCustomer approves the change order (signature)change_order + items
change_order.declinedCustomer declines the change orderchange_order + items

Every job.* payload also carries an assignee object (id, name, email, phone, role — or null when unassigned), which is what a scenario reads to text or email the assigned tech on job.assigned.

Payloads

quote.accepted and quote.signed fire together — signing is acceptance — so subscribe to one. Their payload includes company-side economics (cost basis, gross profit, margin) that never appear on the public quote. pdf_url is the rendered PDF of the signed quote, a signed download link valid about seven days, ample for a scenario to run and retry. quote.sent and quote.expired carry a slim quote object with no economics.

quote.accepted / quote.signed

{
  "event": "quote.accepted",
  "sent_at": "2026-06-11T18:00:00Z",
  "org": { "id": "…", "name": "…", "phone": "…", "email": "…", "address": "…", "website": "…" },
  "quote": {
    "id": "…", "quote_number": "AP-2026-014", "status": "accepted",
    "customer_name": "…", "customer_company": null, "customer_email": "…", "customer_phone": "…",
    "customer_address": "…", "job_address": "…",
    "subtotal": 0, "tax_amount": 0, "total": 0,
    "cost_basis": 0, "gross_profit": 0, "margin_pct": 0,
    "accepted_option_id": "…", "accepted_at": "…", "signed_at": null,
    "has_signature": false, "share_token": "…", "public_path": "/q/…",
    "public_url": "https://…/q/…", "pdf_url": "https://…/q/…?pdf=1", "notes": null
  },
  "accepted_option": {
    "id": "…", "tier": "better", "name": "…",
    "subtotal": 0, "svc_call_amount": 0, "total": 0,
    "cost_basis": 0, "margin_pct": 0, "notes": null,
    "items": [
      { "item_type": "service", "description": "…", "qty": 1, "unit": "ea",
        "unit_cost": 0, "markup_pct": 0, "line_total": 0,
        "store": null, "po_number": null, "note": null }
    ]
  }
}

job.*

{
  "event": "job.completed",
  "sent_at": "…",
  "org": { "id": "…", "name": "…", "phone": "…", "email": "…" },
  "job": {
    "id": "…", "job_number": "JOB-2026-007", "title": "…", "status": "completed",
    "scheduled_at": "…", "started_at": "…", "completed_at": "…",
    "customer_name": "…", "customer_email": "…", "customer_phone": "…",
    "job_address": "…", "total": 0, "notes": null, "quote_id": "…", "created_at": "…"
  }
}

invoice.sent / invoice.paid

{
  "event": "invoice.paid",
  "sent_at": "…",
  "org": { "id": "…", "name": "…", "phone": "…", "email": "…" },
  "invoice": {
    "id": "…", "invoice_number": "…", "status": "paid",
    "customer_name": "…", "customer_email": "…", "customer_phone": "…",
    "customer_address": "…", "job_address": "…",
    "subtotal": 0, "tax_amount": 0, "amount_due": 0, "amount_paid": 0,
    "stripe_payment_link_url": null, "due_date": "…",
    "sent_at": "…", "paid_at": "…", "share_token": "…", "public_path": "/i/…", "org_id": "…"
  },
  "items": [
    { "description": "…", "qty": 1, "unit": "ea", "unit_price": 0, "line_total": 0, "sort_order": 0 }
  ]
}

lead.*

{
  "event": "lead.created",
  "sent_at": "…",
  "org": { "id": "…", "name": "…", "phone": "…", "email": "…" },
  "lead": {
    "id": "…", "status": "new", "name": "…", "email": "…", "phone": "…",
    "address": "…", "service_type": "…", "message": "…", "source": "…", "created_at": "…"
  }
}

REST endpoints

The small authenticated surface: the REST-hook subscribe/unsubscribe pair Zapier uses, a sample payload per event for a trigger test, and the two inbound sinks.

EndpointPurpose
GET /api/zapier/meAuth test — returns { id, name }, labels the Zapier connection
POST /api/zapier/hooksSubscribe: { "target_url": "https://…", "event": "quote.accepted" } → { "id" } (201)
DELETE /api/zapier/hooks/{id}Unsubscribe (idempotent — already-gone is success)
GET /api/zapier/samples/{event}One static sample payload (exact live shape) for the Zap editor's trigger test
POST /api/sms/inboundInbound SMS sink — a Make scenario on Twilio's incoming-message webhook POSTs { "from", "body", "provider_sid?" } here (same Bearer btk_… auth) → stored as an inbound message, returns { "id" } (201)
POST /api/leads/inboundInbound lead sink — an outside automation (AI phone concierge, chatbot, zap) POSTs { "name", "phone?", "email?", "address?", "service_type?", "message?", "source?" } (same Bearer btk_… auth) → lands in the Leads pipeline as new and fires lead.created, returns { "id" } (201)

Inbound leads and SMS

An outside form, chatbot, phone system or Zap can create a lead. It lands in the pipeline as new and fires lead.created:

curl -X POST https://bidtorch.com/api/leads/inbound \
  -H "Authorization: Bearer btk_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Pat Doe","phone":"317-555-0100","service_type":"Water heater","message":"No hot water since this morning","source":"website"}'

Two-way SMS has two transports, chosen per company. The default is webhook-based: a rep sends from the Messages inbox, BidTorch stores it and fires sms.outbound for your scenario to carry to Twilio; a reply comes back through POST /api/sms/inbound. The native transport talks to the company's own Twilio account directly with real delivery status, and inbound messages arrive on a signed, per-company Twilio webhook URL shown in Settings. Carrier registration for business texting applies either way.

Native integrations

  • Stripe — bring your own key; deposits on accept and invoice pay-links. Saving the key registers the payment-notification receiver on your account.
  • Square — connect the account; a per-company signed webhook receiver reconciles payments to the invoice.
  • QuickBooks Online — OAuth connect; sent invoices push to QuickBooks and payments recorded there reconcile the BidTorch invoice. Pulling customers or estimates into BidTorch is not built in.
  • Calendar — a read-only ICS feed (Settings → Calendar feed) that Google, Apple and Outlook subscribe to. Regenerating the token revokes every previously shared URL.
  • Everything else — HubSpot, Google Drive, Slack, review tools: webhook plus Make or Zapier.

Questions

Do I have to build and publish an app to get BidTorch events?

No. Paste a destination URL under Settings → Integrations and BidTorch POSTs the full event as JSON — signed, retried, with a stable event_id for de-duplication. Make, Zapier, n8n or your own code can listen.

Is the webhook delivered exactly once?

At least once. A failed delivery (network, timeout, non-2xx) is retried with exponential backoff, up to eight attempts, so an event can arrive more than once. De-duplicate on event_id; treat the webhook as a trigger, and the API or CSV export as the source of truth.

How do I get data INTO BidTorch?

POST a lead to /api/leads/inbound with your company API key and it lands in the pipeline as new and fires lead.created. Inbound SMS replies post to /api/sms/inbound the same way. Everything else today is outbound.

Does BidTorch have a Zapier app?

Yes, a triggers-only app that authenticates with the same company API key and subscribes through the REST-hook endpoints below. It is invite-only while the public listing is pending — ask support for the link.

Something missing? Ask support — a person answers.