---
title: "Handling SignNow event callbacks"
url: "https://docs.signnow.com/docs/guides-handling-signnow-event-callbacks"
type: "page"
section: "Documentation"
slug: "guides-handling-signnow-event-callbacks"
---

# Handling SignNow event callbacks

# Handling SignNow event callbacks

This guide explains how to keep your own system in sync with signing activity in SignNow. You subscribe to the events relevant to your workflow, then deploy a server that receives SignNow callbacks and processes them.

It has two parts:

1. **Subscribe to SignNow events**: tell SignNow which events to notify you about and where to send them.
2. **Receive callbacks on your server**: build an endpoint that accepts the callback, verifies it, and runs your business logic.

Part 2 includes a complete, runnable example server you can copy and test before wiring it into production.

## The use case

A common integration requirement is to keep an external system in sync with signing activity in SignNow. When your system sends documents to SignNow for signature but is not otherwise connected to it, it has no direct way to know when a document is signed or by whom, and must be notified of these events to update its own records.

SignNow provides **event subscriptions** (also called webhooks) for this purpose. You subscribe to a specific event, and whenever that event occurs, SignNow sends an HTTP request (a callback) to a URL you control. Your server then applies the appropriate logic, such as updating a record, writing to an audit trail, notifying another system, or triggering post-processing of the signed document. SignNow is responsible for delivering the callback; providing the callback URL and implementing its logic are your responsibility.

<!-- theme: info -->

> **Note**: In SignNow, webhooks and event subscriptions mean the same thing. They are available with an API free trial, an active API subscription, or a Site License.

Here is the end to end flow:

```mermaid
sequenceDiagram
    participant Sys as Your system
    participant SN as SignNow
    participant Signer as Signer
    participant CB as Callback server

    Sys->>SN: 1. Create event subscription (event + entity_id + callback URL)
    Sys->>SN: 2. Send document for signature
    SN->>Signer: 3. Invite to sign
    Signer->>SN: 4. Sign the document
    SN->>CB: 5. POST callback (event payload)
    CB->>CB: 6. Verify signature, run business logic
    CB-->>SN: 7. Respond 200 OK (acknowledge receipt)
    CB->>Sys: 8. Update your data
```

## Part 1. Subscribe to SignNow events

A subscription answers three questions: **which event**, **on which entity**, and **where to send the callback**.

### Choose the entity level

An **entity type** limits the scope of what you get notified about. You subscribe at the level that matches how you track work in your own system.

| Entity level | Subscribe on | Example event | You are notified about |
| --- | --- | --- | --- |
| **User** | `user_id` | `user.document.complete` | Any document of that user reaching completion |
| **Document** | `document_id` | `document.complete` | One specific document reaching completion |
| **Document group** | `document_group_id` | `document_group.complete` | One specific document group reaching completion |

Subscribing at the user level is the widest net: you get callbacks for every document that user handles. Subscribing at the document or document group level is precise: you get callbacks for just that one entity. Pick the narrowest level that still covers what you need, so your server handles fewer requests.

