Skip to main content

Webhooks

Webhooks send signed HTTP POST notifications to your receiver when supported site-visit events occur. Set up and validate a sandbox receiver before enabling production delivery. Your receiver must verify signatures, safely accept duplicate events, and return a 2xx response promptly.

1. Register a receiver​

Use a bearer token for the client and environment that will own the subscription. The paths below are relative to that environment's base URL ending in /api/v1. See Authentication for token acquisition.

ActionEndpointRequired scope
List your subscriptionsGET /webhook-subscriptionswebhooks:read
Create a subscriptionPOST /webhook-subscriptionswebhooks:write
Change, disable, or re-enable a subscriptionPATCH /webhook-subscriptions/{id}webhooks:write

Save a randomly generated shared secret in your receiver's secret store before registering. The API requires at least 16 characters; prefer a value generated from 32 random bytes. Replace every placeholder below with your own value. Do not use the example secret as a credential.

POST /api/v1/webhook-subscriptions
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{
"targetUrl": "https://receiver.example.com/ssn/webhooks",
"description": "Sandbox site-visit notifications",
"eventTypes": ["ticket.created", "job.completed"],
"secret": "YOUR_RANDOM_SHARED_SECRET",
"signingVersion": "v1"
}

A successful create returns 201 with the subscription under data. Save its id (a whsub_... value) for later updates. New subscriptions are active and use signingVersion: "v1". The secret is never returned by create, list, or update. Select only the event types your integration handles; the list of supported values is below.

