Guide to SignNow Webhooks: Features and Best Practices
SignNow webhooks tell your systems where a document is in its signing process: an invite is sent, a document is opened, a signer signs or declines, all fields are complete, or a document group finishes. If you send documents for signature with SignNow, the airSlate e-signature platform, webhooks are how that progress reaches your CRM, document storage and workflow engine without polling.
This guide covers how SignNow event subscriptions are scoped and created, how to make sure they are signed, the events and payloads they send, the retry and unsubscribe rules, and the practices that keep a SignNow receiver reliable.
What are SignNow webhooks?
In SignNow, webhooks and event subscriptions are the same thing. A subscription links one event, such as document.complete, to one entity and one callback URL. The entity is a user, a document or a document group, identified by its ID, and it limits which activity the subscription hears about. A user subscription covers what that user does, a document subscription covers everything that happens to that document, and a document_group subscription covers everything that happens to that group.
Each subscription is a separate POST to the API, so integrations that need several events create several subscriptions. Webhooks are available with an API free trial, an active API subscription or a Site License.
SignNow only signs callbacks when the subscription was created with a secret_key. You choose the key yourself. When it is set, every callback carries an X-SignNow-Signature header holding the base64 HMAC-SHA256 of the request body. When it isn't, there is nothing to verify.
SignNow webhook features
| Feature | Details |
|---|---|
| Configuration | POST https://api.signnow.com/v2/event-subscriptions, or API Dashboard > Webhooks |
| Scope | One event, one entity (user, document or document_group) and one callback URL per subscription |
| Limits | 20 active subscriptions per entity and event. 10 in total without an API subscription |
| Event identity | meta.event in the body |
| Signature header | X-SignNow-Signature, only with a secret_key |
| Scheme | HMAC-SHA256, base64 |
| Signed content | The raw request body |
| Secret | Your own string, up to 300 characters, set per subscription |
| Replay window | None documented. meta.timestamp is in the signed body |
| Response window | 20 seconds per attempt |
| Retries | Depend on the error: see below |
| Options | retry_count, delay (up to 100 seconds), include_metadata, custom headers |
| Logs | API Dashboard > Webhooks > Events History, or the callbacks API |
The callback URL must be publicly reachable. SignNow rejects localhost and private IP addresses, and checks that the host can be reached when you create the subscription. It publishes no source IP list.
Common events
SignNow documents 96 events in three families. The document and document_group families describe what happens to one document or group, and the user family mirrors them for everything a user owns. Here are the ones most integrations need:
| Event | Fires when |
|---|---|
document.fieldinvite.sent | A field invite for the document is sent |
document.open | The document is opened |
document.fieldinvite.signed | A field invite for the document is signed |
document.fieldinvite.decline | A field invite for the document is declined |
document.complete | All the required fields on the document are filled in |
document_ | A signer completes all documents in a group invite |
document_group.complete | All required fields in a document group are filled in, or the invite has expired |
user.document.fieldinvite.signed | A field invite sent by the user is signed |
user.document.complete | All the required fields on one of the user's documents are filled in |
The full list also covers updates and deletes, templates, freeform invites, reassignments, consent, failed authentication, failed invite emails and expired invites. Spelling is not consistent between families: a declined field invite is document.fieldinvite.decline, but a declined group invite is document_. Copy event names from SignNow's tables rather than guessing them.
Every callback has two parts, meta and content. Here is SignNow's user.document.open example:
{
"meta": {
"timestamp": 1728310375,
"event": "user.document.open",
"environment": "https://app.signnow.com/api",
"callback_url": "https://webhook.site/ba2650c4-f8a3-4664-a42f-a1ea40b17bbc?Param={\"docId\":\"3cd9758f093545089954894be9da90af210741b0\"}",
"access_token": "********",
"initiator_id": "646a5e78f4154a0880a596b11c21a95866ee66e2",
"metadata": {
"sales_rep": "Jane Doe",
"source": "webhook"
}
},
"content": {
"document_id": "3cd9758f093545089954894be9da90af210741b0",
"document_name": "Sales Agreement",
"user_id": "646a5e78f4154a0880a596b11c21a95866ee66e2"
}
}
meta.event names the event and meta.timestamp is in Unix seconds. content depends on the entity: document events carry document_id and document_name, while group invite events carry group_id, group_invite_id, status, the signer's details and document_ids. meta.metadata only appears when the subscription sets include_metadata, and it is an object when the document has metadata but an empty array ([]) when it doesn't, so handle both.
The documented examples show access_token masked. SignNow doesn't say whether live callbacks mask it, so treat callback bodies as sensitive and keep them out of general-purpose logs.
Setting up SignNow webhooks
- Choose a
secret_key: any string up to 300 characters, such as the output ofopenssl rand -hex 32. Store it server-side asSIGNNOW_WEBHOOK_SECRET. - Get an access token and the ID of the user, document or document group you want to watch.
- Create one subscription per event, with the secret in
attributes.secret_key:
curl -X POST "https://api.signnow.com/v2/event-subscriptions" \
-H "Authorization: Bearer $SIGNNOW_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event": "document.complete",
"entity_id": "0f7848d0cfadeb2cdd230e4ad433efbca2423d8f",
"attributes": {
"callback": "https://example.com/webhooks/signnow",
"secret_key": "'"$SIGNNOW_WEBHOOK_SECRET"'",
"include_metadata": true
}
}'
In the API Dashboard, go to Webhooks and click Create Webhook to create several subscriptions at once. Enter your own key, or click Generate New Key and copy it before you save. GET /v2/event-subscriptions returns the configured secret_key if you need to check it later.
If you use Hookdeck, create a SignNow source, set the same key as its Webhook Signing Secret, and use the source URL as the callback. Set a secret_key on every subscription that points at the source. Without one, SignNow sends unsigned callbacks and they fail verification. If you decide not to sign at all, leave verification off on the source. Because each subscription has its own key, give every subscription that shares a Hookdeck source the same secret_key.
Securing SignNow webhooks
Compute an HMAC-SHA256 of the raw request body with the secret_key as a UTF-8 string, base64-encode the raw digest, and compare it with X-SignNow-Signature in constant time. SignNow's PHP sample computes a hex digest and converts it to base64, which gives the same value.
const crypto = require('crypto');
const express = require('express');
function verifySignNowSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !secret) return false;
// base64 of the raw digest, not hex. Base64 is case-sensitive: don't lowercase it.
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
const received = Buffer.from(signatureHeader.trim(), 'utf8');
const computed = Buffer.from(expected, 'utf8');
return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}
const app = express();
app.post('/webhooks/signnow', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.SIGNNOW_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifySignNowSignature(req.body, req.get('x-signnow-signature'), secret)) {
return res.sendStatus(401);
}
const callback = JSON.parse(req.body.toString('utf8'));
// Route on callback.meta.event. Deduplicate on event + entity ID + meta.timestamp.
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
def verify_signnow_signature(raw_body: bytes, signature_header, secret: str) -> bool:
if not signature_header or not secret:
return False
digest = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).digest()
expected = base64.b64encode(digest)
return hmac.compare_digest(signature_header.strip().encode("utf-8"), expected)
# In FastAPI: raw_body = await request.body()
# verify_signnow_signature(raw_body, request.headers.get("x-signnow-signature"),
# os.environ["SIGNNOW_WEBHOOK_SECRET"])
To check your verifier, use the secret MySecretKey and this exact body:
{"meta":{"timestamp":1728310375,"event":"document.complete"},"content":{"document_id":"3cd9758f093545089954894be9da90af210741b0"}}
The signature should be:
zcUS9p5uMwBbxv4tjBPKPsH0u1MUd+GiNQqDWv0PYcM=
Make SignNow webhooks production-ready. Hookdeck Event Gateway verifies the
X-SignNow-Signatureheader, answers SignNow inside its 20-second window, and durably queues every document and invite event before your service sees it.
SignNow webhook limitations and pain points
Unsigned unless you ask
The Problem: Callbacks arrive without X-SignNow-Signature, so they can't be verified.
Why It Happens: SignNow only signs callbacks for subscriptions created with a secret_key, and its own quick-start examples create subscriptions without one.
Workarounds:
- Include
secret_keyin every create request, and check new subscriptions in the API Dashboard. - Reject callbacks without a valid signature once all subscriptions have a key.
- Recreate older subscriptions that were set up without a key.
How Hookdeck Can Help: Hookdeck's SignNow source verifies the header, and records unsigned or mis-signed callbacks as failed verification instead of delivering them to your service.
Client errors can unsubscribe you
The Problem: Callbacks stop arriving entirely, and you find a cancellation email.
Why It Happens: SignNow never retries a 4xx. If it gets 10 or more in 10 minutes it sends a warning email, and at 30 or more in 60 minutes it unsubscribes you. An unreachable host gets 5 retries 10 seconds apart and up to 2 more 4 hours apart, then the subscription is cancelled. Only 5xx errors never lead to unsubscribing.
Workarounds:
- Return
5xx, not4xx, when the problem is on your side, such as a database outage. - Make sure the warning emails reach someone who will act on them.
- After fixing the endpoint, reactivate or recreate the subscriptions, then recover missed events from the callbacks log.
How Hookdeck Can Help: Hookdeck accepts callbacks while your service is down or failing, so SignNow sees successful deliveries and never counts your errors towards unsubscribing. Hookdeck then retries delivery to your service on your own schedule.
One subscription per event and entity
The Problem: Missing events, or the same event delivered several times.
Why It Happens: Each subscription covers one event for one entity. A forgotten event means a gap, and up to 20 active subscriptions can share an entity and event as long as their callback URLs differ, so overlaps produce duplicates.
Workarounds:
- Keep your subscription list in code and reconcile it against
GET /v2/event-subscriptions. - Prefer
usersubscriptions when you want everything a user sends, rather than one per document. - Filter Subscriptions in the API Dashboard by event and entity to find extras.
How Hookdeck Can Help: With every subscription pointing at one Hookdeck source, you can see all SignNow callbacks in one place, and deduplication can drop the copies that overlapping subscriptions create.
No event ID
The Problem: Nothing in a callback marks it as a retry or gives it a unique ID.
Why It Happens: Callbacks carry no event ID. SignNow's own advice is to build a key from meta.timestamp, the event name and the entity ID.
Workarounds:
- Deduplicate on
meta.event, the entity ID fromcontentandmeta.timestamp. - Make handlers idempotent, so a repeated
document.completedoes no harm.
How Hookdeck Can Help: Hookdeck's deduplication can drop repeated callbacks based on those same body fields.
Best practices
Verify against the raw body
The HMAC covers the exact bytes SignNow sent. In Express, use express.raw({ type: '*/*' }) and parse after verification. In Next.js, read await request.text(). In FastAPI, read await request.body().
Handle both shapes of meta.metadata
With include_metadata on, meta.metadata is an object when the document has metadata and [] when it doesn't. Normalise it before reading fields.
Limit 5xx retries where duplicates are costly
retry_count caps the retries SignNow makes for 5xx errors at a value from 1 to 10. Lower it if a late duplicate is worse than a missed event, and rely on reconciliation for the rest.
Recover from the callbacks log
GET /v2/event-subscriptions/{subscription_ lists the callbacks SignNow sent, with their response codes. Use it to find gaps after an outage, and compare against document status for anything critical.
Acknowledge fast and process asynchronously
Each attempt times out after 20 seconds. Verify, store the callback, return 200, then download documents or update records afterwards. See why to process webhooks asynchronously and our guide to webhook idempotency.
Conclusion
SignNow event subscriptions send a meta and content callback for one event on one user, document or document group, and they are signed only if the subscription has a secret_key. Set one on every subscription, verify the base64 HMAC-SHA256 in X-SignNow-Signature against the raw body, route on meta.event, deduplicate on event, entity and timestamp, and avoid 4xx responses, which SignNow never retries and which can unsubscribe you.
Hookdeck Event Gateway sits in front of your receiver, checks the SignNow signature, and holds each callback until your service has processed it, so your errors don't cost you a subscription.
Get started with Hookdeck for free and handle SignNow webhooks reliably in minutes.