Receive Real-Time Contract Events via Webhooks
Goal
Subscribe to SpotDraft contract lifecycle events so your integration can react to review, signature, and execution changes without polling every contract.
When to use this
Use this when your integration needs low-latency lifecycle updates for review, signature, execution, or repository events.
Endpoints used
GET /api/v2.1/public/webhooks/hmac_key/POST /api/v2.1/public/webhooks/DELETE /api/v2.1/public/webhooks/{webhook_id}
Prerequisites
- a public HTTPS endpoint that accepts
POSTrequests - access to the raw request body in your webhook receiver
- durable storage or a queue for event processing
- a credential that can access the target workspace
Developer Settingspermission if you will create or delete webhook subscriptions through the API
Overview
Persist the webhook id returned from the create call. You need that exact id for later rotation or deletion.
The HMAC-key fetch endpoint and the create/delete endpoints have different permission behavior:
GET /webhooks/hmac_key/only requires workspace accessPOST /webhooks/andDELETE /webhooks/{webhook_id}also requireDeveloper Settingspermission
1. Fetch the HMAC key used to verify webhook payloads
- Python
- Node.js
- curl
import requests
base_url = "https://api.in.spotdraft.com" # Replace with your workspace region.
headers = {
"client-id": SPOTDRAFT_CLIENT_ID,
"client-secret": SPOTDRAFT_CLIENT_SECRET,
"Content-Type": "application/json",
}
hmac_response = requests.get(
f"{base_url}/api/v2.1/public/webhooks/hmac_key/",
headers=headers,
timeout=30,
)
hmac_response.raise_for_status()
hmac_key = hmac_response.json()["hmac_key"]
const baseUrl = 'https://api.in.spotdraft.com'; // Replace with your workspace region.
const headers = {
'client-id': process.env.SPOTDRAFT_CLIENT_ID,
'client-secret': process.env.SPOTDRAFT_CLIENT_SECRET,
'Content-Type': 'application/json',
};
const hmacResponse = await fetch(`${baseUrl}/api/v2.1/public/webhooks/hmac_key/`, {
headers,
});
if (!hmacResponse.ok) {
throw new Error(`HMAC key fetch failed: ${hmacResponse.status}`);
}
const {hmac_key: hmacKey} = await hmacResponse.json();
curl --request GET \
--url https://api.in.spotdraft.com/api/v2.1/public/webhooks/hmac_key/ \
--header "client-id: $SPOTDRAFT_CLIENT_ID" \
--header "client-secret: $SPOTDRAFT_CLIENT_SECRET"
Persist the returned key securely. Use it only for webhook verification.
Example response:
{
"hmac_key": "BASE64_ENCODED_SECRET"
}
2. Register the webhook
- Python
- Node.js
- curl
webhook_payload = {
"title": "Contract Created webhook",
"url": "https://yourdomain.com/webhooks/event-handler",
"only_for_activities": [
"CONTRACT_CREATED",
"CONTRACT_EXECUTED",
"CONTRACT_SENT_TO_COUNTERPARTY",
],
}
webhook_response = requests.post(
f"{base_url}/api/v2.1/public/webhooks/",
headers=headers,
json=webhook_payload,
timeout=30,
)
webhook_response.raise_for_status()
webhook = webhook_response.json()
const webhookPayload = {
title: 'Contract Created webhook',
url: 'https://yourdomain.com/webhooks/event-handler',
only_for_activities: [
'CONTRACT_CREATED',
'CONTRACT_EXECUTED',
'CONTRACT_SENT_TO_COUNTERPARTY',
],
};
const webhookResponse = await fetch(`${baseUrl}/api/v2.1/public/webhooks/`, {
method: 'POST',
headers,
body: JSON.stringify(webhookPayload),
});
if (!webhookResponse.ok) {
throw new Error(`Webhook registration failed: ${webhookResponse.status}`);
}
const webhook = await webhookResponse.json();
curl --request POST \
--url https://api.in.spotdraft.com/api/v2.1/public/webhooks/ \
--header "client-id: $SPOTDRAFT_CLIENT_ID" \
--header "client-secret: $SPOTDRAFT_CLIENT_SECRET" \
--header "Content-Type: application/json" \
--data '{
"title": "Contract Created webhook",
"url": "https://yourdomain.com/webhooks/event-handler",
"only_for_activities": [
"CONTRACT_CREATED",
"CONTRACT_EXECUTED",
"CONTRACT_SENT_TO_COUNTERPARTY"
]
}'
3. Verify the signature in your receiver
Verify X-SD-WEBHOOK-CONTENT-HASH against the raw request body before any business processing.
import base64
import hashlib
import hmac
def verify_spotdraft_webhook(raw_body: bytes, received_hash: str, hmac_key: str) -> bool:
computed_hash = hmac.new(
base64.b64decode(hmac_key),
raw_body,
digestmod=hashlib.sha512,
).hexdigest()
return hmac.compare_digest(computed_hash, received_hash)
4. Persist first, process later
Recommended production behavior:
- verify the signature
- persist the raw payload and headers
- deduplicate by delivery id or payload fingerprint
- enqueue downstream work
- return
2xxquickly
Downstream work typically includes:
- syncing contract status back into your own system
- fetching contract metadata or content after execution
- generating a download link for the signed document
5. Remove or rotate a webhook
If you need to replace an endpoint, delete the old subscription explicitly.
- Python
- Node.js
- curl
webhook_id = os.environ["SPOTDRAFT_WEBHOOK_ID"]
delete_response = requests.delete(
f"{base_url}/api/v2.1/public/webhooks/{webhook_id}",
headers=headers,
timeout=30,
)
delete_response.raise_for_status()
const deleteResponse = await fetch(
`${baseUrl}/api/v2.1/public/webhooks/${process.env.SPOTDRAFT_WEBHOOK_ID}`,
{
method: 'DELETE',
headers,
},
);
if (!deleteResponse.ok) {
throw new Error(`Webhook delete failed: ${deleteResponse.status}`);
}
curl --request DELETE \
--url "https://api.in.spotdraft.com/api/v2.1/public/webhooks/$SPOTDRAFT_WEBHOOK_ID" \
--header "client-id: $SPOTDRAFT_CLIENT_ID" \
--header "client-secret: $SPOTDRAFT_CLIENT_SECRET"
Production checklist
- webhook id
- subscribed activity types
- verified payload body
- contract id and external metadata from the event payload
- your own processing state for each delivery
Production notes
- Persist the webhook id returned from the create call and use that exact id for later rotation or deletion.
- Verify signatures against the raw request body bytes before downstream processing.
- Return
2xxonly after verification and persistence, not after expensive business logic.
Common failure points
-
deleting the wrong webhook Persist the
idreturned from the create call and use that exact value for delete operations. -
signature verification on parsed JSON Verify against the raw request body bytes, not a reserialized JSON object.