Webhooks on Legalesign
What are webhooks
Webhooks are your URLs where Legalesign will send real-time status updates. You can create your own URL webhooks or generate them within your preferrered automation system.
For example, in our demo we use a Microsoft Power Automate webhook to trigger a flow that saves newly signed PDFs to Sharepoint. Learn more about MS Power Automate.
Why you need webhooks
Webhooks enable you to maintain an events-driven system. Webhooks will update you on all the events within your groups. You might use them to create your own live dashboard and/or use them to sync with your own database.
As mentioned above, generated webhooks from automation systems like Power Automate enable you to kick off storage procedures, emails, record keeping, etc, which, combined with the API, provides everything you need for process automation.
Types of webhook
- Realtime events
- All events every 6 minutes (legacy)
- Upon [event] (legacy)
For legacy webhooks see: legacy webhooks
Webhooks are most commonly used to download a signed PDF. When you create the webhook apply the event filter 'Final PDF created'. In your code, the incoming request.body is JSON, parse it and extract documentId = ['data']['uuid']
. Now issue the API query to download the PDF - GET https://eu-api.legalesign.com/api/v1/pdf/${documentId}/
.
How to add or remove a webhook
Add or remove a webhook using the web app
Go to API dashboard and click on 'Config'. Scroll down to the webhooks section. The form has simple controls to add or remove webhooks. In the image below an ngrok endpoint is receiving all events.
Please note you must have two factor authentication on to use the API dashboard.
Your webhooks receive events from all accounts where you are an admin, both dev and prod. Use the group filter to create different webhooks for your various groups.
Add or remove a webhook using the API
For more information see the API documentation: REST API webhooks
What's in a webhook?
The quickest way to inspect your data is to add a webhook, send/sign/reject some test documents in the web app, and then view the webhooks log in the Legalesign API Dashboard.
If you need a temporary webhook URL for testing then check out ngrok.
Quick how-to on ngrok: once you download ngrok, fire it up in the terminal with ./ngrok http 80
and it will give you an https address. Enter that as your webhook. Now open http://127.0.0.1:4040. That's it, you'll start seeing all the webhooks and their data. Start sending and signing test docs. Note: Ngrok may return a 5XX error, so you can expect to see multiple attempts and error messages since the system will consider the 5XX status a failed attempt.
Ensure your receiving webhook code returns a 2XX success response. If you have several possible exceptions in your code return different 5XX status code. The webhook logs in the API dashboard will then tell you which exception was triggered.
The format of realtime webhook
The format of data can be one of two schemas, either a 'document' schema or a 'recipient' schema. Inspect the 'object' attribute to determine which schema you are dealing with.
Both schema also have an 'event' attribute. You can filter by object and optionally event when you create the webhook.
A table of all possible objects and events is below. This image illustrates a document object (for a 'created' event). The full JSON schemas are appended to the foot of this article.
Requests arrives as a POST request containing JSON. Please note, the contents of 'data' may expand to include more fields over time.
Use 'tag' attributes. You can set up to 3 tags when you create a document. By using tags you may not have to save Legalesign IDs. You can also use tag to save a secret in order to verify the inbound request (thank you Themis for that suggestion).
Table of possible combinations for 'object' and 'event':
object | event |
---|---|
document | created |
document | rejected |
document | finalPdfCreated |
recipient | completed |
recipient | rejected |
recipient | emailOpened |
recipient | visiting |
recipient | bounced |
recipient | autoReminderEmail |
Most events only occur once. The exceptions are visiting, bounced, and autoReminderEmail, they may occur many times.
Don't forget to turn off CSRF protection for your view receiving these POST requests.
Here is an example of a 'recipient' object, with event 'bounced':
Notice emailBounce and emailBounceMessage in 'data'. 'emailBounceMessage' will indicate the type of bounce as follows:
- Hard bounce: "Message hard bounced"
- Soft bounce: "Email soft bounced (either out of office or a timeout)"
- Delayed: "Message delayed (check email domain exists)"
Debug webhooks in the API dashboard
All webhooks are logged and you can examine their content and the http status code in your API dashboard. To learn more check out the Dashboard tutorial
Go to direct to API dashboard.
Status codes
Click these links to see the reference table for document status and recipient status.
Verify a webhook
The webhook signs the data packet with a private key. You can verify it with the public key - download public key.
- The location of the signature string is in the header X-Signed-SHA256.
- The data that is signed is the whole request.body (as a string).
The 'signature' is base64 encoded. Here is sample verification code in node.js:
const crypto = require('crypto');
const fs = require('fs');
const cert = fs.readFileSync('/location/of/downloadedCert.crt', 'utf8');
const c = new crypto.X509Certificate(cert);
const k = c.publicKey
let verifier = crypto.createVerify('SHA256');
let rawdata = 'sentdata' //JSON stringify data in request.body
let sha256signature = 'xxx' //value of 'X-Signed-Sha256' request header.
verifier.update(rawdata);
return verifier.verify(k, sha256signature, 'base64');
Optionally HMAC based verification is available too. For HMAC, a secret key for your webhooks is shared with you. The signed value will arrive in the header X-HMAC-SHA256.
There are lots of online resources that demonstrate how to verify an HMAC value. Sample verification in python would be:
import hmac, hashlib
sentdata = 'sentdata' # JSON stringify data in request.body
hmacvalue = 'xxx' # value of 'X-HMAC-Sha256' in request header.
v = hmac.new('your secret value'.encode(), 'sentdata'.encode(), hashlib.sha256)
isValid = v.hexdigest() == hmacvalue
Or sample Salesforce Apex code to verify a HMAC is:
String message = 'JSONstring'; //sent in request.body
String privatekey = 'privateKey'; //shared value you have been given
String hmacvalue = 'xxx'; //value of 'X-HMAC-Sha256' in request header.
Boolean verified = Crypto.verifyHMac('HmacSHA256', Blob.valueOf(message), Blob.valueOf(privatekey), EncodingUtil.convertFromHex(hmacvalue));
More webhook?
If you need more webhooks for different events, or more data in them, contact us and get your request in the development pipeline.
Schema reference
There are two possible schemas, one for object 'document' and another for object 'recipient'. Both schema have top level keys 'object' and 'event'. Inspect the 'object' attribute to determine the schema you are dealing with. Or use a webhook filter to return only document or only recipient objects.
Please note that content within the 'data' attribute may increase at any time (but will not decrease).
1. JSON Schema for 'document' object
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"version": {
"type": "string",
"default": "1.0.0"
},
"object": {
"type": "string",
"default": "document"
},
"created": {
"type": "integer",
"description": "unix timestamp"
},
"id": {
"type": "string",
"format": "uuid"
},
"event": {
"type": "string",
"pattern": "^(created|rejected|finalPdfCreated)$"
},
"data": {
"type": "object",
"properties": {
"tag1": {
"type": "string"
},
"recipients": {
"type": "array",
"items": [
{
"type": "object",
"properties": {
"uuid": {
"type": "string",
"format": "uuid"
},
"email": {
"type": "string",
"format": "email"
},
"order": {
"type": "integer"
},
"status": {
"type": "integer"
"pattern": "^(4|5|10|15|20|30|35|39|40|50|60)$"
},
"lastname": {
"type": "string"
},
"roleText": {
"type": "string",
"pattern": "^(signer|approver|witness)$"
},
"firstname": {
"type": "string"
},
"statusText": {
"type": "string",
"pattern": "^(unsent|scheduled|sent|email opened|visited|fields complete|fields complete excluding signature|witnessing required|completed|download final document|rejected|withdrawn)$"
},
"resourceUri": {
"type": "string",
"format": "uri-reference"
},
"rejectReason": {
"type": "string"
}
},
"required": [
"uuid",
"email",
"order",
"status",
"lastname",
"roleText",
"firstname",
"statusText",
"resourceUri",
"rejectReason"
]
}
]
},
"groupResourceUri": {
"type": "string",
"format": "uri-reference"
},
"statusText": {
"type": "string",
"pattern": "^(available|fields complete|completed|removed|rejected|unknown)$"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
},
"resourceUri": {
"type": "string",
"format": "uri-reference"
},
"uuid": {
"type": "string",
"format": "uuid"
},
"tag2": {
"type": "string"
},
"group": {
"type": "string"
},
"status": {
"type": "integer",
"pattern": "^(10|20|30|40|50)$"
}
},
"required": [
"tag1",
"recipients",
"groupResourceUri",
"statusText",
"name",
"tag",
"resourceUri",
"uuid",
"tag2",
"group",
"status"
]
}
},
"required": [
"version",
"object",
"data",
"created",
"id",
"event"
]
}
2. JSON schema for 'recipient' object
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {
"version": {
"type": "string",
"default": "1.0.0"
},
"object": {
"type": "string",
"default": "recipient"
},
"created": {
"type": "integer",
"format": "date-time"
},
"id": {
"type": "string",
"format": "uuid"
},
"event": {
"type": "string",
"pattern": "^(emailOpened|bounced|visiting|rejected|completed|autoReminderEmail)$"
},
"data": {
"type": "object",
"properties": {
"tag": {
"type": "string"
},
"tag1": {
"type": "string"
},
"tag2": {
"type": "string"
},
"uuid": {
"type": "string",
"format": "uuid"
},
"email": {
"type": "string",
"format": "email"
},
"group": {
"type": "string",
"format": "uuid"
},
"order": {
"type": "integer"
},
"status": {
"type": "integer",
"pattern": "^(4|5|10|15|20|30|35|39|40|50|60|70)$"
},
"document": {
"type": "string"
},
"documentName": {
"type": "string"
},
"lastname": {
"type": "string"
},
"roleText": {
"type": "string"
},
"firstname": {
"type": "string"
},
"statusText": {
"type": "string",
"pattern": "^(unsent|scheduled|sent|email opened|visited|fields complete|fields complete excluding signature|witnessing required|completed|download final document|rejected|withdrawn)$"
},
"emailBounce": {
"type": "integer"
},
"resourceUri": {
"type": "string",
"format": "uri-reference"
},
"rejectReason": {
"type": "string"
},
"groupResourceUri": {
"type": "string",
"format": "uri-reference"
},
"emailBounceMessage": {
"type": "string"
},
"documentResourceUri": {
"type": "string",
"format": "uri-reference"
}
},
"required": [
"tag",
"tag1",
"tag2",
"uuid",
"email",
"group",
"order",
"status",
"document",
"documentName",
"lastname",
"roleText",
"firstname",
"statusText",
"emailBounce",
"resourceUri",
"rejectReason",
"groupResourceUri",
"emailBounceMessage",
"documentResourceUri"
]
}
},
"required": [
"version",
"object",
"data",
"created",
"id",
"event"
]
}