> ## Documentation Index
> Fetch the complete documentation index at: https://developers.gonitro.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks & Callbacks

> Configure Nitro API webhooks and callbacks. Receive real-time Sign envelope events or per-request PDF Services job notifications via HTTP POST.

Nitro offers two distinct notification mechanisms depending on the API you are integrating with:

* **PDF Services callbacks**: per-request, point-to-point HTTP notifications attached to asynchronous jobs. Configured in the request body, fired once per request.
* **Sign webhooks**: long-lived subscriptions that deliver real-time events (envelope, document, signature) from the Sign platform. Configured once per application in the admin UI.

Pick the mechanism that matches the API you are using. They are configured and behave differently.

## PDF Services Callbacks

When you submit an asynchronous job (by setting the `Prefer: respond-async` header), you can include a `callback` URL inside the request's `delivery` block. Nitro will send a single `POST` to that URL once the job has been created, letting your service know that processing has started and where to fetch the result later.

There is no global registration, no event subscription, and no event stream. Each request opts in or out of a callback on its own.

### Providing a callback URL

Include a `callback` object inside the `delivery` field of the request body:

```json theme={null}
{
  "callback": {
    "URL": "https://your-domain.example/nitro/callbacks",
    "headers": [
      { "name": "Authorization", "value": "Bearer your-secret" }
    ]
  }
}
```

* `URL`: The `POST` endpoint Nitro should call. Required.
* `headers`: Optional list of `{ "name", "value" }` pairs sent on the callback request, useful for authenticating Nitro to your service.

The endpoint must accept `POST` requests.

### Callback payload

Nitro sends a JSON body containing the job ID and the URL to fetch the job's result:

```json theme={null}
{
  "jobID": "babe2aa7-9b5d-4eb2-a679-5fc12cf0a490",
  "location": "https://api.gonitro.dev/platform/jobs/babe2aa7-9b5d-4eb2-a679-5fc12cf0a490"
}
```

`GET <location>` returns the final response once processing completes. To poll progress before that, use `GET <location>/status`.

### When the callback fires

The callback is invoked **once**, immediately after the job record has been created. It is an acknowledgement that the request was accepted and is being processed, not a completion notification.

To know when the result is ready, poll the `location` URL, or use the `uploadResultTo` / `uploadResultsTo` delivery options to have Nitro upload the output file directly to your own endpoint or to a pre-signed S3 URL.

### See also

For per-endpoint context and the full `delivery` schema, see the request body of any Transformations, Extractions, or Conversions endpoint in the [PDF Services API reference](/api-reference/platform/transformations/optimize).

## Sign Webhooks

<Warning>Sign webhook support is not yet available. It will be introduced in an upcoming release.</Warning>

### Overview

Nitro allows applications added to the API service to register a webhook URL to receive notifications of real-time events in the platform.

### How to register a webhook

Webhook URLs are registered in the global admin user interface, along with the events you want to enable for tracking. One Webhook is permitted per application.

Follow these steps to set it up:

<Note>The steps to register a webhook and events will be detailed in future updates to this documentation</Note>

#### After Registration

Once your webhook is registered, we recommend the following best practices:

* **Scope down events**
  Only subscribe to events that your application needs to handle. This helps reduce unnecessary load on both your server and the Nitro system.

* **Test your webhook**
  The webhook registration UI includes a **Send Test Event** button. Use it as a ping tool to verify that your webhook is responsive before enabling event tracking in production.

### Handling Events

#### Event Payload

When events occur on your application, Nitro will send a POST request to your webhook URL with a JSON payload containing event details and data.
The JSON payload will have the following structure:

<CodeGroup>
  ```json JSON theme={null}
  {
    "id": "<string>",
    "type": "<string>",
    "created": "<string>",
    "data": {...}
  }
  ```
</CodeGroup>

* `id`: The event's unique id.
* `type`: Enum of the event's type. Possible values: `EnvelopeCreated`, `EnvelopeDocumentAdded`, `EnvelopeDocumentDeleted`, `EnvelopeSentForSignature`, `EnvelopeCancelled`, `SignatureRequestReassigned`, `SignatureRequestDeclined`, `SignatureRequestSigned`, `EnvelopeSigningCompleted`, `EnvelopeSealed`.
* `created`: UTC timestamp of the creation of the event.
* `data`: An object with the minimum relevant data for the event. If your process requires additional data, you have to request it via the API.

#### Event Types

The JSON payload of the event will have the following object structure, with the `data` field varying depending on the event type.

