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
| Name | Location | Type / schema |
|---|---|---|
event_type | query | string |
record_id | query | string |
after | query | string |
cursor | query | string |
Documented responses: 200
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 side2. 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
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 side3. 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.
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 side4. Make failures visible and recover deliberately
| Result | Receptionist behavior |
|---|---|
| 400 / validation | Preserve the proposed values, inspect field issues, and ask for the missing information. Do not claim the write succeeded. |
| 401 | Check expiry and connection status. Use serialized token rotation when appropriate; a revoked grant needs fresh consent. |
| 403 | Check consented scopes, app approval, and business access. Do not broaden access silently. |
| 404 | Reconcile the selected record within this connection. Never try a different business’s token to locate it. |
| 409 | Reload the record or availability. Reconcile the caller’s choice before another versioned write. |
| Timeout / server failure | The 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.
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:receptionistShell · server side