Skip to content
Driive Help Center home
Driive Help Center home

Webhook Events and Security

This article covers everything you need to know about the data your webhooks deliver and how to ensure that data is authentic. You'll find the full list of available events, an example payload, instructions for verifying HMAC signatures, and details on retry behavior and secret rotation.


Available events

Driive can send webhook notifications for the following events. When creating a webhook, you choose which events to subscribe to.

New: service requests. A new lead now enters Driive as a service request — the raw inquiry from your website form, a phone call, a text, an email, or your team — and fires service_request.created. Once that request is qualified and committed, it produces an appointment, which fires appointment.created. If you previously relied on appointment.created to catch new leads, subscribe to service_request.created instead. See the Service request webhook migration guide for the full walkthrough.

Service request events

EventFires when
service_request.createdA new lead / service request is created
service_request.updatedA service request is modified (info gathered, qualified, approved, or archived)

Tip: Subscribe to service_request.* to receive all service request events with a single subscription. service_request.updated is debounced — rapid edits to the same request (common while a customer is filling out a form) collapse into a single delivery, roughly once per minute. Preview/test requests are never delivered.

Appointment events

EventFires when
appointment.createdAn appointment is created from a service request — at needs_scheduled, or scheduled when booked immediately. This no longer fires for new leads — see the service request events above.
appointment.updatedAppointment details or status are changed
appointment.confirmedA time slot is confirmed for the appointment
appointment.cancelledThe appointment is cancelled
appointment.rescheduledThe appointment is moved to a new date and time

Tip: You can subscribe to appointment.* to receive all appointment events with a single subscription. This is useful if you want to keep an external system fully in sync without selecting individual events.

Customer events

EventFires when
customer.createdA new customer is created
customer.updatedA customer's details change (name, email, phone, tags, etc.)

Tip: Subscribe to customer.* to receive all customer events with a single subscription.

Issue events

EventFires when
issue.createdA new issue appears in the Inbox — something on a request or appointment needs a human

The issue.* wildcard is also available and currently matches this single event — subscribe to it if you want to automatically pick up any issue events added in the future.


Payload structure

Every webhook delivery sends a JSON payload as an HTTP POST to your destination URL. All payloads follow the same top-level envelope.

Delivery envelope

FieldTypeDescription
idstringUnique delivery ID for deduplication
typestringThe event type (e.g., appointment.created)
previousDataobject or nullThe pre-update snapshot on *.updated events, in the same shape as data. Lets you diff what changed without keeping your own copy. null on *.created and other events.
dataobjectThe event-specific data (varies by event type — see below)
createdAtstringISO 8601 timestamp of when the event occurred

Appointment event data fields

All appointment.* events include the following fields in the data object:

