Webhooks

This guide walks you through creating and consuming webhooks for the Penneo platform. Webhooks allow you to receive real-time event notifications in your application whenever relevant events occur in your Penneo account.

Before you start

  • Public Endpoint: Ensure that your endpoint is publicly accessible. Localhost URLs or private network URLs will not work.
  • Test Your Endpoint: We recommend testing with Webhook.site to confirm your endpoint setup before using your own domain. You can also use the webhook.subscription.test event type to trigger test events once your subscription is created.

Create a subscription

You can subscribe to one or more event types. Once subscribed, Penneo sends notifications to the specified endpoint whenever those events occur. A complete list of available event types can be found in our API documentation.

POST https://app.penneo.com/webhook/api/v1/subscriptions
Authorization: JWT
X-Auth-Token: <your token>
Accept-charset: utf-8
Accept: application/json
Content-Type: application/json
{
  "eventTypes": [
    "sign.casefile.completed", "sign.signer.signed"
  ],
  "endpoint": "https://example.com/webhook"
}

{
  "customerId": <int>,
  "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "isActive": true,
  "secret": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "eventTypes": [
    "string"
  ],
  "endpoint": "https://example.com/webhook"
}

❗️

Important

Webhook Subscription Limit: A rate limit of 5 active webhook subscriptions per customer is in place. This means each customer account is limited to a maximum of 5 concurrently active webhook subscriptions. If you have a business need for more than 5 active webhook subscriptions, please contact Penneo support by submitting a request here.

🚧

Note

If you are not receiving events, verify that the provided endpoint is publicly reachable. Penneo will retry unsuccessful notifications a limited number of times before giving up. See retry policy for details

Consuming webhook events

Webhook events are delivered as HTTPS POST requests with a Content-Type of application/json.

Each request body contains the event payload, and a number of HTTP headers provide metadata to help you identify and validate the request. Your integration should read and parse the headers along with the request body.

Webhook requests must be responded to within 1 second. Connection attempts that take longer than 200ms will fail, and responses that exceed 1 second will be timed out. If your processing exceeds these limits, we recommend buffering the request internally (e.g., enqueuing it) and acknowledging it immediately with a 2xx response.


Verifying webhook event integrity

Before trusting an incoming event, we strongly recommend validating its authenticity and integrity. Each event delivery includes an X-Event-Signature header composed of several components. You should perform the following validations, in order:

Parsing the X-Event-Signature header

The value of the X-Event-Signature header is a comma-separated list of key-value pairs. Each component is in the form:

key=value

Example:

t=1714567890,h=x-event-id x-event-type,v1=abc123...

You should parse the string into a map and ensure the following required components are present:

  • t: the signed timestamp
  • h: the list of signed headers
  • v1: the signature

Reject the request if the header is malformed or any of these components are missing.

1. Validate the timestamp

The t component contains the UNIX timestamp of when the signature was generated.

To prevent replay attacks, validate that this timestamp is within an acceptable threshold (e.g., ±5 minutes) from the current time. Reject the request if the difference between the current time and the signed timestamp exceeds the allowed threshold.

2. Validate the signature

Use the values from the parsed X-Event-Signature components to validate the request:

  1. Extract the values of the headers listed in the h component (case-insensitive).

  2. Concatenate the following parts using . as a separator:

    {t}.{h}.{header values joined with '.'}.{raw request body}
    
  3. Compute an HMAC-SHA256 digest of this string using the subscription’s secret as the key.

  4. Compare the resulting hex digest to the v1 value using a constant-time comparison.

Reject the request if any component is missing or if the signature does not match.

The secret token is provided when you create the subscription via the Create Subscription endpoint.
If needed, it can also be retrieved later using the Get Subscriptions endpoint.
Webhook events are delivered at-least-once. You should design your webhook event handlers to be idempotent using the X-Event-Id header.
Only 2xx responses are considered successful. Any other response code—including 3xx, 4xx, or 5xx—will trigger a retry.

3. Validate the event ID

Each request includes a unique event identifier in the X-Event-Id header.

To prevent replay of previously seen events, verify that the event ID has not already been processed. We recommend caching event IDs for a short period (e.g., the same duration as the timestamp threshold) to avoid duplicates.

Retry policy

A request can fail for various reasons, most commonly because the target endpoint is not publicly accessible. In addition, a request attempt will fail if it takes longer than 200ms to establish a connection and will timeout after 1 second.

If we fail to send a request to the subscription’s endpoint, we will retry according to the following strategy:

  1. Up to 5 “fast” retries: Each retry occurs after an interval that starts at 5 seconds and increases by 5 seconds each time (e.g., 5s, 10s, 15s…), until we have attempted 5 fast retries.
  2. Up to 30 “slow” retries: If it still fails after the fast retries, we switch to slow retries, performing one retry per hour for up to 30 attempts.

