Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.spitshake.io/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Webhooks let your application receive real-time notifications when events happen in DocuTrust. Instead of polling the API, DocuTrust 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

DocuTrust emits 14 event types across four resource categories:

Form Events

Triggered when a submitter interacts with a signing form.
EventDescription
form.createdA new signing form was generated and assigned to a submitter.
form.startedThe submitter began filling out the form (first field interaction).
form.viewedThe submitter opened the signing link.
form.completedThe submitter finished and submitted the form.
form.declinedThe submitter declined to sign the document.
form.expiredThe form’s expiration date passed without completion.
signing.completeIdentity-bound only. Fires after form.completed when the submission was signed via the SpitShake Identity Engine handoff (JWT with verified_name + stripe_verification_id). Payload includes an identity block with the verified tuple. See Identity-bound signing.

Field Events

EventDescription
field.updatedA submitter changed the value of a field in a form.

Submission Events

Triggered when the overall submission (containing one or more submitters) changes state.
EventDescription
submission.createdA new submission was created from a template.
submission.completedAll submitters in the submission have completed their forms.
submission.archivedThe submission was archived by an admin.
submission.expiredThe submission expired before all submitters completed.

Template Events

EventDescription
template.createdA new template was created.
template.updatedA template’s fields, documents, or settings were modified.
template.archivedA template was archived.

Event Payloads

Every webhook delivery is an HTTP POST with Content-Type: application/json. The top-level structure is always:
{
  "event": "event.type",
  "timestamp": "ISO-8601 timestamp",
  "data": { }
}

Form Event Payload

All form events (form.created, form.started, form.viewed, form.completed, form.declined, form.expired) share this structure:
{
  "event": "form.completed",
  "timestamp": "2026-04-09T14:32:00Z",
  "data": {
    "submitter": {
      "id": 142,
      "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "email": "jane.doe@example.com",
      "name": "Jane Doe",
      "role": "First Party",
      "status": "completed",
      "external_id": "cust_12345",
      "metadata": {
        "department": "Legal"
      },
      "opened_at": "2026-04-09T14:20:00Z",
      "completed_at": "2026-04-09T14:32:00Z",
      "declined_at": null
    },
    "submission": {
      "id": 89,
      "slug": "abc123def456",
      "status": "completed",
      "template_id": 15
    },
    "template": {
      "id": 15,
      "name": "Employment Agreement"
    }
  }
}
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:
{
  "event": "submission.completed",
  "timestamp": "2026-04-09T14:35:00Z",
  "data": {
    "submission": {
      "id": 89,
      "slug": "abc123def456",
      "status": "completed",
      "created_at": "2026-04-09T10:00:00Z",
      "completed_at": "2026-04-09T14:35:00Z",
      "template_id": 15,
      "submitters": [
        {
          "id": 142,
          "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
          "email": "jane.doe@example.com",
          "name": "Jane Doe",
          "role": "First Party",
          "status": "completed"
        },
        {
          "id": 143,
          "uuid": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
          "email": "john.smith@acme.com",
          "name": "John Smith",
          "role": "Second Party",
          "status": "completed"
        }
      ]
    },
    "template": {
      "id": 15,
      "name": "Employment Agreement"
    }
  }
}

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:
{
  "event": "submission.completed",
  "data": {
    "submission": {
      "id": 42,
      "status": "completed",
      "documents": [
        {
          "id": 1,
          "document_type": "signed_pdf",
          "filename": "signed_contract.pdf",
          "download_url": "https://your-instance.com/api/v1/submissions/42/documents/1"
        },
        {
          "id": 2,
          "document_type": "combined",
          "filename": "complete_contract.pdf",
          "download_url": "https://your-instance.com/api/v1/submissions/42/documents/2"
        }
      ]
    }
  }
}
The download_url requires authentication (Bearer token or X-Auth-Token with submissions:read scope).

Template Event Payload

