Polling the Inkless API every few seconds to check whether a document has been signed is wasteful, fragile, and hard to scale. Your server burns requests checking for changes that may not have happened yet, and there is always a gap between when something occurs and when you find out about it. Webhooks solve this problem cleanly: instead of your server repeatedly asking Inkless "has anything changed?", Inkless tells your server the moment something happens.
This guide explains how Inkless webhooks work, walks through the full list of available events, and shows you how to build a secure, production-ready webhook receiver. By the end, you will be able to automate everything from sending notifications when a recipient signs, to triggering downstream workflows the moment an envelope is fully completed.
What Is a Webhook?
A webhook is an HTTP callback - a POST request that Inkless sends to a URL you specify whenever a subscribed event occurs. Unlike a REST API where you initiate every request, webhooks are server-to-server pushes initiated by Inkless. Your endpoint simply receives the payload, processes it, and responds with a 200 OK.
Webhook deliveries from Inkless are queued and sent asynchronously. This means they are reliable and do not block the signing flow, but you should not expect them to arrive in strict chronological order under all network conditions. Design your handler to be idempotent - processing the same event twice should not cause side effects.
Why Webhooks Are the Right Approach for Document Status
The alternative to webhooks is polling: calling POST /api/get_envelope_status on a timer and comparing the result. There are a few problems with that approach in practice.
Polling introduces latency proportional to your interval. If you check every 30 seconds and a signer completes at the second one, your system does not know for nearly half a minute. Webhooks deliver the update within seconds of the event firing.
Polling also consumes API quota unnecessarily. If you have 500 envelopes in flight and you check each one every minute, that is 500 requests per minute just to find out that nothing has changed. With webhooks, Inkless only calls your endpoint when something actually happens.
Finally, polling requires you to store and compare state on your side to detect changes. With webhooks, the event type tells you exactly what happened.
How Inkless Webhooks Work
When a subscribed event occurs, Inkless POSTs a JSON payload to your configured endpoint. The payload includes the event name, the relevant tokens, and a timestamp. All webhook requests are signed with HMAC-SHA256 so your endpoint can verify the request genuinely came from Inkless and that the body has not been tampered with in transit.
Here is an example payload for a document_signed event:
|
{ "event": "document_signed", "secure_link_id": 271, "document_token": "a1b2c3d4...", "timestamp": "2026-03-11T23:25:31Z"} |
The payload fields are consistent across all events:
|
Field |
Type |
Description |
|
event |
string |
The event name, e.g. document_signed. |
|
secure_link_id |
integer |
The secure link that triggered the event (where applicable). |
|
document_token |
string |
Token for the affected document. Use this to call download_signed or download_archive. |
|
timestamp |
string |
ISO 8601 timestamp of when the event fired. |
You configure your webhook URL in Company Settings inside the Inkless dashboard. Inkless only delivers webhooks to HTTPS endpoints - plain HTTP endpoints will be rejected.
Available Webhook Events
Inkless fires webhooks across the full lifecycle of a document - from the moment a signing link is sent, through signing, document processing, and envelope completion, to email and SMS delivery events. The table below lists every available event.
|
Event |
When It Fires |
|
document_signing_link_sent |
A signing link is dispatched to a recipient. |
|
link_viewed |
A recipient opens a signing link. |
|
link_resent |
A signing link is resent. |
|
otp_sent |
An OTP SMS is sent for a signing session. |
|
otp_verified |
An OTP is successfully verified. |
|
document_signed |
A recipient signs a document. Fires per signer. |
|
document_complete |
The document has finished processing and the archive is ready to download. |
|
envelope_complete |
All documents in the envelope are complete. Use this as your final trigger. |
|
sms_sent |
An SMS message is accepted by the provider. |
|
sms_delivered |
An SMS message is reported as delivered. |
|
sms_failed |
An SMS message fails delivery. |
|
email_delivered |
An email is reported as delivered. |
|
email_opened |
A recipient opens an email. |
|
email_clicked |
A recipient clicks a link in an email. |
|
email_bounced |
An email bounces. |
|
email_rejected |
An email is rejected by the receiving server. |
For most automation use cases, the three events you will care about most are document_signed (a single signer has completed their part), document_complete (the document has finished processing and is ready to download), and envelope_complete (every document in the envelope is fully signed and processed). Use envelope_complete as your final trigger for downstream workflows.
Setting Up Your Webhook Endpoint
Before you can receive events, Inkless runs a verification handshake to confirm your endpoint is reachable and under your control. When you save a webhook URL in Company Settings, Inkless POSTs a JSON payload containing a verification_secret. Your endpoint must respond with that exact token as plain text with an HTTP 200 status.
The verification request looks like this:
|
{ "verification_secret": "abc123...", "provider": "inkless", "webhook_id": 42, "timestamp": "2026-03-11T23:25:31Z"} |
Here is a minimal Node.js/Express endpoint that handles verification and is ready to receive live events:
|
const express = require('express');const crypto = require('crypto');const app = express();const WEBHOOK_SECRET = process.env.INKLESS_WEBHOOK_SECRET;// Use express.text() to preserve the raw body for signature verificationapp.post('/webhook', express.text({ type: '*/*' }), (req, res) => { const rawBody = req.body; // Step 1: Handle the verification handshake try { const parsed = JSON.parse(rawBody); if (parsed.verification_secret) { return res.status(200).send(parsed.verification_secret); } } catch (_) {} // Step 2: Verify the HMAC signature const signature = req.headers['x-inkless-signature']; if (!signature || !verifySignature(rawBody, signature, WEBHOOK_SECRET)) { return res.status(401).send('Invalid signature'); } // Step 3: Process the event const event = JSON.parse(rawBody); handleEvent(event); // Always respond 200 quickly - do heavy work asynchronously res.status(200).send('ok');});function verifySignature(rawBody, signature, secret) { const computed = crypto .createHmac('sha256', secret) .update(rawBody) .digest('base64'); try { return crypto.timingSafeEqual( Buffer.from(computed), Buffer.from(signature) ); } catch (_) { return false; }}async function handleEvent(event) { switch (event.event) { case 'document_signed': console.log(`Signer ${event.secure_link_id} signed ${event.document_token}`); break; case 'document_complete': await downloadSignedDocument(event.document_token); break; case 'envelope_complete': await triggerDownstreamWorkflow(event.document_token); break; default: console.log(`Received event: ${event.event}`); }}app.listen(3000); |
|
Respond fast, process async Inkless expects a 200 response promptly. If your event handler performs slow operations - database writes, external API calls, sending emails - offload them to a background queue or run them with setImmediate/process.nextTick. A slow response risks a timeout and a retry. |
Verifying Webhook Signatures
Every live webhook request from Inkless includes two headers: X-Inkless-Signature (a base64-encoded HMAC-SHA256 signature of the raw request body) and X-Inkless-Alg (currently always SHA256). Your endpoint should verify every incoming request before processing it.
The webhook secret itself is never sent in the request. Inkless stores it when you configure the webhook and uses it to compute the signature. Your side does the same computation and compares the results.
The verification steps are:
-
Read the exact raw request body bytes - do not parse and re-serialise the JSON, as this can alter the byte sequence and break the comparison.
-
Compute HMAC-SHA256 of that raw body using your stored webhook secret.
-
Base64-encode the result.
-
Compare it to the X-Inkless-Signature header value using a timing-safe comparison to prevent timing attacks.
The verifySignature function in the example above implements exactly this pattern. If the signatures do not match, return 401 and do not process the event.
|
Use timing-safe comparison Always use crypto.timingSafeEqual() (Node.js) or equivalent in your language rather than a plain string comparison. Standard equality checks short-circuit on the first mismatched byte, which can leak information about how much of the signature is correct. |
Practical Automation Patterns
With signature verification in place, here are the most common patterns for each key event.
Auto-downloading signed documents on document_complete
The document_complete event fires when the document has finished processing and the signed PDF is ready. This is the correct time to call POST /api/download_signed using the document_token from the payload. Do not attempt to download before this event fires - the document may still be processing.
|
async function downloadSignedDocument(documentToken) { const response = await inkless.post('/download_signed', { document_token: documentToken, }); if (response.data.ok) { const pdfBuffer = Buffer.from( response.data.file.content_base64, 'base64' ); // Save to disk, upload to S3, store in your database, etc. fs.writeFileSync(`./signed/${response.data.file.filename}`, pdfBuffer); console.log(`Saved ${response.data.file.filename}`); }} |
Triggering workflows on envelope_complete
The envelope_complete event is your all-clear signal: every document in the envelope is signed and processed. This is the right place to trigger downstream actions - updating a CRM record, sending a completion notification, unlocking the next stage in an onboarding flow, or archiving the full envelope using POST /api/download_envelope_archive.
|
case 'envelope_complete': // Notify your team await sendSlackNotification( `Envelope ${event.document_token} is fully signed.` ); // Update your database record await db.envelopes.update( { document_token: event.document_token }, { status: 'complete', completed_at: event.timestamp } ); break; |
Tracking signer progress on document_signed
For multi-party envelopes with a signing order, document_signed fires once per signer. You can use this event to notify other parties that it is their turn, log progress in your database, or send a 'thank you' confirmation to the signer who just completed.
The secure_link_id in the payload identifies which recipient signed. Cross-reference it against the secure_link_id values returned when you originally called POST /api/send_link to identify the specific signer.
Testing Your Webhook Locally
Webhook endpoints need to be reachable over the internet, which rules out localhost during development. The standard approach is to use a tunnelling tool such as ngrok to expose your local port to a public HTTPS URL. Start your local server and then run:
|
ngrok http 3000 |
ngrok will give you a forwarding URL like https://abc123.ngrok.io. Use that as your webhook URL in Company Settings for development and testing.
Once your endpoint is live, you can also use Inkless test mode to verify your integration without sending real documents. If any recipient email in a /api/send_link request is set to api@inkless.co.uk, the API returns a mock success response without creating a real envelope or billing your account. This lets you verify that your code handles the response correctly before going live.
|
Handle retries gracefully If your endpoint returns anything other than a 2xx response, Inkless will retry the delivery. Make your handler idempotent by checking whether you have already processed a given event before acting on it - store the document_token and event pair in your database and skip duplicates. |
Putting It All Together
With webhooks wired up, your integration moves from a request-driven model to a fully event-driven one. Inkless does the work of detecting state changes and notifying your system - you just need a secure endpoint, signature verification, and handlers for the events you care about.
To get started, log in to your Inkless account, navigate to Company Settings, and add your HTTPS webhook URL. For the full developer reference including API endpoints, request schemas, and error codes, see the Inkless API documentation including webhooks.