FieldTypeDescription
appointmentIdstringUnique appointment ID
organizationIdstringYour organization ID
appointmentTypeIdstringThe appointment type ID
appointmentTypeNamestringThe appointment type name (e.g., "Roof Inspection")
statusstringCurrent appointment status (see Appointment statuses below)
typestringMeeting type: phone, in_person, virtual, or other
customerobjectCustomer details (see below)
customerNotesstring or nullNotes the customer provided
descriptionstring or nullInternal description of the appointment
startTimestring or nullScheduled start time (ISO 8601)
endTimestring or nullScheduled end time (ISO 8601)
durationnumber or nullDuration in minutes
locationobject or nullService address
leadScorenumber or nullLead qualification score (0-100)
bookedAtstring or nullWhen the appointment was booked
cancelledAtstring or nullWhen the appointment was cancelled
cancellationReasonstring or nullReason for cancellation
utmParamsobject or nullUTM tracking parameters captured from the booking URL
bookingUrlstringCustomer-facing booking link for this appointment
timezonestring or nullThe timezone used for the local time fields (the customer's timezone, when known)
startTimeLocalstring or nullStart time in that timezone, with UTC offset
endTimeLocalstring or nullEnd time in that timezone, with UTC offset
assignedTeamMembersarrayUsers assigned to this appointment — each entry is { id, firstName, lastName, email, avatarUrl }
leadTechobject or nullThe lead user on the appointment, in the same shape as an assignedTeamMembers entry
createdAtstringWhen the appointment was created (ISO 8601)
updatedAtstringWhen the appointment was last updated (ISO 8601)
legacybooleanPresent only on the deprecated compatibility bridge. See Legacy appointment.created bridge.
deprecationNoticestringPresent only on the deprecated compatibility bridge — a human-readable migration notice.

Appointment statuses

Gathering and approval now happen on the service request, not the appointment. As a result, the former needs_information and needs_approval statuses no longer appear on appointments — a qualified, committed request produces an appointment that starts at needs_scheduled (or scheduled when booked immediately).

StatusMeaning
needs_scheduledThe appointment exists but has no confirmed date and time yet. Awaiting scheduling.
scheduledA date and time are confirmed. On the calendar.
completedThe service was delivered. The job is done.
cancelledThe appointment was cancelled by the customer or your team.

These four are the only appointment statuses. Archiving is not a status — an archived appointment keeps its status and is simply hidden from the default views.

Customer object:

This shape is shared by both appointment.* and service_request.* payloads.

FieldTypeDescription
firstNamestringCustomer's first name
lastNamestringCustomer's last name
emailstring or nullEmail address
phonestring or nullPhone number
timezonestring or nullCustomer's timezone
streetstring or nullStreet address
citystring or nullCity
statestring or nullState
zipstring or nullZIP code
latstring or nullLatitude of the geocoded address
lonstring or nullLongitude of the geocoded address
placeIdstring or nullGeocoder place ID for the address

Service request event data fields

All service_request.* events include the following fields in the data object. Fields shared with the appointment payload keep the same shape and meaning.

FieldTypeDescription
serviceRequestIdstringUnique service request ID — the primary identifier for the lead
organizationIdstringYour organization ID
customerIdstring or nullThe linked customer record's ID
customerobject or nullCustomer details (same shape as the appointment customer object)
appointmentTypeIdstring or nullThe appointment type ID, if known yet
appointmentIdstring or nullThe appointment produced from this request, once one exists — your join key to appointment.* events
channelstringHow the lead arrived: phone, sms, chat, email, companycam, web_form, preview, manual, or leadconnector
originstringWhat created the request: agent, team_member, public_booking, email_forward, web_form, companycam, manual, or leadconnector
issueobject or nullThe open "needs attention" issue: { id, reasons[], pinned }. See Issue reasons below.
questionAnswersarray or nullAnswers to your booking questions (questionId, questionLabel, answerValue, answerText, answerType)
leadScorenumber or nullLead qualification score (0-100)
disqualificationReasonstring or nullWhy the lead failed qualification, if it did
summarystring or nullHuman-readable summary of the request
addressstring or nullThe raw address string as entered (location is the geocoded version)
locationobject or nullGeocoded service address (same shape as the appointment location)
utmParamsobject or nullUTM tracking parameters captured from the booking URL
customFieldsobject or nullCustom field key/value map
metadataobjectFree-form metadata
previewboolean or nullPreview/test flag (preview requests are never delivered, so expect false)
approvedAtstring or nullWhen the request was approved, if it was
deletedAtstring or nullArchive marker — set when the request is archived
createdAtstringWhen the request was created (ISO 8601)
updatedAtstringWhen the request was last updated (ISO 8601)

Service request issue reasons

A service request's issue.reasons array tells you why it needs attention. It replaces the appointment status as the "what needs a human" signal at the lead stage.

ReasonMeaning
needs_informationIncomplete — waiting on more details before it can move forward
failed_qualificationGathered and complete, but the lead scored below your threshold
out_of_service_areaGathered and complete, but the location is outside your service area
needs_user_approvalGathered and complete, but the appointment type requires your sign-off
manual_reviewFlagged by a user for a human to look at
user_cancelledThe customer cancelled the appointment via the public booking link
call_dropA voice/agent call dropped without producing an appointment

Customer event data fields

All customer.* events include the following fields in the data object:

FieldTypeDescription
customerIdstringUnique customer ID
organizationIdstringYour organization ID
firstNamestringCustomer's first name
lastNamestringCustomer's last name
businessNamestring or nullBusiness name, if any
emailstring or nullEmail address
phonestring or nullPhone number
streetstring or nullStreet address
citystring or nullCity
statestring or nullState
zipstring or nullZIP code
latstring or nullLatitude of the geocoded address
lonstring or nullLongitude of the geocoded address
placeIdstring or nullGeocoder place ID for the address
timezonestring or nullCustomer's timezone
statusstringCustomer status: lead, customer, or inactive
sourcestringWhere the customer came from (e.g., booking, manual, import)
notesstring or nullNotes on the customer
tagsarray or nullTags applied to the customer
customFieldsobject or nullCustom field key/value map
deletedAtstring or nullArchive marker — set when the customer is archived
createdAtstringWhen the customer was created (ISO 8601)
updatedAtstringWhen the customer was last updated (ISO 8601)

Issue event data fields

The issue.created event includes the following fields in the data object:

FieldTypeDescription
issueIdstringUnique issue ID
organizationIdstringYour organization ID
objectTypestringWhat the issue points at: service_request or appointment
serviceRequestIdstring or nullThe request that needs attention — set when objectType is service_request
appointmentIdstring or nullThe appointment that needs attention — set when objectType is appointment
reasonsarrayWhy it needs attention — one or more reason codes. Service request issues use the issue reasons above; appointment issues use needs_scheduled (an appointment exists but has no time yet).
pinnedbooleanWhether the issue is pinned to the top of the Inbox
createdAtstringWhen the issue was created (ISO 8601)
updatedAtstringWhen the issue was last updated (ISO 8601)

Exactly one of serviceRequestId / appointmentId is set, matching objectType.

Example: appointment.created

{ "id": "del_abc123", "type": "appointment.created", "createdAt": "2025-01-15T10:30:00Z", "data": { "appointmentId": "apt_abc123", "organizationId": "org_xyz789", "appointmentTypeId": "aty_def456", "appointmentTypeName": "Roof Inspection", "status": "needs_scheduled", "type": "in_person", "customer": { "firstName": "John", "lastName": "Smith", "email": "john@example.com", "phone": "+15551234567", "timezone": "America/Denver", "street": "123 Main St", "city": "Denver", "state": "CO", "zip": "80202", "lat": "39.7392", "lon": "-104.9903", "placeId": "ChIJ-aBc123" }, "startTime": null, "endTime": null, "duration": 60, "location": { "street": "123 Main St", "city": "Denver", "state": "CO", "zip": "80202", "lat": "39.7392", "lon": "-104.9903" }, "leadScore": 85, "bookedAt": null, "cancelledAt": null, "cancellationReason": null, "bookingUrl": "https://app.getdriive.com/l/apt_abc123", "timezone": "America/Denver", "startTimeLocal": null, "endTimeLocal": null, "assignedTeamMembers": [], "leadTech": null, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } }

Example: appointment.confirmed

{ "id": "del_ghi789", "type": "appointment.confirmed", "createdAt": "2025-01-15T14:00:00Z", "data": { "appointmentId": "apt_abc123", "organizationId": "org_xyz789", "appointmentTypeId": "aty_def456", "appointmentTypeName": "Roof Inspection", "status": "scheduled", "customer": { "firstName": "John", "lastName": "Smith", "email": "john@example.com", "phone": "+15551234567", "timezone": "America/Denver", "street": "123 Main St", "city": "Denver", "state": "CO", "zip": "80202", "lat": "39.7392", "lon": "-104.9903", "placeId": "ChIJ-aBc123" }, "startTime": "2025-01-20T16:00:00Z", "endTime": "2025-01-20T17:00:00Z", "duration": 60, "location": { "street": "123 Main St", "city": "Denver", "state": "CO", "zip": "80202", "lat": "39.7392", "lon": "-104.9903" }, "leadScore": 85, "bookedAt": "2025-01-15T14:00:00Z", "cancelledAt": null, "cancellationReason": null, "bookingUrl": "https://app.getdriive.com/l/apt_abc123", "timezone": "America/Denver", "startTimeLocal": "2025-01-20T09:00:00-07:00", "endTimeLocal": "2025-01-20T10:00:00-07:00", "assignedTeamMembers": [ { "id": "mem_xyz789", "firstName": "Alex", "lastName": "Rivera", "email": "alex@example.com", "avatarUrl": null } ], "leadTech": { "id": "mem_xyz789", "firstName": "Alex", "lastName": "Rivera", "email": "alex@example.com", "avatarUrl": null }, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T14:00:00Z" } }

Example: appointment.rescheduled

{ "id": "del_jkl012", "type": "appointment.rescheduled", "createdAt": "2025-01-16T09:00:00Z", "data": { "appointmentId": "apt_abc123", "organizationId": "org_xyz789", "appointmentTypeId": "aty_def456", "appointmentTypeName": "Roof Inspection", "status": "scheduled", "customer": { "firstName": "John", "lastName": "Smith", "email": "john@example.com", "phone": "+15551234567", "timezone": "America/Denver", "street": "123 Main St", "city": "Denver", "state": "CO", "zip": "80202", "lat": "39.7392", "lon": "-104.9903", "placeId": "ChIJ-aBc123" }, "startTime": "2025-01-22T16:00:00Z", "endTime": "2025-01-22T17:00:00Z", "duration": 60, "location": { "street": "123 Main St", "city": "Denver", "state": "CO", "zip": "80202", "lat": "39.7392", "lon": "-104.9903" }, "leadScore": 85, "bookedAt": "2025-01-15T14:00:00Z", "cancelledAt": null, "cancellationReason": null, "bookingUrl": "https://app.getdriive.com/l/apt_abc123", "timezone": "America/Denver", "startTimeLocal": "2025-01-22T09:00:00-07:00", "endTimeLocal": "2025-01-22T10:00:00-07:00", "assignedTeamMembers": [ { "id": "mem_xyz789", "firstName": "Alex", "lastName": "Rivera", "email": "alex@example.com", "avatarUrl": null } ], "leadTech": { "id": "mem_xyz789", "firstName": "Alex", "lastName": "Rivera", "email": "alex@example.com", "avatarUrl": null }, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-16T09:00:00Z" } }

Example: service_request.created

{ "id": "del_pqr678", "type": "service_request.created", "previousData": null, "createdAt": "2025-01-15T10:30:00Z", "data": { "serviceRequestId": "sr_abc123", "organizationId": "org_xyz789", "customerId": "cus_def456", "appointmentTypeId": "aty_def456", "appointmentId": null, "channel": "web_form", "origin": "public_booking", "customer": { "firstName": "John", "lastName": "Smith", "email": "john@example.com", "phone": "+15551234567", "timezone": "America/Denver", "street": "123 Main St", "city": "Denver", "state": "CO", "zip": "80202", "lat": "39.7392", "lon": "-104.9903", "placeId": "ChIJ-aBc123" }, "issue": { "id": "iss_123", "reasons": ["needs_information"], "pinned": false }, "questionAnswers": [ { "questionId": "q_1", "questionLabel": "What's going on?", "answerValue": "Roof is leaking after the storm", "answerText": "Roof is leaking after the storm", "answerType": "text" } ], "leadScore": 85, "disqualificationReason": null, "summary": "Customer reports a leaking roof after last week's storm.", "address": "123 Main St, Denver, CO 80202", "location": { "street": "123 Main St", "city": "Denver", "state": "CO", "zip": "80202", "lat": "39.7392", "lon": "-104.9903" }, "utmParams": { "utm_source": "google", "utm_medium": "cpc", "utm_campaign": "spring-roofing" }, "customFields": { "referral_code": "SPRING25" }, "metadata": {}, "preview": false, "approvedAt": null, "deletedAt": null, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } }

Example: customer.created

{ "id": "del_mno345", "type": "customer.created", "previousData": null, "createdAt": "2025-01-15T10:30:00Z", "data": { "customerId": "cus_def456", "organizationId": "org_xyz789", "firstName": "John", "lastName": "Smith", "businessName": null, "email": "john@example.com", "phone": "+15551234567", "street": "123 Main St", "city": "Denver", "state": "CO", "zip": "80202", "lat": "39.7392", "lon": "-104.9903", "placeId": "ChIJ-aBc123", "timezone": "America/Denver", "status": "lead", "source": "booking", "notes": null, "tags": ["roofing"], "customFields": null, "deletedAt": null, "createdAt": "2025-01-15T10:30:00Z", "updatedAt": "2025-01-15T10:30:00Z" } }

Example: issue.created

{ "id": "del_stu901", "type": "issue.created", "previousData": null, "createdAt": "2025-01-15T10:31:00Z", "data": { "issueId": "iss_123", "organizationId": "org_xyz789", "objectType": "service_request", "serviceRequestId": "sr_abc123", "appointmentId": null, "reasons": ["needs_information"], "pinned": false, "createdAt": "2025-01-15T10:31:00Z", "updatedAt": "2025-01-15T10:31:00Z" } }

Legacy appointment.created bridge (deprecated)

To keep existing integrations working during the move to service requests, Driive offers an opt-in compatibility bridge. When it's enabled for your organization, a new service request also emits a stand-in appointment.created event, so an un-migrated integration keeps receiving new leads.

The bridge is a stopgap and is deliberately easy to detect:

  • legacy is true.
  • deprecationNotice carries a message explaining the deprecation.
  • appointmentId is actually the serviceRequestId — there is no real appointment behind it.
  • status is hardcoded to needs_information.
  • bookingUrl points at the public request page (/r/...), not an appointment.
{ "id": "del_def456", "type": "appointment.created", "data": { "appointmentId": "sr_abc123", "status": "needs_information", "legacy": true, "deprecationNotice": "DEPRECATED legacy compatibility event. `appointmentId` is actually a serviceRequestId. Migrate to the `service_request.created` webhook — this payload will be removed soon.", "...": "..." } }

Important: The bridge is off by default and will be removed. Don't build anything new on it. Once you've migrated to service_request.created, ignore (and drop) any appointment.created payload where legacy is true. See the Service request webhook migration guide for the full migration path.


HMAC signature verification

Every webhook delivery includes an X-Webhook-Signature header. This header contains a timestamp and an HMAC-SHA256 hash that you should verify to confirm the payload was sent by Driive and hasn't been tampered with.

The header looks like this:

X-Webhook-Signature: t=1736937000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t is a Unix timestamp (in seconds) of when the delivery was signed.
  • v1 is a hex-encoded HMAC-SHA256 signature.

How it works

  1. Driive takes the timestamp and the raw request body, joined with a period: {timestamp}.{payload}.
  2. Driive computes an HMAC-SHA256 hash of that string using your webhook's signing secret as the key.
  3. The timestamp and hash are sent together in the X-Webhook-Signature header as t={timestamp},v1={signature}.
  4. Your server extracts t and v1 from the header, performs the same computation over {t}.{raw body}, and compares its result to v1.

Because the timestamp is part of the signed content, you can also reject stale deliveries (say, older than five minutes) to protect against replay attacks.

Verification pseudocode

t, v1 = parse(request.headers["X-Webhook-Signature"]) // "t=...,v1=..." expected = HMAC-SHA256(signing_secret, t + "." + request_body) if (constant_time_equal(expected, v1)) { // Payload is authentic — process it } else { // Payload may be forged — reject it }

Verification example (Node.js)

const crypto = require("crypto"); function verifyWebhook(signingSecret, requestBody, signatureHeader) { // Header format: "t={timestamp},v1={signature}" const parts = Object.fromEntries( signatureHeader.split(",").map((pair) => pair.split("=")) ); const { t: timestamp, v1: signature } = parts; if (!timestamp || !signature) return false; const expected = crypto .createHmac("sha256", signingSecret) .update(`${timestamp}.${requestBody}`) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); }

Verification example (Python)

import hmac import hashlib def verify_webhook(signing_secret, request_body, signature_header): # Header format: "t={timestamp},v1={signature}" parts = dict(pair.split("=", 1) for pair in signature_header.split(",")) timestamp = parts.get("t") signature = parts.get("v1") if not timestamp or not signature: return False signed_payload = f"{timestamp}.{request_body}" expected = hmac.new( signing_secret.encode(), signed_payload.encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature)

Important: Always use a constant-time comparison function (like timingSafeEqual or compare_digest) to prevent timing attacks. And always compute the hash over the raw request body exactly as received — parsing and re-serializing the JSON can change whitespace or key order and break the signature.

Why verify signatures?

Without verification, anyone who discovers your webhook URL could send fake payloads to your server. Signature verification ensures:

  • The payload was sent by Driive, not a third party
  • The payload hasn't been modified in transit
  • Your integration only processes authentic events

HTTP headers

Every webhook delivery includes the following headers:

HeaderDescription
Content-Typeapplication/json
X-Webhook-IdThe ID of the webhook receiving this delivery — useful when several webhooks point at the same endpoint
X-Webhook-EventThe event type (e.g., service_request.created), so you can route without parsing the body
X-Webhook-Signaturet={timestamp},v1={signature} — see HMAC signature verification
User-AgentDriive-Webhooks/1.0

Timeouts and retry logic

Your endpoint has 30 seconds to respond to each delivery. If it doesn't respond in time, or responds with anything other than a 2xx status code (200-299), Driive treats the delivery as failed and retries automatically.

Retry behavior:

  • Retries: Up to 5 retry attempts after a failed delivery
  • Backoff strategy: Exponential backoff — each retry waits longer than the previous one

Failures never disable a webhook automatically — the endpoint keeps receiving new events. Its health is shown in the Status column on the Webhooks page: a Failing badge with the last error message and the number of consecutive failures.

Tips for handling retries:

  • Make your webhook endpoint idempotent — processing the same event twice should produce the same result. Use the delivery id field to detect duplicates.
  • Return a 200 status code as quickly as possible. If you need to do heavy processing, accept the webhook, return 200, and process the data asynchronously.
  • Keep an eye on the Status column on the Webhooks page for patterns of failure.

Secret rotation

If your signing secret is compromised (or as part of routine security hygiene), you can rotate it through the API:

POST /v1/organizations/{orgId}/webhooks/{webhookId}/rotate-secret

Driive generates a new signing secret and returns it once in the response — store it securely, because it can't be retrieved again. The old secret stops working immediately. See the interactive API reference for the full request and response details.

Tip: Because the old secret is invalidated the moment you rotate, be ready to update your server's verification logic right away. To avoid rejecting deliveries during the switchover, you can briefly accept either secret by checking the signature against both.


Best practices

  • Always verify signatures — Never skip HMAC verification in production
  • Respond quickly — Return 200 within a few seconds; process data asynchronously if needed
  • Handle duplicates — Your endpoint may receive the same event more than once due to retries
  • Log deliveries — Keep your own record of received webhooks for debugging
  • Use HTTPS — Your destination URL must use HTTPS to protect data in transit
  • Monitor delivery health — Check the Status column on the Webhooks page regularly and set up your own alerting for failed deliveries

Next steps