Skip to main content

Overview

Webhooks let your application receive real-time notifications when events happen in SpitShake. Instead of polling the API, SpitShake sends an HTTP POST request to your configured endpoint whenever a relevant event fires. Each webhook delivery includes a JSON payload describing the event, a cryptographic signature for verification, and a unique delivery ID for deduplication.

Supported Events

SpitShake emits 14 event types across four resource categories:

Form Events

Triggered when a submitter interacts with a signing form.

Field Events

Submission Events

Triggered when the overall submission (containing one or more submitters) changes state.

Template Events

Event Payloads

Every webhook delivery is an HTTP POST with Content-Type: application/json. The top-level structure is always:

Form Event Payload

All form events (form.created, form.started, form.viewed, form.completed, form.declined, form.expired) share this structure:
For form.declined events, the declined_at field will contain a timestamp and completed_at will be null. For form.expired events, both completed_at and declined_at will be null.

Submission Event Payload

Submission events include the full list of submitters and their statuses:
Submission event metadata echoes the caller-provided submission metadata. Keys reserved for SpitShake internals, currently flattening_failed, are omitted. Submitter metadata remains excluded; use each submitter’s external_id for caller-owned correlation.

Document download URLs in payloads

The submission.completed and form.completed webhook payloads include a documents array with download URLs, so you can retrieve signed PDFs directly from the event without a follow-up API call:
The download_url requires authentication (Bearer token or X-Auth-Token with submissions:read scope). download_url needs your API token. If your integration expects a link it can fetch without credentials (DocuSeal’s documents[].url), an account admin can turn on Public document links in Settings → Webhooks, or via PUT /api/settings/document_links. Each document then also carries:
  • Anyone with url can download the PDF, so treat it like a password.
  • Links expire after the account’s setting (30 days by default); set expires_in_days to null for links that never expire. url_expires_at is null for those.
  • Turning links off, or POST /api/settings/document_links/revoke, stops every link already sent. Changing the expiry only affects new links.
  • To mint a link on demand (with its own expiry), POST /api/submissions/{id}/documents/{document_id}/link.
  • A redelivery replays the original body, including the link it carried.

Template Event Payload

Template events include metadata about the template’s structure:

Field Updated Payload

Fired each time a submitter changes a field value. Includes the old and new values for change tracking:

Webhook Headers

Every webhook delivery includes the following HTTP headers:

HMAC Signature Verification

Every webhook includes an X-Webhook-Signature header containing an HMAC-SHA256 digest of the raw request body, signed with your webhook secret key:
Always verify the signature before processing a webhook to ensure it was sent by SpitShake and was not tampered with in transit.
Always use the raw request body (bytes) for signature verification, not a parsed or re-serialized version. Re-serializing JSON may change key order or whitespace, causing verification to fail.

Managing Webhooks via API

Create a Webhook Endpoint

Response:

Update a Webhook Endpoint

Response:

List All Webhook Endpoints

Response:

Delete a Webhook Endpoint

Returns HTTP 204 No Content on success.

Webhook Delivery Log

Every attempt to reach this endpoint, newest first: real events, retries, and sends from the test endpoint (flagged "test": true). Each entry carries the HTTP status the endpoint answered, how long it took, and the error text when it failed.
Query parameters: Response:
A delivery that is missing entirely was never dispatched: check the subscription rules under Which events actually reach you.

Redelivering a failed webhook

If a delivery failed or was exhausted, you can trigger a redeliver:
This creates a new delivery attempt with a fresh delivery_uuid.

Test a Webhook Endpoint