After all 35 total retries (5 fast + 30 slow) have been exhausted without success, we stop retrying entirely and the subscription is automatically disabled. An email notification is sent to the user who created the subscription, as well as all administrators of the associated customer.

To resume event delivery, the subscription must be manually updated (e.g., to fix the URL) and re-enabled using the Edit Subscription endpoint.

Specifically for the Sign API

By default, the payload of the webhook contains only a few details: a status code and the numeric ID for either the casefile or the signer.

{
  "topic": "casefile",
  "eventType": "rejected",
  "eventTime": {
    "date": "2023-01-01 12:59:59.000000",
    "timezone_type": 3,
    "timezone": "UTC"
  },
  "payload": {
    "id": <int>,
    "status": <int>
  }
}
{
  "topic": "signer",
  "eventType": "finalized",
  "eventTime": {
    "date": "2023-01-01 12:59:59.000000",
    "timezone_type": 3,
    "timezone": "UTC"
  },
  "payload": {
    "id": <int>,
    "caseFile": { 
      "id": <int>, 
      "status": <int>
    }
  }
}

❗️

Important

Webhook notifications are sent for all case files created within the same account, including those created by other team members. Unless you have access to the relevant case file or it’s in a shared folder, you might not have permission to fetch further details for that case file or signer.

If you need a more detailed payload (e.g., title, signer information, etc.), consider creating a super API user. For more information, see Archiving signed documents.

Authorization

Webhooks can use any of the supported Penneo authorization methods, not just JWT as shown in the examples.

Supported subscription event types

Below is an overview of the event types you can subscribe to. You can include one or more of these in a single subscription.

[
“sign.casefile.completed”,
“sign.casefile.expired”,
“sign.casefile.failed”,
“sign.casefile.rejected”,
“sign.signer.requestSent”,
“sign.signer.requestOpened”,
“sign.signer.opened”,
“sign.signer.signed”,
“sign.signer.rejected”,
“sign.signer.reminderSent”,
“sign.signer.undeliverable”,
“sign.signer.requestActivated”,
“sign.signer.finalized”,
“sign.signer.deleted”,
“sign.signer.signedWithImageUploadAndNAP”,
“sign.signer.transientBounce”,
“webhook.subscription.test”
]

🚧

Migrating from Topics to Event Types (old vs. new solution)

In the old (now deprecated) webhook solution, you subscribed to topics. In the new solution, you subscribe to event types instead. For example, if you previously subscribed to the “casefile” topic, you can now achieve the same by subscribing to all “casefile” event types.

Event types follow this naming format: app.topic.event. For instance the payload for a new subscription would be:

{
  "eventTypes": [
    "sign.casefile.completed",
    "sign.casefile.expired",
    "sign.casefile.failed",
    "sign.casefile.rejected"
  ],
  "endpoint": "https://example.com"
}

Casefile Events

Event TypeDescription
sign.casefile.completedEveryone has signed and the finalized PDF documents are ready for download.
sign.casefile.expiredThe signing process has expired (e.g., a deadline was reached without all required signatures).
sign.casefile.failedAn error occurred on our side; this may require contacting support.
sign.casefile.rejectedOne or more signers have rejected the signing request.

Signer Events

Event TypeDescription
sign.signer.requestActivatedThe signer is ready to sign; either the case file has been activated, or their signing round has just come up.
sign.signer.requestSentThe initial signing request email has been sent out.
sign.signer.reminderSentA signing reminder email has been sent.
sign.signer.requestOpenedA signing request email has been opened.
sign.signer.undeliverablePenneo cannot send emails to the signer; check the signer’s email address.
sign.signer.openedThe signer has viewed the signing page.
sign.signer.rejectedThe signer has rejected the signing request.
sign.signer.signedThe signer has signed.
sign.signer.signedWithImageUploadAndNAPThe signer has signed using image upload and NAP (if applicable to your organization’s signing flow).
sign.signer.finalizedThe casefile this signer belongs to has been finalized.
sign.signer.deletedThe signer has been deleted from the casefile.
sign.signer.transientBounceThe signer’s email temporarily bounced.

Other Events

Event TypeDescription
webhook.subscription.testA test event you can trigger to ensure your webhook endpoint is set up correctly and can receive notifications.

📘

Notes and Additional Details

  • Multiple Occurrences: Some events, such as requestSent, opened, or signed, can occur multiple times if a signer signs in multiple rounds.
  • Subscribe to Multiple Events: You can combine multiple event types in a single subscription to avoid creating separate subscriptions for each.

If you have any questions or need assistance, please contact our support team.


What’s Next