<AccordionGroup>
  <Accordion title="EnvelopeCreated">
    The envelope has been created.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "EnvelopeCreated",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                }
            }
        }
    ```
  </Accordion>

  <Accordion title="EnvelopeDocumentAdded">
    A document has been uploaded and attached to the envelope.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "EnvelopeDocumentAdded",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                }
                "document": {
                    "id": "<document_id>",
                    "name": "<file_name>",
                    "fileSizeBytes": 123456,
                    "pages": 12,
                    "origin": "Upload"
                }
            }
        }
    ```
  </Accordion>

  <Accordion title="EnvelopeDocumentDeleted">
    A document has been deleted from an envelope.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "EnvelopeDocumentDeleted",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                },
                "document": {
                    "id": "<document_id>"
                }
            }
        }
    ```
  </Accordion>

  <Accordion title="EnvelopeSentForSignature">
    An envelope has been sent for signature.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "EnvelopeSentForSignature",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                },
                "documents": [
                    { "id": "<document_id>", "name": "<file_name>", "fileSizeBytes": 123456, "pages": 12, "origin": "Upload" }
                ],
                "participants": [
                    { "email": "<participant_email>", "role": "<participant_role>" },
                    { "email": "<participant_email>", "role": "<participant_role>" }
                ]
            }
        }
    ```
  </Accordion>

  <Accordion title="EnvelopeCancelled">
    The envelope has been cancelled by the owner.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "EnvelopeCancelled",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                }
            }
        }
    ```
  </Accordion>

  <Accordion title="SignatureRequestReassigned">
    A signature request has been forwarded to another participant.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "SignatureRequestReassigned",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                },
                "signature": {}
            }
        }
    ```
  </Accordion>

  <Accordion title="SignatureRequestDeclined">
    A signature request has been declined.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "SignatureRequestDeclined",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                },
                "signature": {
                    "declinedBy": "<participant_email>"
                }
            }
        }
    ```
  </Accordion>

  <Accordion title="SignatureRequestSigned">
    A participant has signed the envelope.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "SignatureRequestSigned",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                },
                "signature": {
                    "signedBy": "<participant_email>",
                    "order": 1
                }
            }
        }
    ```
  </Accordion>

  <Accordion title="EnvelopeSigningCompleted">
    All participants have signed and sealing will start.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "EnvelopeSigningCompleted",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                }
            }
        }
    ```
  </Accordion>

  <Accordion title="EnvelopeSealed">
    The envelope is sealed and digitally signed. No further changes can be made.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "EnvelopeSealed",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                }
            }
        }
    ```
  </Accordion>

  <Accordion title="OwnerMessaged">
    The owner received a message from one of the signers.

    ```json theme={null}
        {
            "id": "<event_id>",
            "type": "OwnerMessaged",
            "created": "<created_timestamp>",
            "data": {
                "envelope": {
                    "id": "<envelope_id>",
                    "name": "<envelope_name>"
                },
                "signer": {
                  "email": "<participant_email>",
                  "order": 1
                },
                "message": "<message sent by signer>"
            }
        }
    ```
  </Accordion>
</AccordionGroup>

#### Webhook's Response

When delivering events, our system interprets your webhook's response code as follows:

* **2xx responses**: Any response in the 200–299 range is treated as a success. No further delivery attempts are made.

* **4xx responses**: Any response in the 400–499 range indicates the request reached your server. These are treated as acknowledged, and no further delivery attempts are made.

* **5xx responses or network errors**: Responses in the 500–599 range, or network failures, are considered temporary errors. Nitro will retry event delivery.

#### Backwards Compatibility

Events are implemented with backward compatibility, similar to the Nitro API.
This means there is no versioning for Events. Once your event handlers are implemented, you can expect them to continue working reliably.

Any data added to support new features will be included as optional fields, which can be ignored if your application does not require them.

#### Event Ordering

The delivery order of events is **not guaranteed** to match the real-world order of occurrence.

In most cases, events will arrive in the same sequence as the actions that triggered them. However, network conditions, retries, and asynchronous processes may result in out-of-order delivery.

For example, when the last participant signs an envelope, you might receive events in this order:

1. `EnvelopeSigningComplete`
2. `SignatureRequestSigned`

In this case, the `SignatureRequestSigned` event represents the last signer completing their signature, which logically happens before the `EnvelopeSigningComplete` event. Because these actions occur almost simultaneously, the events are sometimes delivered in the reverse order.

We recommend that your application does not rely on strict event ordering:

* Use unique event IDs to de-duplicate and track processing.
* Derive the current status from the event payload (timestamps, resource state), not from the event order.
* Design handlers to be idempotent and resilient to out-of-order messages.
* Store events as data points and replay them in your system if needed.

#### Fault tolerance

