Skip to content

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.