Developers
Webhooks
Subscribe to events instead of polling: event types, delivery guarantees, signature verification and retry behaviour.
Webhooks let your systems hear about changes without polling. Configure endpoints in Settings → Developers → Webhooks.
Events
| Event | Fires when |
| --- | --- |
| request.created | Inbound work arrives from any channel |
| quote.sent / quote.accepted / quote.declined | A quote changes state |
| job.created / job.completed | A job opens or closes |
| visit.scheduled / visit.rescheduled / visit.completed | The board changes |
| invoice.issued / invoice.paid / invoice.overdue | Money moves, or does not |
| payment.succeeded / payment.failed | A payment attempt resolves |
Delivery
Delivery is at least once. Your endpoint must be idempotent: every payload
carries an id, and processing the same id twice must be safe. This is not a
theoretical concern: a timeout on your side after you have already committed
produces exactly this case.
Failures retry with exponential backoff for 24 hours: after 1 minute, then 5, 25, 2 hours, 6 hours, 24 hours. An endpoint that fails every attempt for three consecutive days is disabled, and the account owner is emailed.
Respond 2xx quickly, then do the work. A webhook handler that waits on a slow
third party is a webhook handler that times out.
Verifying the signature
Every delivery carries X-Elevate-Signature and X-Elevate-Timestamp. The
signature is an HMAC-SHA256 over timestamp + "." + raw body, using the
endpoint's signing secret.
expected = OpenSSL::HMAC.hexdigest(
"SHA256", secret, "#{timestamp}.#{raw_body}"
)
valid = ActiveSupport::SecurityUtils.secure_compare(expected, signature)
Reject anything whose timestamp is more than five minutes old, and compare in constant time. Verify against the raw body: a re-serialised payload will not match, which is the single most common cause of "the signature never validates".
Payload shape
{
"id": "evt_3fa85f64",
"type": "invoice.paid",
"created_at": "2026-09-15T09:03:11Z",
"data": {
"invoice": { "id": 4417, "total_cents": 124000, "currency": "USD" },
"job": { "id": 8812 },
"client": { "id": 331, "name": "Bridgeport HOA" }
}
}
Payloads carry identifiers and the fields most consumers need. Fetch the full record from the REST API when you need more, instead of relying on a payload staying the same shape forever. New fields are added without notice; existing fields are not removed without it.