Template events include metadata about the template’s structure:
{
  "event": "template.updated",
  "timestamp": "2026-04-09T09:15:00Z",
  "data": {
    "template": {
      "id": 15,
      "name": "Employment Agreement",
      "slug": "employment-agreement-v2",
      "external_id": "tmpl_emp_2026",
      "folder_name": "HR Documents",
      "source": "web",
      "created_at": "2026-03-01T08:00:00Z",
      "updated_at": "2026-04-09T09:15:00Z",
      "submitters": [
        {
          "uuid": "c3d4e5f6-a7b8-9012-cdef-123456789012",
          "name": "Employee"
        },
        {
          "uuid": "d4e5f6a7-b8c9-0123-defa-234567890123",
          "name": "HR Manager"
        }
      ],
      "field_count": 12,
      "document_count": 1
    }
  }
}

Field Updated Payload

Fired each time a submitter changes a field value. Includes the old and new values for change tracking:
{
  "event": "field.updated",
  "timestamp": "2026-04-09T14:25:00Z",
  "data": {
    "event": "field.updated",
    "submitter": {
      "id": 142,
      "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "email": "jane.doe@example.com",
      "name": "Jane Doe",
      "role": "First Party"
    },
    "submission": {
      "id": 89,
      "slug": "abc123def456",
      "template_id": 15
    },
    "field": {
      "uuid": "f1a2b3c4-d5e6-7890-abcd-ef1234567890",
      "name": "Start Date",
      "type": "date",
      "old_value": null,
      "new_value": "2026-05-01",
      "was_prefilled": false
    },
    "changed_at": "2026-04-09T14:25:00Z"
  }
}

Webhook Headers

Every webhook delivery includes the following HTTP headers:
HeaderDescriptionExample
Content-TypeAlways JSON.application/json
X-Webhook-SignatureHMAC-SHA256 signature of the raw request body.sha256=5d7b1e3a9f...
X-Webhook-SecretThe shared secret configured for this webhook endpoint.whsec_abc123def456
X-Webhook-Delivery-IdUnique UUID identifying this delivery attempt (use for deduplication).e7f8a9b0-c1d2-3456-e7f8-a9b0c1d23456
User-AgentIdentifies the sender.DocuTrust-Webhook/1.0

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:
X-Webhook-Signature: sha256=HMAC-SHA256(secret_key, raw_body)
Always verify the signature before processing a webhook to ensure it was sent by DocuTrust and was not tampered with in transit.
require 'openssl'

def verify_webhook(request, secret)
  raw_body = request.body.read
  signature_header = request.headers['X-Webhook-Signature']

  expected = 'sha256=' + OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    secret,
    raw_body
  )

  if Rack::Utils.secure_compare(expected, signature_header)
    JSON.parse(raw_body)
  else
    raise 'Invalid webhook signature'
  end
end
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

curl -X POST https://your-instance.spitshake.io/api/webhooks \
  -H "X-Auth-Token: YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/docutrust",
    "events": [
      "form.completed",
      "form.declined",
      "submission.completed",
      "submission.expired"
    ],
    "secret": "whsec_your_signing_secret_here",
    "enabled": true
  }'
Response:
{
  "id": 34,
  "url": "https://yourapp.com/webhooks/docutrust",
  "events": [
    "form.completed",
    "form.declined",
    "submission.completed",
    "submission.expired"
  ],
  "secret": "whsec_your_signing_secret_here",
  "enabled": true,
  "created_at": "2026-04-09T10:00:00Z",
  "updated_at": "2026-04-09T10:00:00Z"
}

Update a Webhook Endpoint

curl -X PUT https://your-instance.spitshake.io/api/webhooks/34 \
  -H "X-Auth-Token: YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/docutrust-v2",
    "events": [
      "form.completed",
      "form.declined",
      "form.expired",
      "submission.completed",
      "submission.expired",
      "template.updated"
    ],
    "enabled": true
  }'
