Receive webhooks
On this page
API docs
Get started
Endpoints
Account
Templates
Sending
Documents
Files
OpenAPI
Guides
Receive webhooks
A webhook is a request Inkly sends to your server when something happens to a document, such as a signer finishing. It saves you asking the API over and over whether anything has changed.
This guide sets up an endpoint step by step. The webhooks reference lists every event, payload field, and retry.
Add an endpoint
Click Add endpoint on API settings
Open API settings. Only the workspace's primary owner or a Teams admin can add endpoints, and a workspace can have up to 5.
Enter your URL and choose events
The URL must use HTTPS on the standard port, with a hostname rather than an IP address. Choose only the events you act on. They are
document.sent,recipient.viewed,recipient.signed,recipient.declined,document.completed, anddocument.voided.Copy the signing secret
It starts with
whsec_and Inkly shows it once. Keep it with your server's other secrets, in an environment variable calledINKLY_WEBHOOK_SECRET. Your server uses it to check that each request really came from Inkly.
Verify each delivery
Anyone can send a request to your URL, so check every one before acting on it. Inkly signs each delivery, and this function accepts it only if the signature matches and the delivery is under 5 minutes old, which stops an old request being replayed.
import { createHmac, timingSafeEqual } from "node:crypto"
const MAX_AGE_SECONDS = 5 * 60
// Pass the raw request body as a string, before any JSON parsing.
export function verifyInklyWebhook(rawBody, headers, secret) {
const id = headers["webhook-id"]
const timestamp = headers["webhook-timestamp"]
const signatures = headers["webhook-signature"]
if (!id || !timestamp || !signatures) return false
const age = Math.abs(Date.now() / 1000 - Number(timestamp))
if (!(age <= MAX_AGE_SECONDS)) return false
const key = Buffer.from(secret.slice("whsec_".length), "base64")
const expected = Buffer.from(
"v1," + createHmac("sha256", key).update(id + "." + timestamp + "." + rawBody).digest("base64"),
)
// During a secret roll the header carries two signatures separated by a space.
return signatures.split(" ").some((signature) => {
const candidate = Buffer.from(signature)
return candidate.length === expected.length && timingSafeEqual(candidate, expected)
})
}Inkly uses the Standard Webhooks format, so a library for that format works too. The reference describes the signature format if you write your own.
A complete endpoint
A whole endpoint reads the raw body, verifies it, skips events it has already handled, acts on the ones it cares about, and answers 200:
import express from "express"
import { verifyInklyWebhook } from "./verify-inkly-webhook.js"
const app = express()
// express.raw keeps the body exactly as it arrived, which the signature needs.
app.post("/webhooks/inkly", express.raw({ type: "application/json" }), async (req, res) => {
const rawBody = req.body.toString("utf8")
if (!verifyInklyWebhook(rawBody, req.headers, process.env.INKLY_WEBHOOK_SECRET)) {
return res.sendStatus(400)
}
const event = JSON.parse(rawBody)
// Your own store of event IDs you have handled, because a delivery can repeat.
if (await alreadyHandled(event.id)) return res.sendStatus(200)
switch (event.type) {
case "recipient.signed":
await markSignerDone(event.document.id, event.recipient.id)
break
case "document.completed":
await downloadSignedPdf(event.document.id)
break
}
await rememberHandled(event.id)
res.sendStatus(200)
})
app.listen(3000)alreadyHandled and rememberHandled stand for your own store of event IDs, such as a database table with a unique column. On document.completed the signed PDF is already saved, so you can download it straight away.
Test it
Deploy the endpoint, open it on API settings, and click Send test event. A ping event arrives with no document, and the endpoint's delivery log shows whether your server answered with a 2xx.
Then send a real document to your own email, as in the quickstart, and sign it. You should get document.sent, recipient.viewed, recipient.signed, and document.completed, if you chose those events.
Answer quickly
Answer with any 2xx within 10 seconds. A slower answer, an error, or a redirect counts as a failure, and Inkly tries again later on a schedule that lasts about 3 days. An endpoint that keeps failing is turned off, and the people who manage keys get an email.
If your work takes longer, such as downloading the signed PDF and saving it elsewhere, put it on a queue and answer first.
When your server was down, open the endpoint on API settings. Its delivery log covers 30 days, and Resend sends a failed delivery again.
Duplicates and order
The same event can arrive more than once, for example when your answer was lost on the way back. Save the id of every event you handle, and skip any you have seen. It stays the same on every retry, and arrives in the webhook-id header too.
Events can also arrive out of order, so recipient.signed may come after document.completed. Do not move a record backwards because of an older event. When order matters, get the document and act on its current status.
Roll a secret
Roll the secret if it may have leaked, such as when someone who knew it leaves. Nothing is lost while you switch:
Click Roll secret on the endpoint
Copy the new secret. Inkly shows it once.
Update your server within 24 hours
For 24 hours Inkly signs each delivery with both the old and the new secret, so your server keeps accepting deliveries while you deploy the new one. The examples above already check both signatures.