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.
- As a team admin, open Admin > Developers in the main menu and find the Webhooks section.
- Click Create new Webhook and enter a name, the target URL and at least one trigger.
- Copy the Verification key shown on the webhook. It starts with
whsec_and is used to verify that requests come from Resly. - 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.idis not a real envelope.
New triggers are not added to existing webhooks automatically.
Events
| Event | Sent when |
|---|---|
envelope.sealed | All 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" }
}| Field | Description |
|---|---|
type | The event name. |
teamId | Your team ID. |
timestamp | ISO 8601 time in UTC when the event occurred. Identical on every retry of the same event. |
data | The 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:
| Header | Description |
|---|---|
content-type | application/json |
webhook-id | Unique ID of this delivery event. Each webhook gets its own ID for the same seal, and it stays the same across retries. |
webhook-timestamp | Unix timestamp (seconds) of when this delivery attempt was signed. |
webhook-signature | Signature in the format v1,<base64>. |
webhook-version | 2 |
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.
| Retry | Wait after the previous attempt |
|---|---|
| 1 | 1 min |
| 2 | 2 min |
| 3 | 4 min |
| 4 | 8 min |
| 5 | 16 min |
| 6 | 32 min |
| 7 | 64 min |
| 8 | about 2.1 h |
| 9 | about 4.3 h |
| 10 to 17 | 6 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.