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.
| Action | Endpoint | Required scope |
|---|---|---|
| List your subscriptions | GET /webhook-subscriptions | webhooks:read |
| Create a subscription | POST /webhook-subscriptions | webhooks:write |
| Change, disable, or re-enable a subscription | PATCH /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
:443is 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-idx-ssn-event-typex-ssn-delivery-idx-ssn-attemptx-ssn-signaturex-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:
- Capture the original body bytes and verify the expected signature version, timestamp, and digest before parsing or trusting the event.
- Validate the event shape. Use the verified JSON body's
idas the durable deduplication key; do not use the delivery ID or timestamp for deduplication. - 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.
- 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.createdrep.assignedjob.scheduledschedule.updatedjob.dispatchedjob.completeddeliverables.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 includesfieldRepNameonly; it does not include rep email, phone, or internal app ID. If the rep is cleared,fieldRepNameis returned asAwaiting 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.