Webhooks
Webhooks let your application receive HTTP callbacks when events occur in TheTerms — for example, when a document is signed or a signing request expires.
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 3 times with exponential backoff
Setting Up a Webhook
Section titled “Setting Up a Webhook”Create a webhook endpoint
Section titled “Create a webhook endpoint”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", "events": ["signing.completed", "signing.viewed"] }'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 | Description |
|---|---|
signing.completed | A signer completed signing (all mandatory clauses accepted) |
signing.viewed | A signer opened the signing page |
signing.expired | A signing request’s token expired |
document.published | A document version was published |
document.archived | A document version was archived |
Payload Format
Section titled “Payload Format”Every webhook delivery sends a JSON body:
{ "event": "signing.completed", "timestamp": "2026-02-21T10:15:00.000Z", "data": { "signingRequestId": "sr_abc123...", "signerEmail": "jane@example.com", "signerName": "Jane Doe", "documentId": "doc_xyz789...", "responses": [ { "clauseId": "clause-1", "accepted": true }, { "clauseId": "clause-2", "accepted": false } ] }}Verifying Signatures
Section titled “Verifying Signatures”Every webhook request includes an X-Webhook-Signature header containing an HMAC-SHA256 signature. Verify it to ensure the request came from TheTerms:
import crypto from "node:crypto";
function verifyWebhook(payload, signature, secret) { const expected = crypto .createHmac("sha256", secret) .update(payload) .digest("hex"); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) );}
// In your webhook handler:app.post("/webhooks/theterms", (req, res) => { const signature = req.headers["x-webhook-signature"]; const isValid = verifyWebhook( JSON.stringify(req.body), signature, 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: str, secret: str) -> bool: 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 = request.headers.get("X-Webhook-Signature", "") if not verify_webhook(request.data, signature, 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:
| Attempt | Delay |
|---|---|
| 1st retry | ~2 seconds |
| 2nd retry | ~4 seconds |
| 3rd retry | ~8 seconds |
After 3 failed retries, the delivery is marked as failed. You can inspect failed deliveries via the API:
curl "$THETERMS_URL/webhooks/{webhookId}/deliveries" \ -H "X-Api-Key: $THETERMS_API_KEY"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.
Response 200:
{ "data": [ { "id": "018e1234-abcd-7000-8000-000000000040", "url": "https://your-server.com/webhooks/theterms", "is_active": true, "secret": "whsec_...", "org_id": "018e1234-abcd-7000-8000-000000000000", "created_at": "2026-02-01T09:00:00.000Z" } ]}Create a webhook POST /api/v1/webhooks
Section titled “Create a webhook /api/v1/webhooks”| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to receive events |
{ "url": "https://your-server.com/webhooks/theterms" }Response 201: Webhook object (same as list). The secret is shown here and never again — save it immediately for signature verification.
Error cases:
| Status | Cause |
|---|---|
400 | url is not a valid HTTPS URL |
409 | A webhook with this URL already exists |
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 5 webhook endpoints per organisation
- Each webhook can subscribe to multiple event types