Response:
{
  "id": 34,
  "url": "https://yourapp.com/webhooks/docutrust-v2",
  "events": [
    "form.completed",
    "form.declined",
    "form.expired",
    "submission.completed",
    "submission.expired",
    "template.updated"
  ],
  "secret": "whsec_your_signing_secret_here",
  "enabled": true,
  "created_at": "2026-04-09T10:00:00Z",
  "updated_at": "2026-04-09T11:30:00Z"
}

List All Webhook Endpoints

curl -X GET https://your-instance.spitshake.io/api/webhooks \
  -H "X-Auth-Token: YOUR_API_TOKEN"
Response:
{
  "data": [
    {
      "id": 34,
      "url": "https://yourapp.com/webhooks/docutrust-v2",
      "events": [
        "form.completed",
        "form.declined",
        "form.expired",
        "submission.completed",
        "submission.expired",
        "template.updated"
      ],
      "secret": "whsec_your_signing_secret_here",
      "enabled": true,
      "created_at": "2026-04-09T10:00:00Z",
      "updated_at": "2026-04-09T11:30:00Z"
    }
  ],
  "pagination": {
    "count": 1,
    "next": null,
    "prev": null
  }
}

Delete a Webhook Endpoint

curl -X DELETE https://your-instance.spitshake.io/api/webhooks/34 \
  -H "X-Auth-Token: YOUR_API_TOKEN"
Returns HTTP 204 No Content on success.

Webhook Delivery Log

Retrieve the delivery history for a specific webhook endpoint. Each entry includes the HTTP status code, response time, payload, and any error details.
curl -X GET https://your-instance.spitshake.io/api/webhooks/34/deliveries \
  -H "X-Auth-Token: YOUR_API_TOKEN"