Destination requirements​

  • Use a publicly reachable HTTPS endpoint on port 443. Explicit :443 is allowed; other ports and HTTP are unsupported.
  • Do not include URL credentials (user:password@host) or a fragment (#...).
  • Every DNS address returned for the hostname must be public. Localhost, private, link-local, metadata, and other special-use addresses are rejected, including a hostname with mixed public and private answers.
  • Serve the final receiver URL directly with a valid TLS certificate. Redirects, including redirects to a login page or a trailing-slash URL, are not followed.

Registration checks the URL and DNS, but does not send a test event or prove that your receiver accepts deliveries. A policy violation returns 422; temporary DNS resolution failure returns 503. The destination is checked again on each delivery, so a later DNS or redirect change can block delivery.

Subscription creation and updates do not use Idempotency-Key or If-Match. After a lost response, list subscriptions and reconcile before repeating a write; creating a second subscription can produce another delivery of each matching event. Secrets cannot be read back, so an uncertain secret change requires coordination. See operation-specific retry guidance.

2. Verify the signature​

Common delivery headers:

  • x-ssn-event-id
  • x-ssn-event-type
  • x-ssn-delivery-id
  • x-ssn-attempt
  • x-ssn-signature
  • x-ssn-signature-alg: hmac-sha256

Verify v1 signatures​

New subscriptions expose signingVersion: "v1"; existing ones remain legacy until explicitly upgraded with a supplied secret. Secrets are never returned.

V1 adds x-ssn-signature-version: v1 and x-ssn-timestamp (canonical decimal Unix seconds). Compute HMAC-SHA256 hex using the secret's exact UTF-8 bytes over timestamp + "." + rawBody. Capture the original body Buffer before JSON parsing. Do not trim, normalize Unicode, or base64/hex-decode the secret.

Pin the expected version and algorithm, require a signing timestamp within 300 seconds of your clock in either direction, and compare decoded digests in constant time. Never fall back to legacy when verification fails. Then validate JSON and deduplicate on the signed body's id in durable storage. The timestamp limits old replay; dedupe remains necessary inside that window. Other transport headers are informational, not separately authenticated identities.

Download the standalone Node receiver verifier. It accepts a raw Buffer and lower-case header names with no package dependencies. Prefer a secret generated from 32 random bytes; when exchanged as base64 text, that text itself is the shared secret, without decoding.

Retries preserve event ID and persisted payload but sign with a fresh delivery timestamp. Use the signing timestamp for freshness, not occurredAt.

Fixed test vector (set the verifier's test clock to the timestamp):

  • Secret: fixture-shared-secret-32-bytes-long
  • Timestamp: 1790092800
  • Exact UTF-8 body, no trailing newline: {"id":"evt_fixture","note":"café 🚚","amount":1}
  • Signature: cd6810a35fb96cd13fff75873f3fe4ecfe570cc7bc64a642658b5a6b106bffb8

3. Accept events and handle retries​

For each incoming request:

  1. Capture the original body bytes and verify the expected signature version, timestamp, and digest before parsing or trusting the event.
  2. Validate the event shape. Use the verified JSON body's id as the durable deduplication key; do not use the delivery ID or timestamp for deduplication.
  3. Atomically record acceptance and durably queue or process the work. Return a 2xx response only after that acceptance is safe. Return 2xx for an event ID you have already accepted, without repeating its side effects.
  4. Process slower downstream work asynchronously after acknowledgment. Do not acknowledge first and rely on an in-memory job that could be lost.

Delivery is at least once; a timeout or lost acknowledgment can cause the same event to arrive again even if your receiver processed it. Events can be retried independently, so do not rely on their arrival order. occurredAt describes the event time, not its position in a delivery sequence.

SSN uses bounded automatic retries. The defaults are eight total claimed attempts per delivery and a 10-second request timeout; environment settings can change these limits. Any non-2xx response, including 4xx and 429, or a transient DNS/network/timeout failure is retried within that budget. Retry timing is managed by SSN's delivery queue; do not assume your Retry-After header controls it. A blocked destination or redirect ends the delivery without further automatic recipient attempts. An inactive subscription also stops delivery when claimed.

When automatic attempts end, ask your SSN integration contact to inspect the failed delivery. Include the environment, subscription ID, event ID if received, UTC time, and your receiver's response/error evidence. Do not send the shared secret or bearer token. Clients do not have a public delivery-list or replay endpoint. An operator can retry a terminal failed delivery after the cause is corrected; this retains the event ID, so your deduplication still applies.

4. Maintain a subscription​

List first to confirm the subscription ID and current configuration:

GET /api/v1/webhook-subscriptions
Authorization: Bearer YOUR_ACCESS_TOKEN

Update only the fields you intend to change. For example, changing the receiver:

PATCH /api/v1/webhook-subscriptions/whsub_YOUR_SUBSCRIPTION_ID
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{"targetUrl":"https://receiver.example.com/ssn/webhooks-v2"}

The current destination is validated on updates, including ordinary metadata or secret updates. To disable an unreachable or disallowed destination, send only:

PATCH /api/v1/webhook-subscriptions/whsub_YOUR_SUBSCRIPTION_ID
Authorization: Bearer YOUR_ACCESS_TOKEN
Content-Type: application/json

{"isActive":false}

Disabling prevents new matching events from being queued for this subscription. An already in-flight request can still arrive; queued deliveries that are later claimed while inactive become terminal failures. To re-enable, PATCH {"isActive":true} after the receiver is ready. Re-enabling validates the URL but does not replay missed events or automatically recover terminal failed ones. Coordinate any needed recovery with SSN. There is no public delete route.

For v1 secret rotation, prepare your receiver and PATCH {"secret":"NEW_RANDOM_SHARED_SECRET"}. The signing version stays unchanged. Coordinate with SSN before changing a live receiver's secret or signing version: in-flight work can use the previous configuration, while later claims use the current one. Disabling a subscription alone is not a safe way to drain its queue.

Upgrade an existing subscription​

After preparing its recipient, PATCH the subscription with {"signingVersion":"v1","secret":"NEW_RANDOM_SHARED_SECRET"}. Upgrade requires an explicit secret. Ordinary metadata changes and secret rotation preserve the current version. Downgrades/new legacy subscriptions are not supported; a stale update racing a version change returns 409.

Legacy signs the body with its stored server-materialized key. Earlier guidance claiming legacy used the raw shared secret alone was incorrect; do not request the global server salt. Coordinate a v1 upgrade instead. Pending deliveries use the current version/key when claimed: pause/drain delivery work during coordinated migration to avoid old in-flight signatures. Upgrades do not enroll or resend historical excluded deliveries.

Event reference​

Supported event types:

  • ticket.created
  • rep.assigned
  • job.scheduled
  • schedule.updated
  • job.dispatched
  • job.completed
  • deliverables.transmitted

ticket.created can originate from client API-created site visits or from SSN-created or registered site visits outside the API. rep.assigned, job.scheduled, schedule.updated, job.dispatched, job.completed, and deliverables.transmitted originate from SSN/Quickbase lifecycle workflows and are delivered only when webhook access is enabled and the client is API-enabled through the SSN API Enabled setting.

All site-visit lifecycle events use the same event envelope:

{
"id": "evt_...",
"type": "ticket.created",
"occurredAt": "2026-04-03T16:19:30.074Z",
"resourceType": "site-visits",
"resourceId": "visit_...",
"payloadVersion": "2026-03-24",
"data": {
"clientTicketNumber": "CLIENT-TKT-1001",
"clientSiteId": "STORE-100",
"status": "New"
}
}

Event-specific behavior:

  • ticket.created: emitted when a site visit is created.
  • job.scheduled: a schedule date and optional ETA time have been entered on the site visit.
  • rep.assigned: a field rep assignment state changed on the ticket. The payload includes fieldRepName only; it does not include rep email, phone, or internal app ID. If the rep is cleared, fieldRepName is returned as Awaiting reassignment.
  • schedule.updated: an already entered schedule date or ETA time changed before dispatch.
  • job.dispatched: the job is locked in and the ticket has been dispatched to the field rep.
  • job.completed: the site-visit status is completed after SSN QA has passed the visit.
  • deliverables.transmitted: field-rep deliverables and notes have been transmitted to the client endpoint.

Lifecycle events that originate from SSN operations in Quickbase are gated by the client's SSN API Enabled setting. If a client is not API-enabled, SSN does not publish these lifecycle webhooks for that client's site visits.

ticket.created can originate from either the client API or SSN workflows when a ticket is created outside the API. SSN deduplicates those internal publishing paths so a single created site visit should not produce duplicate ticket.created deliveries. If a ticket is created outside the API, SSN registers an API visit_... ID for that Quickbase record before publishing the webhook.