Webhooks

Get notified when an envelope is sealed, and verify each request.

Webhooks let Resly Sign notify your system when something happens, so you do not have to poll the API. Resly sends an HTTP POST with a JSON body to a URL you choose.

Set up a webhook

Webhooks are managed in the Resly app, not through the REST API.

  1. As a team admin, open Admin > Developers in the main menu and find the Webhooks section.
  2. Click Create new Webhook and enter a name, the target URL and at least one trigger.
  3. Copy the Verification key shown on the webhook. It starts with whsec_ and is used to verify that requests come from Resly.
  4. Use Send test event in the webhook menu to send sample data to your URL. You can only send a test event for an event the webhook subscribes to. A test event is sent once, is not retried or listed under Webhook Requests, and its data.id is not a real envelope.

New triggers are not added to existing webhooks automatically.

Events

EventSent when
envelope.sealedAll signers have signed and Resly has sealed the envelope (the final PDF exists).

Events are sent for every envelope in the team, including envelopes created in the Resly app. envelope.sealed is not sent for envelopes created by a flow. Return 2xx for event types and envelopes you do not recognize, and ignore them: more events will be added over time.

The request

Resly always sends a POST with this JSON body (data shortened):

{
  "type": "envelope.sealed",
  "teamId": "64f0c0ffee0123456789abcd",
  "timestamp": "2026-01-15T09:30:00.000Z",
  "data": { "id": "64f0c0ffee0123456789abce", "status": "SEALED" }
}
FieldDescription
typeThe event name.
teamIdYour team ID.
timestampISO 8601 time in UTC when the event occurred. Identical on every retry of the same event.
dataThe envelope as it was when the event occurred, the same shape as the response of Get Envelope. Retries send the same data; call Get Envelope for the current state.

Headers:

HeaderDescription
content-typeapplication/json
webhook-idUnique ID of this delivery event. Each webhook gets its own ID for the same seal, and it stays the same across retries.
webhook-timestampUnix timestamp (seconds) of when this delivery attempt was signed.
webhook-signatureSignature in the format v1,<base64>.
webhook-version2

Verify the signature

Verify every request with a Standard Webhooks library, using the verification key as the secret. Verify the raw request body, exactly as received: parsing and re-serializing the JSON changes the bytes and breaks the signature.

import express from 'express'
import { Webhook } from 'standardwebhooks'

const webhook = new Webhook(process.env.RESLY_WEBHOOK_SECRET)
const app = express()

app.post('/resly-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  let event

  try {
    event = webhook.verify(req.body, req.headers)
  } catch {
    return res.status(400).send('Invalid signature')
  }

  if (event.type === 'envelope.sealed') {
    // Queue event.data.id for processing; skip webhook-id values you have already handled
  }

  res.sendStatus(200)
})

verify throws if a header is missing, the signature does not match, or webhook-timestamp is more than 5 minutes away from your server clock. On success it returns the parsed body.

To verify without a library: compute an HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, using the base64-decoded part of the verification key after whsec_ as the key. Base64-encode the result and compare it, in constant time, with the part after v1, in each space-separated entry of webhook-signature. Also reject the request if webhook-timestamp is more than 5 minutes away from your server clock, to block replayed requests.

Respond and retries

Return a 2xx status within 30 seconds, and do slow work afterwards, for example in a queue. A non-2xx status, a timeout or a connection error counts as a failed delivery, and Resly retries it. Use the final URL: redirects are followed, but a 301, 302 or 303 turns the POST into a GET without a body.

Retries use exponential backoff. The wait before a retry is measured from the previous attempt: it starts at 1 minute and doubles each time, up to 6 hours. A job checks once a minute, so a retry is usually sent within a minute of that time, sometimes later.

RetryWait after the previous attempt
11 min
22 min
34 min
48 min
516 min
632 min
764 min
8about 2.1 h
9about 4.3 h
10 to 176 h

There is one initial delivery and up to 17 retries, the last about 2.4 days after the event. After that the event is abandoned. Under Webhook Requests on the Developers page you can inspect the 30 most recent events, with each request and your endpoint's response. Editing a webhook's URL also applies to retries that are still pending. Deleting a webhook deletes its events too, which cancels pending retries. If a webhook misbehaves, contact [email protected] before you delete it, so its events can still be inspected.

Because of retries, the same event can reach you more than once. Store webhook-id and ignore ones you have already processed. Do not rely on event order.

Get the signed file

When you receive envelope.sealed, request a download URL with Download Content: set objectType to envelopes, objectId to the envelope ID and fileId to content[].sealedFileData.fileId. Each content item has its own signed PDF. URLs are valid for 600 seconds and need no API key, so request one when you need it.

Get Envelope and Download Content return 403 if the member connected to the API key has no access to the envelope, for example one created in the Resly app and not shared with that member. See the access rules under Authentication.