Query parameters:
ParameterTypeDefaultDescription
statusstringFilter by delivery status: "success", "failed", "pending".
eventstringFilter by event type (e.g., "form.completed").
afterintegerCursor for pagination (return items after this delivery ID).
beforeintegerCursor for pagination (return items before this delivery ID).
limitinteger25Items per page (1-100).
Response:
{
  "data": [
    {
      "id": 1892,
      "webhook_id": 34,
      "event": "form.completed",
      "status": "success",
      "http_status": 200,
      "url": "https://yourapp.com/webhooks/docutrust-v2",
      "request_headers": {
        "Content-Type": "application/json",
        "X-Webhook-Signature": "sha256=5d7b1e3a9f...",
        "X-Webhook-Secret": "whsec_abc123def456",
        "X-Webhook-Delivery-Id": "e7f8a9b0-c1d2-3456-e7f8-a9b0c1d23456",
        "User-Agent": "DocuTrust-Webhook/1.0"
      },
      "request_body": {
        "event": "form.completed",
        "timestamp": "2026-04-09T14:32:00Z",
        "data": {
          "submitter": {
            "id": 142,
            "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            "email": "jane.doe@example.com",
            "name": "Jane Doe",
            "role": "First Party",
            "status": "completed",
            "external_id": "cust_12345",
            "metadata": {
              "department": "Legal"
            },
            "opened_at": "2026-04-09T14:20:00Z",
            "completed_at": "2026-04-09T14:32:00Z",
            "declined_at": null
          },
          "submission": {
            "id": 89,
            "slug": "abc123def456",
            "status": "completed",
            "template_id": 15
          },
          "template": {
            "id": 15,
            "name": "Employment Agreement"
          }
        }
      },
      "response_status": 200,
      "response_body": "{\"received\": true}",
      "response_time_ms": 142,
      "attempt": 1,
      "max_attempts": 6,
      "error_message": null,
      "delivered_at": "2026-04-09T14:32:01Z",
      "created_at": "2026-04-09T14:32:00Z"
    },
    {
      "id": 1885,
      "webhook_id": 34,
      "event": "form.viewed",
      "status": "failed",
      "http_status": null,
      "url": "https://yourapp.com/webhooks/docutrust-v2",
      "request_headers": {
        "Content-Type": "application/json",
        "X-Webhook-Signature": "sha256=8c4a2f1b7e...",
        "X-Webhook-Secret": "whsec_abc123def456",
        "X-Webhook-Delivery-Id": "f8a9b0c1-d2e3-4567-f8a9-b0c1d2e34567",
        "User-Agent": "DocuTrust-Webhook/1.0"
      },
      "request_body": {
        "event": "form.viewed",
        "timestamp": "2026-04-09T14:20:00Z",
        "data": {
          "submitter": {
            "id": 142,
            "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
            "email": "jane.doe@example.com",
            "name": "Jane Doe",
            "role": "First Party",
            "status": "opened",
            "external_id": "cust_12345",
            "metadata": {
              "department": "Legal"
            },
            "opened_at": "2026-04-09T14:20:00Z",
            "completed_at": null,
            "declined_at": null
          },
          "submission": {
            "id": 89,
            "slug": "abc123def456",
            "status": "pending",
            "template_id": 15
          },
          "template": {
            "id": 15,
            "name": "Employment Agreement"
          }
        }
      },
      "response_status": null,
      "response_body": null,
      "response_time_ms": null,
      "attempt": 3,
      "max_attempts": 6,
      "error_message": "Connection timed out after 5000ms",
      "delivered_at": null,
      "created_at": "2026-04-09T14:20:00Z"
    }
  ],
  "pagination": {
    "count": 2,
    "next": 1885,
    "prev": null
  }
}
Delivery object fields:
FieldTypeDescription
idintegerUnique delivery identifier.
webhook_idintegerThe webhook endpoint this delivery belongs to.
eventstringThe event type that triggered this delivery.
statusstringDelivery status: "success", "failed", or "pending".
http_statusinteger or nullThe HTTP status code returned by your endpoint, or null if the request failed before receiving a response.
urlstringThe URL the delivery was sent to.
request_headersobjectThe HTTP headers sent with the delivery.
request_bodyobjectThe full JSON payload sent in the request body.
response_statusinteger or nullThe response HTTP status code.
response_bodystring or nullThe response body (truncated to 1KB).
response_time_msinteger or nullResponse time in milliseconds.
attemptintegerThe attempt number (1 = first delivery, 2+ = retries).
max_attemptsintegerMaximum number of delivery attempts (always 6).
error_messagestring or nullError description if the delivery failed.
delivered_atstring or nullISO 8601 timestamp when the delivery was successfully received.
created_atstringISO 8601 timestamp when the delivery was created.

Redelivering a failed webhook

If a delivery failed or was exhausted, you can trigger a redeliver:
POST /api/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver
This creates a new delivery attempt with a fresh delivery_uuid.

Test a Webhook Endpoint

Send a test delivery to verify your endpoint is working correctly.
curl -X POST https://your-instance.spitshake.io/api/webhooks/34/test \
  -H "X-Auth-Token: YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "form.completed"
  }'
This sends a sample payload to your registered URL with realistic test data. The delivery includes all standard headers so you can verify your signature validation logic.

Retry Policy

If your endpoint does not return an HTTP 2xx response, DocuTrust retries the delivery up to 5 times with increasing delays:
AttemptDelay After Failure
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry12 hours
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 DocuTrust 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

DocuTrust validates all webhook URLs before delivering events. The following restrictions apply to protect against Server-Side Request Forgery (SSRF) attacks: Blocked IP ranges:
CIDR BlockDescription
10.0.0.0/8Private network (Class A)
172.16.0.0/12Private network (Class B)
192.168.0.0/16Private network (Class C)
127.0.0.0/8Loopback (localhost)
169.254.0.0/16Link-local / cloud metadata
100.64.0.0/10Carrier-grade NAT (shared address space)
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. DocuTrust will stop retrying after 5 failures.