Send a sample delivery of any single event to your registered URL, so you can exercise the handler you actually wrote before a real signer ever touches the document.
200 OK
The send is logged like any other delivery, flagged "test": true, so GET /api/webhooks/{id}/deliveries shows what your endpoint answered. A failed test send is logged as failed with the error text and is not retried; the response is 422 with the same delivery_id.
  • event may be any event in Supported Events, including one this endpoint is not subscribed to — which is what makes it useful behind a “Test” button next to an event picker. Omit it and you get the first event the endpoint subscribes to, or form.completed if it subscribes to all.
  • An unrecognised event is rejected with 422 and nothing is sent:
    422 Unprocessable Entity
  • The body is the real payload shape for that event, filled with obviously-fake sample data (sample.signer@example.com, template “Sample Agreement”, ids in the 900000 range). Document download_urls (and public urls, when links are on) point at your own signing domain but resolve to nothing — they are for checking your URL handling, not for downloading.
  • The delivery carries every standard header, so it exercises your signature verification exactly like a real one.
A sample is marked as a test in two places: "test": true at the top level of the body, and the X-Webhook-Test: true header. Check one of them before acting. A sample submission.completed is shaped exactly like the real thing, so a handler that ignores both flags will happily create records for a submission that does not exist. Real deliveries never carry either flag.
Sample body
Test deliveries are sent synchronously and are not recorded in the delivery log — the HTTP response above is the result. Use the log for real events.

Testing your integration

A path from “nothing wired up” to “confident in production”, without emailing anyone.
1

Exercise your handler with a sample of each event

POST /api/webhooks/{id}/test for every event you subscribe to. Confirm your signature check passes, your parser copes with the shape, and you skip anything carrying "test": true.
2

Run a real submission that emails nobody

Create a submission with "send_email": false. No invitation is sent, but the submission is real and webhooks still fire normally — so you can open the signing link yourself, complete it, and watch genuine form.* and submission.completed deliveries arrive.
Each submitter in the response carries a signing_url you can open directly.
3

Read the delivery log when something does not arrive

GET /api/webhooks/{id}/deliveries shows every attempt, test sends included ("test": true), with the status your endpoint answered and the error text when it did not. A delivery that is missing entirely was never dispatched — check the subscription rules below rather than your handler.

Which events actually reach you

Deliveries are retried on any non-2xx, so a handler that 500s while you are debugging will keep receiving the same X-Webhook-Delivery-Id for up to 12 hours. Return 200 and discard what you cannot use yet.

Retry Policy

If your endpoint does not return an HTTP 2xx response, SpitShake retries the delivery up to 5 times with increasing delays: After 5 failed attempts, the delivery is marked as permanently failed. You can view failed deliveries in the webhook logs section of your account settings. Timeout thresholds:
  • Connect timeout: 5 seconds. If SpitShake cannot establish a TCP connection within 5 seconds, the attempt fails.
  • Read timeout: 10 seconds. If your server does not send a complete response within 10 seconds of connection, the attempt fails.
Each retry includes the same X-Webhook-Delivery-Id, so you can use it to deduplicate deliveries on your end.
Return a 200 OK response as quickly as possible. Process the webhook payload asynchronously (e.g., via a background job) to avoid hitting the read timeout.

SSRF Protection

SpitShake validates all webhook URLs before delivering events. The following restrictions apply to protect against Server-Side Request Forgery (SSRF) attacks: Blocked IP ranges: Additional restrictions:
  • HTTPS required in production. HTTP URLs are only accepted in development/test environments.
  • Localhost blocked. URLs containing localhost, 127.0.0.1, or [::1] are rejected.
  • Cloud metadata endpoints blocked. URLs targeting 169.254.169.254 and similar metadata service IPs are rejected.

Best Practices

Always validate the X-Webhook-Signature header before processing any webhook payload. Never skip verification, even in development.
Store the X-Webhook-Delivery-Id header value and check for duplicates before processing. Retries will send the same delivery ID.
Return a 200 status immediately and enqueue the payload for background processing. Long-running synchronous handlers risk timeouts and unnecessary retries.
Your webhook handler may receive the same event more than once due to retries. Design your processing logic to be safe for repeated execution.
Selecting specific events reduces traffic to your endpoint and simplifies your handler logic. You can always add more events later.
Set up alerting for elevated error rates or slow response times on your webhook handler. SpitShake will stop retrying after 5 failures.