Webhooks
Webhooks let your application receive HTTP callbacks when signing events occur in TheTerms.
How It Works
Section titled “How It Works”- You register a webhook URL in TheTerms (via API or dashboard)
- When an event occurs, TheTerms sends an HTTP POST to your URL
- Your server processes the payload and responds with
2xx - If delivery fails, TheTerms retries up to 2 more times with exponential backoff (3 attempts total)
Setting Up a Webhook
Section titled “Setting Up a Webhook”Create a webhook endpoint
Section titled “Create a webhook endpoint”Webhook creation takes a url only — there is no event-filtering field. Every active webhook receives all events.
curl -X POST "$THETERMS_URL/webhooks" \ -H "X-Api-Key: $THETERMS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhooks/theterms" }'Response:
{ "data": { "id": "018e1234-abcd-7000-8000-000000000040", "url": "https://your-server.com/webhooks/theterms", "secret": "whsec_...", "is_active": true, "org_id": "018e1234-abcd-7000-8000-000000000000", "created_at": "2026-02-21T10:00:00.000Z" }}Event Types
Section titled “Event Types”| Event | When it fires |
|---|---|
signing.request.created | A signing request is sent (including via bulk/batch import) |
signing.request.viewed | The signer opens the signing page |
signing.request.completed | The signer submits a decision — accepted or rejected |
There is no event for a document being published or archived, and no separate “expired” event.
Payload Format
Section titled “Payload Format”Every webhook delivery sends a JSON body:
{ "event": "signing.request.completed", "timestamp": "2026-02-21T10:15:00.000Z", "data": { "requestId": "sr_abc123...", "signerEmail": "jane@example.com", "signerName": "Jane Doe", "versionId": "doc_xyz789..." }}Verifying Signatures
Section titled “Verifying Signatures”Every webhook request includes an X-Webhook-Signature header in the form sha256=<hex-digest> — an HMAC-SHA256 signature of the raw request body, hex-encoded. Strip the sha256= prefix before comparing:
import crypto from "node:crypto";
function verifyWebhook(payload, signatureHeader, secret) { const signature = signatureHeader.replace(/^sha256=/, ""); const expected = crypto .createHmac("sha256", secret) .update(payload) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(signature, "hex"), Buffer.from(expected, "hex") );}
// In your webhook handler:app.post("/webhooks/theterms", (req, res) => { const signatureHeader = req.headers["x-webhook-signature"]; const isValid = verifyWebhook( JSON.stringify(req.body), signatureHeader, process.env.THETERMS_WEBHOOK_SECRET );
if (!isValid) { return res.status(401).send("Invalid signature"); }
// Process the event console.log("Event:", req.body.event); res.status(200).send("OK");});import hmacimport hashlib
def verify_webhook(payload: bytes, signature_header: str, secret: str) -> bool: signature = signature_header.removeprefix("sha256=") expected = hmac.new( secret.encode(), payload, hashlib.sha256, ).hexdigest() return hmac.compare_digest(signature, expected)
# In your webhook handler (Flask example):@app.route("/webhooks/theterms", methods=["POST"])def handle_webhook(): signature_header = request.headers.get("X-Webhook-Signature", "") if not verify_webhook(request.data, signature_header, WEBHOOK_SECRET): return "Invalid signature", 401
event = request.json print(f"Event: {event['event']}") return "OK", 200Retry Behaviour
Section titled “Retry Behaviour”If your server returns a non-2xx response or doesn’t respond within 10 seconds, TheTerms retries:
| Attempt | Delay before it |
|---|---|
| 1st (original send) | — |
| 2nd (1st retry) | 4 seconds |
| 3rd (2nd retry, final) | 16 seconds |
3 total attempts. After the 3rd fails, the delivery is marked FAILED — there is currently no API endpoint to inspect delivery history.
Managing Webhooks
Section titled “Managing Webhooks”List webhooks GET /api/v1/webhooks
Section titled “List webhooks /api/v1/webhooks”Returns all webhook endpoints for the organisation. The secret is not included in list responses — only on creation.
Response 200:
{ "data": [ { "id": "018e1234-abcd-7000-8000-000000000040", "org_id": "018e1234-abcd-7000-8000-000000000000", "url": "https://your-server.com/webhooks/theterms", "is_active": true, "created_at": "2026-02-01T09:00:00.000Z", "updated_at": "2026-02-01T09:00:00.000Z", "_count": { "deliveries": 42 } } ]}Create a webhook POST /api/v1/webhooks
Section titled “Create a webhook /api/v1/webhooks”| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | http:// or https:// URL to receive events — must resolve to a public, routable address |
{ "url": "https://your-server.com/webhooks/theterms" }Response 201: Webhook object (same shape as create’s response above, including secret). The secret is shown here and never again — save it immediately for signature verification.
Error cases:
| Status | Cause |
|---|---|
400 | URL fails to parse, isn’t http/https, or resolves to a private/internal address |
403 | Webhook limit reached for the organisation’s plan (TIER_LIMIT) |
Update a webhook PUT /api/v1/webhooks/:id
Section titled “Update a webhook /api/v1/webhooks/:id”| Field | Type | Description |
|---|---|---|
url | string | New endpoint URL |
is_active | boolean | Enable (true) or pause (false) delivery |
{ "is_active": false }Response 200: Updated webhook object.
Delete a webhook DELETE /api/v1/webhooks/:id
Section titled “Delete a webhook /api/v1/webhooks/:id”Permanently removes the webhook and stops all future deliveries.
Response 200:
{ "data": { "success": true } }Error cases:
| Status | Cause |
|---|---|
404 | Webhook not found in this organisation |
Limits
Section titled “Limits”Maximum webhook endpoints per organisation depends on your plan:
| Tier | Max webhooks |
|---|---|
| Free | 3 |
| Individual | 10 |
| Team / Pro / Enterprise | Unlimited |
Every active webhook receives every event — there is no per-webhook event filtering.