Developer

Webhook

Webhook events

The booking.completed event, the JSON envelope and payload your endpoint receives, headers, and delivery and retry behavior.

Updated Sep 30, 2026

ServiceReserve sends a webhook event when a customer completes a booking. You configure endpoints on the Webhooks page. They apply to every booking form in the workspace. For the default webhook endpoint kind, each event arrives as a JSON POST. The other kinds are covered in Endpoint kinds.

Event types

ServiceReserve emits a single event today. Any events added later will use the same envelope.

booking.completed
event
Fires after the customer submits the booking form and the request is saved.

Envelope

Every payload has this top-level shape.

eventrequired
string
The event type, for example booking.completed.
organization_id
uuid
Your workspace id. Present on deliveries to endpoints configured on the Webhooks page.
form_id
uuid
Present only on legacy form-level endpoints. For workspace endpoints, read data.form_id to learn which form fired.
sent_atrequired
ISO 8601 string
When the delivery was queued. Retries of the same delivery keep the same value.
datarequired
object
Event-specific body. See the payload below.

booking.completed payload

{
  "event": "booking.completed",
  "organization_id": "9e8d7c6b-5a4f-4e3d-8c2b-1a0f9e8d7c6b",
  "sent_at": "2026-09-29T18:33:43.120Z",
  "data": {
    "request_id": "f0e9c1d8-94c3-4d50-8f81-2b63f4a47a4c",
    "form_id": "bd28906b-7f9a-4aaf-ab86-85c1a1ccd4f2",
    "contact": {
      "first_name": "Jane",
      "last_name": "Doe",
      "email": "[email protected]",
      "phone": "+15555550123",
      "company": null
    },
    "address": {
      "address": "123 Main St",
      "address2": "Unit 4",
      "city": "Austin",
      "state": "TX",
      "zip": "78701",
      "country": "US",
      "entrance_info": "Gate code 1234"
    },
    "category": {
      "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "name": "Drains",
      "intake": [
        { "question": "Which drain is clogged?", "answer": "Kitchen sink", "type": "select" }
      ],
      "services": [
        {
          "id": "6d1f0c2a-3b4e-4f5a-9c8d-7e6f5a4b3c2d",
          "name": "Drain cleaning",
          "price": 195,
          "priceFrom": null,
          "priceDisplay": true,
          "free": false,
          "durationMinutes": 60,
          "intake": []
        }
      ]
    },
    "services": [
      {
        "id": "6d1f0c2a-3b4e-4f5a-9c8d-7e6f5a4b3c2d",
        "name": "Drain cleaning",
        "price": 195,
        "priceFrom": null,
        "priceDisplay": true,
        "free": false,
        "durationMinutes": 60,
        "intake": []
      }
    ],
    "total_amount": 195,
    "intake_responses": [
      {
        "question": "How old is the system?",
        "answer": "5-10 years",
        "type": "select",
        "scope": "template",
        "scope_name": "Plumbing services"
      },
      {
        "question": "Which drain is clogged?",
        "answer": "Kitchen sink",
        "type": "select",
        "scope": "category",
        "scope_name": "Drains"
      }
    ],
    "template_intake": [
      { "question": "How old is the system?", "answer": "5-10 years", "type": "select" }
    ],
    "preferred_time": { "date": "2026-10-06", "slot": "morning" },
    "attribution": {
      "source": null,
      "page_url": "https://example.com/book?utm_source=google&utm_medium=cpc&utm_campaign=fall-sale",
      "referrer": "https://www.google.com/",
      "utm_source": "google",
      "utm_medium": "cpc",
      "utm_campaign": "fall-sale"
    },
    "profile": {
      "id": "52c58cc6-c7fb-4419-ad88-9c4a0e89ad2f",
      "address": "434 Northwest 1st Avenue, Fort Lauderdale, FL 33301",
      "city": "Fort Lauderdale",
      "state": "FL",
      "tag": "FL-South"
    },
    "source": null
  }
}

Payload fields

request_id
uuid
Id of the saved request. Pass it to the Get request API. Use it as your idempotency key.
form_id
uuid
Booking form the customer used.
contact
object
first_name, last_name, email, phone, company. Fields the form didn't collect are null.
address
object
address, address2, city, state, zip, country, entrance_info. Missing values are null.
category
object | null
The service category the customer picked: id, name, category-level intake answers, and the chosen services. null when no category was selected.
services
array
Chosen services with id, name, price, priceFrom, priceDisplay, free, durationMinutes, and service-level intake answers. Name and pricing are read from your service catalog when the booking is submitted.
total_amount
number | null
The total the widget showed the customer at submit, rounded to 2 decimals. null when the widget sent none.
intake_responses
array
Every intake answer as { question, answer, type, scope, scope_name }. scope is template, category, or service.
template_intake
array
Only the form-wide (template-scope) answers, as { question, answer, type }.
preferred_time
object | null
{ date, slot }. date is YYYY-MM-DD. slot is a start time such as 09:00, or morning / afternoon. null when no time was picked.
attribution
object | null
Flat map of source, page_url, referrer, utm_* tags, and click ids such as gclid captured on the booking page.
profile
object | null
The business profile the booking was attributed to. null when no profile applies.
source
string | null
Channel tag: the ?source= or ?ref= URL parameter, the embed's data-source attribute, or google_rwg for Reserve with Google bookings.

The profile block carries the business profile the booking was attributed to. It includes the profile's street, city, and state plus the tag you set under Profile tag on the profile's page. Use the tag to route or group bookings (per-territory queues, per-campaign Slack channels) without parsing UTM campaigns. The tag is copied when the booking is submitted, so editing it later doesn't change past events.

Headers

A webhook-kind endpoint receives these headers:

Content-Type
string
application/json
User-Agent
string
ServiceReserve-Webhook/1.0
X-ServiceReserve-Event
string
Same value as event in the body. Useful for routing without parsing JSON.
X-ServiceReserve-Signature
string
sha256=<hex> HMAC of the raw body, computed with your signing secret. Present only when the endpoint has a secret. See Signing & verification.

api-kind endpoints also send User-Agent and X-ServiceReserve-Event unless your headers template overrides them. telegram-kind endpoints send none of these headers.

Delivery semantics

  • Queued, not inline. A booking is saved first. Deliveries then go through a background queue, normally within a second or two.
  • Timeout. 10 seconds. An endpoint that doesn't respond in time counts as a failed attempt.
  • Retries. A failed attempt (non-2xx response, network error, or timeout) is retried up to 2 more times. After that the delivery is marked failed.
  • Duplicates are possible. For example, your endpoint might process a request but time out before it responds. Make your handler idempotent on data.request_id.

Delivery history and re-sending

The Recent deliveries panel on the Webhooks page lists the last 50 deliveries, newest first, with status code, duration, and any error.

To send an event again for a specific booking, open the request. In the Integration activity card, pick Webhooks from the integration list and click Send. Resends go to the endpoints on the Webhooks page. The event is rebuilt from the stored request, so it differs from the original event. category and total_amount are null. services holds the stored service ids rather than service objects. intake_responses holds the stored intake objects (the same shape as the Get request API). profile and template_intake are omitted, and created_at is added.

Test deliveries

The Send test button on each endpoint sends a synthetic booking.completed event with data.test: true. The test body is a short fixture (test, a sample booking object, and a message), not a full booking payload. Filter on data.test in your sink so test sends don't pollute production data.

Enabling and disabling

The toggle in the Webhooks page header turns delivery on or off for every endpoint at once. It is the same switch as the Webhooks card on the Integrations page.

Related