For the full list of events at each level, see the [Webhooks guide](/docs/guides-webhooks#entity-events).

### Create the subscription

Before you begin, you need a way to authorize your API requests, the ID of the entity you are subscribing on, and a publicly reachable callback URL (Part 2 covers how to build and expose one). For authorization, you can use either an API key from the [API Dashboard](/docs/account#api-keys) or an OAuth 2.0 access token; pick whichever fits your setup. For details, see [Authentication](/docs/authentication). The examples below pass this value in the `Authorization` header after `Bearer`.

Create one subscription per event with a [POST request](/docs/manage-event-subscriptions/operations/create-event-subscription-2). The `callback` attribute is where SignNow will send the event.

```bash
curl --request POST \
  --url https://api.signnow.com/v2/event-subscriptions \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer {{access_token}}' \
  --header 'Content-Type: application/json' \
  --data '{
    "event": "document_group.complete",
    "entity_id": "4c442866d7d2fea14f0935bd350213fb55ec5a6c",
    "attributes": {
      "callback": "https://your-domain.com/signnow/callback"
    }
  }'
```

You can attach optional attributes to control delivery and security:

- **`secret_key`**: enables HMAC verification. SignNow signs each callback with this key so your server can confirm the request is authentic. You choose the value (any string up to 300 characters). Save it: you need the same value on your server.
- **`include_metadata`**: set to `true` to receive any metadata attached to the document or document group in the payload.
- **`retry_count`**: limits how many times SignNow retries when your callback URL returns a `5xx` error (1 to 10).
- **`delay`**: delays the callback by up to 100 seconds after the event.

```bash
curl --request POST \
  --url https://api.signnow.com/v2/event-subscriptions \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer {{access_token}}' \
  --header 'Content-Type: application/json' \
  --data '{
    "event": "document_group.complete",
    "entity_id": "4c442866d7d2fea14f0935bd350213fb55ec5a6c",
    "attributes": {
      "callback": "https://your-domain.com/signnow/callback",
      "secret_key": "MySecretKey",
      "include_metadata": true
    }
  }'
```

For the complete attribute reference, see [Create event subscription](/docs/manage-event-subscriptions/operations/create-event-subscription-2).

For a full step-by-step walkthrough of creating, listing, testing, and deleting subscriptions, see [Create webhook via API](/docs/guides-webhooks#create-webhook-via-api) in the Webhooks guide.

## Part 2. Receive callbacks on your server

When a subscribed event fires, SignNow sends a `POST` request to your callback URL. Your job is to accept it, confirm it really came from SignNow, and act on it.

### What the callback looks like

The body is JSON with two objects: `meta` (what happened, who did it, when) and `content` (the entity it happened to). The following is an example payload for a document group event:

```json
{
  "meta": {
    "timestamp": 1661430933,
    "event": "document_group.invite.signer.complete",
    "environment": "https://api.signnow.com",
    "callback_url": "https://your-domain.com/signnow/callback",
    "access_token": "********",
    "initiator_id": "5dXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  },
  "content": {
    "group_id": "61XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "group_invite_id": "deXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "status": "fulfilled",
    "step_order": 1,
    "signer_unique_id": "5dXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "signer_email": "signer@example.com",
    "document_ids": [
      "66XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
    ]
  }
}
```

The most useful `meta` fields are the same for every event: `event` (what happened), `initiator_id` (who triggered it), and `timestamp` (when). The `content` fields depend on the entity type: document group events carry `group_id` along with signer details such as `signer_email` and `status`, while document and user events carry `document_id` (and `user_id` for user events) instead.

### What your server must do

Inside your callback route, follow this sequence:

```mermaid
flowchart TD
    A["Receive POST at callback route"] --> B{"Valid HMAC signature?"}
    B -- "No" --> R1["Respond 401 and stop"]
    B -- "Yes" --> C{"Already handled this callback?"}
    C -- "Yes" --> R2["Respond 200 and skip"]
    C -- "No" --> D["Parse event, initiator, entity IDs"]
    D --> E["Run your business logic"]
    E --> F["Respond 200 OK within 20s"]
```

1. **Verify the request.** If you set a `secret_key`, SignNow includes an `X-SignNow-Signature` header: the payload hashed with HMAC-SHA256 using your key, base64 encoded. Recompute it over the raw body and compare. Reject with `401` if it does not match. For details, see [HMAC security](/docs/guides-webhooks#hmac-security).
2. **Deduplicate.** Retries and overlapping subscriptions can deliver the same callback more than once. Build an idempotency key from the event, entity ID, and timestamp, and skip anything you have already handled.
3. **Read the event.** Pull `event`, `initiator_id`, `timestamp`, and the entity IDs.
4. **Run your logic.** Update your record, write an audit entry, call another system, or start post-processing. This part is entirely yours.
5. **Acknowledge fast.** Return a `2xx` within the 20-second timeout. If your processing is slow, handle it asynchronously (queue it) and respond immediately: a request that times out or returns an error is treated as a failed delivery and retried, as described in [Retries](/docs/guides-webhooks#retries).

### Example callback server

The example below is a complete, runnable Express server that implements all five steps. Copy it into a file named `server.js`.

```javascript
const express = require('express');
const crypto = require('crypto');

const app = express();
const PORT = process.env.PORT || 3000;
const SECRET_KEY = process.env.SIGNNOW_SECRET_KEY || '';

const entityStatus = new Map(); // mock DB: entityId -> last event
const seenCallbacks = new Set();  // idempotency keys already handled

// Keep the raw body so the HMAC is computed over the exact bytes SignNow signed.
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; },
}));

function isValidSignature(rawBody, signatureHeader) {
  if (!SECRET_KEY) return true;          // verification disabled
  if (!signatureHeader) return false;
  const expected = crypto
    .createHmac('sha256', SECRET_KEY)
    .update(rawBody)
    .digest('base64');
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader);
  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);   // constant-time compare
}

app.post('/signnow/callback', (req, res) => {
  // 1. Verify the request really came from SignNow.
  if (!isValidSignature(req.rawBody, req.get('X-SignNow-Signature'))) {
    return res.status(401).json({ error: 'invalid signature' });
  }

  const { meta = {}, content = {} } = req.body || {};
  const event = meta.event;

  // The ID in `content` depends on the entity type the event belongs to:
  //   document events:       document_id
  //   document group events: group_id
  //   user events:           user_id (plus the document or document group that changed)
  const entityId =
    content.document_id || content.documentId || // document
    content.group_id ||                          // document group
    content.user_id || content.userId;           // user

  // 2. Deduplicate.
  const idKey = `${event}:${entityId}:${meta.timestamp}`;
  if (seenCallbacks.has(idKey)) {
    return res.status(200).json({ status: 'duplicate ignored' });
  }
  seenCallbacks.add(idKey);

  // 3. Read the event.
  console.log(`event=${event} initiator=${meta.initiator_id} entity=${entityId}`);

  // 4. Run your business logic (replace with your own).
  //    Works for any entity type: store the latest event for the entity.
  entityStatus.set(entityId, event);

  // 5. Acknowledge quickly.
  return res.status(200).json({ status: 'received' });
});

app.listen(PORT, () => console.log(`Listening on http://localhost:${PORT}`));
```

Run it from the folder that contains `server.js`:

```bash
npm init -y
npm install express
SIGNNOW_SECRET_KEY=MySecretKey node server.js
```

<!-- theme: info -->

> **Note**: Set `SIGNNOW_SECRET_KEY` to the same value you passed as `secret_key` when creating the subscription. Leave it unset to skip verification for a first quick test.

### Verifying HMAC in other languages

The verification is the same idea in any language: HMAC-SHA256 over the raw body with your `secret_key`, base64 encoded, compared against `X-SignNow-Signature`. Here it is in PHP:

```php
<?php
class HMAC_Service
{
    public static function ComputeHash($secret, $payload)
    {
        $hexHash = hash_hmac('sha256', $payload, utf8_encode($secret));
        return base64_encode(hex2bin($hexHash));
    }
    public static function HashIsValid($secret, $payload, $verify)
    {
        return hash_equals($verify, self::ComputeHash($secret, $payload));
    }
}
?>
```

### Expose your server to SignNow

SignNow reaches your server over the public internet, so `localhost` will not work directly. During development, expose it with a tunnel:

```bash
ngrok http 3000
```

Use the public HTTPS URL it prints, with your route appended, as the callback URL when you create the subscription:

```
https://<your-subdomain>.ngrok.app/signnow/callback
```

<!-- theme: warning -->

> **Note**: If your callback host is locked down by a firewall, allow inbound requests from SignNow. A callback URL that points to `localhost` or a private IP is rejected with `Host is not allowed.` (error `15006001`).

### Test the full flow

1. Start your server and expose it publicly.
2. Create an event subscription pointing at your callback URL (Part 1).
3. Trigger the event, for example by sending a document for signature and signing it.
4. Watch the callback arrive in your server logs.

You can also confirm delivery from SignNow's side: check **API Dashboard** > **Webhooks** > **Events History**, or call [Get list of callbacks by event subscription](/docs/callbacks-info/operations/get-v2-dashboard-event-history) to list the callbacks sent for a subscription.

### What to build in your business logic

The handler is where your integration earns its keep. Common patterns:

- Update the document's status in your own database.
- Append an entry to an audit trail.
- Notify a third system (CRM, ERP, ticketing).
- Start post-processing: download the signed file, extract fields, archive it.

Whatever you do, keep the acknowledgement fast and the handler idempotent.

## Reliability notes

- **Retries.** SignNow retries failed deliveries and can unsubscribe you after repeated failures. See [Retries](/docs/guides-webhooks#retries).
- **Recovery.** If you miss callbacks during downtime, reconcile against document status and, if needed, ask support to retrigger. See [Webhook recovery](/docs/guides-webhooks#webhook-recovery).
- **Troubleshooting.** For unreachable hosts, `4xx`/`5xx` handling, and duplicate callbacks, see [Troubleshooting](/docs/guides-webhooks#troubleshooting).

If your callbacks stop and your subscriptions look active, your API subscription may be inactive. Check [API Dashboard > Plan usage](/docs/account#plan-usage), and if the plan should be active, contact API support at **api@signnow.com**.

## Next steps

- Browse the full event list in the [Webhooks guide](/docs/guides-webhooks).
- See the [event subscription API reference](/docs/manage-event-subscriptions/operations/create-event-subscription-2).


---
*Full page: https://docs.signnow.com/docs/guides-handling-signnow-event-callbacks*
