ServiceKeel.Developers
All 9 chapters · Code and API details

CHAPTER 08 OF 8

Keep the connection reliable after the call

Follow record changes, verify webhooks, and handle partial work and disconnection before onboarding customers.

1. Follow changes with the event cursor

Use the event feed to reconcile changes made after event capture was enabled. Events contain record identifiers; retrieve the current record through the appropriate endpoint when you need its details. Store the returned cursor after processing, and continue while has_more is true.

Events settle for 30 seconds before becoming visible in this feed. Do not treat an immediately empty result as a failed write. Keep a cursor per connected business, and deduplicate event IDs.

GET/field_service/events

Read tenant record change events

Parameters from the API specification
NameLocationType / schema
event_typequerystring
record_idquerystring
afterquerystring
cursorquerystring

Documented responses: 200

Open full API reference
Read the next batch of changes
const query = new URLSearchParams();
if (savedCursor) query.set('cursor', savedCursor);
const events = await api('GET', '/events?' + query);
// Process events.items idempotently, then persist events.next_cursor.
// Repeat immediately when events.has_more; otherwise poll later.
JavaScript · server side

2. Register a webhook for future changes

Register a publicly reachable HTTPS destination for the events you need. The subscription is bound to the current OAuth application and grant and requires both read and write scopes. Register during connection setup in production so it is ready before the business starts using your receptionist.

Store signing_secret securely when returned; it is shown once. The demo registers its subscription at the end to demonstrate the endpoint. Its integration test does not exercise outbound delivery.

POST/field_service/webhooks

Subscribe to tenant record events

The specification does not enumerate parameters here. Use the workflow example for the request body.

Documented responses: 201

Open full API reference
Subscribe to request, job, and appointment changes
const hook = await api('POST', '/webhooks', {
  url: 'https://your-app.example/servicekeel/events',
  events: ['request.created', 'job.updated', 'appointment.updated'],
});
// Securely persist hook.signing_secret and hook.subscription.id.
// The appointment event name is appointment.updated, not visit.updated.
JavaScript · server side

3. Verify delivery before processing

Verify the HMAC over the exact raw request bytes and reject timestamps outside a five-minute window. The current wire headers are X-SiteStack-Timestamp and X-SiteStack-Signature; event deliveries also include X-Servicekeel-Event-ID. Header names are case-insensitive.

After verification, deduplicate the event ID within the connection, durably enqueue work, and respond successfully. Delivery is at least once, so a repeated event must not create another customer, appointment, or charge.

Verify the current webhook signature format
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyWebhook(rawBody, headers, secret) {
  const timestamp = headers['x-sitestack-timestamp'];
  const signature = headers['x-sitestack-signature'];
  if (typeof timestamp !== 'string' || !/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  if (typeof signature !== 'string' || !/^v1=[a-f0-9]{64}$/.test(signature)) return false;
  const expected = createHmac('sha256', secret)
    .update(timestamp + '.').update(rawBody).digest();
  const received = Buffer.from(signature.slice(3), 'hex');
  return received.length === expected.length && timingSafeEqual(received, expected);
}
// Verify BEFORE JSON parsing; then deduplicate the delivered event ID.
JavaScript · server side

4. Make failures visible and recover deliberately

ResultReceptionist behavior
400 / validationPreserve the proposed values, inspect field issues, and ask for the missing information. Do not claim the write succeeded.
401Check expiry and connection status. Use serialized token rotation when appropriate; a revoked grant needs fresh consent.
403Check consented scopes, app approval, and business access. Do not broaden access silently.
404Reconcile the selected record within this connection. Never try a different business’s token to locate it.
409Reload the record or availability. Reconcile the caller’s choice before another versioned write.
Timeout / server failureThe write may already have succeeded. Inspect saved IDs and current state before retrying. Use idempotency keys only on endpoints that document them.

Persist a small workflow record in your system: connected business, call ID, caller-confirmed choices, completed steps, and returned ServiceKeel IDs. This lets a human resume partial work without replaying the entire conversation.

5. Validate with your own voice application

  • Test consent denial, expired state, token rotation, revocation, and a second business’s isolation.
  • Configure the business’s catalog, service coverage, qualified staff, and booking hours.
  • Check no availability, a competing booking, stale versions, and a partially completed workflow.
  • Capture explicit approval for scheduling, cancellations, pricing, and recurring work.
  • Verify real storage, payment configuration, and signed webhook delivery in your intended environment.
  • Submit your app, support information, and setup instructions for ServiceKeel review before onboarding other businesses.

The working example lives in examples/mock-receptionist in the frontend repository. Its Playwright test is tests/integration/receptionist.integration.spec.mts. The local test needs Node dependencies, Playwright Chromium, Go, the backend checkout, and local MongoDB.

Run the reproducible local workflow
npm run test:receptionist
# Watch the browser:
npm run test:receptionist -- --headed
# Capture chapter screenshots from the same integration:
SERVICEKEEL_CAPTURE_GUIDE=1 npm run test:receptionist
Shell · server side