We will make a best effort to deliver webhook events reliably. In cases where delivery to your endpoint does not succeed, our system will retry sending the event.

<Note>Specific retry mechanisms will be detailed in future updates to this documentation</Note>

##### Event handler recommendations

* **Idempotency**
  In general we recommend that the endpoint implementation is idempotent and resilient to temporary delivery delays.
* **Response time**:
  Your server should respond within 15 seconds. Requests that exceed this time will be treated as failed.
* **Asynchronous Processing**:
  Your endpoint should return a `2xx` response as quickly as possible to acknowledge receipt of the event.
  Avoid performing heavy or time-consuming operations (e.g., database writes, external API calls) before sending this acknowledgement.

  If additional work is required, enqueue it for asynchronous processing after the acknowledgement has been sent.

### Security and Verification

#### SSL Certificates

Your webhook's server requires a valid SSL certificate in order to receive events from Nitro.

#### Signature Headers

Nitro signs webhook events using cryptographic signatures to guarantee their authenticity and integrity, following the [RFC 9421 standard](https://datatracker.ietf.org/doc/rfc9421/).

To validate an event, generate a cryptographic signature using the event headers together with your client secret key and then compare this signature with the one included in the event headers. Matching signatures confirm that the event originated from Nitro and that its contents have not been altered.

Follow these steps to implement signature validation:

<Steps>
  <Step title="Extract data from headers">
    The event will include these three headers:

    ```
    - Content-Digest: sha-256=:<base64-of-body-hash>:
    - Signature-Input: sig1=("@method" "@path" "host" "date" "content-digest");
    keyid="clientId"; alg="hmac-sha256"; created=1758022496
    - Signature: sig1=:Lve95gjOVATpfV8EL5X4nxwjKHE=:
    ```

    Where:

    * Content-Digest: A SHA-256 hash of the request body, base64-encoded.
    * Signature-Input: Signature parameters and metadata of signing method.
    * Signature: The cryptographic signature you will later compare.
  </Step>

  <Step title="Construct a canonical representation">
    Using the header data, build a canonical representation of the signed components as a single continuous string, making sure to follow the exact order defined in the Signature-Input header.

    ```text theme={null}
    1. "@method": POST (the HTTP method)
    2. "@path": /your-webhooks-endpoint (the URL path)
    3. "host": customer-service (the host header value)
    4. "date": Tue, 15 Sep 2025 12:34:56 GMT (The date header value, as a timestamp in RFC 1123 format)
    5. "content-digest": sha-256=:uoqx...abc=: (the same hash from Content-Digest header)
    6. "@signature-params": ("@method" "@path" "host" "date" "content-digest");
    ```

    At the end, append a line for signature parameters (created, keyid, alg) as per RFC 9421.

    ```
    created=<created>; keyid=<keyid>; alg=<alg> (metadata about the signature)
    ```

    The final result should look like this::

    ```text theme={null}
    "@method": POST
    "@path": /your-webhooks-endpoint
    "host": customer-service
    "date": Tue, 15 Sep 2025 12:34:56 GMT
    "content-digest": sha-256=:uoqx...abc=:
    "@signature-params": ("@method" "@path" "host" "date" "content-digest");
    created=1758022496; keyid="clientId"; alg="hmac-sha256"
    ```

    When writing the string in code, represent line breaks with \n. For example:

    ```python theme={null}
    signature_base = '"@method": POST\n"@path": /your-webhooks-endpoint\n"host": customer-service\n"date": Tue, 15 Sep 2025 12:34:56 GMT\n"content-digest": sha-256=:uoqx...abc=:\n"@signature-params": ("@method" "@path" "host" "date" "content-digest"); created=1758022496; keyid="clientId"; alg="hmac-sha256"'
    ```
  </Step>

  <Step title="Apply Canonicalization rules (format the string)">
    The string must be formatted according to strict rules below. Any deviation will produce a different signature. Once formatted, the result is called the `signature_base_string`, which will be the dynamic portion of the cryptographic signature.

    * Convert all header names to lowercase.
    * Remove leading/trailing whitespace from values.
    * Normalize internal whitespace.
    * If a header has multiple values, join them with commas.
    * Maintain the exact order specified in Signature-Input.
  </Step>

  <Step title="Generate signature">
    With the `signature_base_string` ready, apply the following formula in your code to generate the expected signature:

    `expected_signature = base64encode(hmac_sha256(your_secret_key, signature_base_string))`
  </Step>

  <Step title="Compare signature values">
    Compare the signature you generated with the value in the Signature header. A match confirms that the event is authentic and that its contents have not been altered.
  </Step>
</Steps>
