If you are building a Node.js application that handles contracts, consent forms, onboarding documents, or any agreement that requires a signature, integrating an e-signature API directly into your app removes friction from the process entirely. Rather than routing users to a separate platform or asking them to print, sign, and scan, you can deliver a seamless signing experience without them ever leaving your product.
Node.js is particularly well-suited to this kind of integration. Its event-driven, non-blocking architecture handles asynchronous operations naturally: sending documents, waiting for signing events, and receiving real-time webhook notifications all fit cleanly into the Node.js programming model.
In this guide, you will learn how to integrate the Inkless e-signature API into a Node.js application from scratch. We cover everything from project setup and authentication through to sending signing requests, handling webhook callbacks, and downloading completed documents and audit logs - all with accurate, working code based on the real Inkless API documentation.
Why Choose Inkless for Node.js E-Signature Integration?
Before diving into the code, it is worth understanding what makes Inkless a practical choice for developers - particularly those building for UK-based or European customers.
Simple API key authentication
Inkless uses API key authentication: you pass your key as a Bearer token in every request header and you are ready to make calls. There are no OAuth flows to implement, no JWT grants to configure, and no token refresh logic to maintain. This is a meaningful difference from providers like DocuSign, whose JWT grant authentication adds several steps before you can send your first document.
UK data hosting and court-ready audit trails
All documents are stored in the UK, which matters if you are handling data subject to UK GDPR. Inkless complies with UK eIDAS requirements and provides court-ready audit trails: tamper-evident records of every signing event, including timestamps, IP addresses, and signer identity verification. Read more in the Inkless GDPR guide for a full breakdown of how the platform approaches data protection.
Pay-as-you-go pricing
Unlike subscription-based competitors, Inkless charges per document sent, not per user or per month. For applications with variable signing volumes - seasonal hiring spikes, occasional contract renewals, or low-frequency but high-value agreements - this model scales considerably more cleanly. See the Inkless pricing page for a full comparison.
Inkless vs major competitors
|
Feature |
Inkless |
DocuSign |
Adobe Sign |
BoldSign |
|---|---|---|---|---|
|
Pricing model |
Pay-as-you-go |
Subscription |
Subscription |
Subscription |
|
Authentication |
API key (Bearer) |
OAuth (JWT grant) |
OAuth 2.0 |
API key |
|
UK data hosting |
Yes |
US-based |
US-based |
No |
|
UK eIDAS compliance |
Yes |
Partial |
Partial |
No |
|
Court-ready audit trail |
Yes |
Yes |
Yes |
Basic |
|
Webhook support |
Yes |
Yes |
Yes |
Yes |
|
SMS delivery |
Yes (built-in) |
No |
No |
No |
|
Balance/billing API |
Yes (get_balance) |
No |
No |
No |
How the Inkless API Works
Before writing any code, it helps to understand the three key concepts the API is built around:
-
Document templates: Signature layouts are created once in your Inkless dashboard and referenced by their template ID in API calls. You can optionally replace the document content at send time using a base64-encoded file.
-
Envelopes: A single call to POST /api/send_link creates the envelope, assigns recipients, and dispatches signing invitations in one step. The response returns an envelope_token and a document_token used for all follow-up calls.
-
Tokens: All subsequent API calls - status checks, downloads, resending - use the envelope_token or document_token returned by the initial send call. Store these securely in your database immediately after receiving them.
The base URL for all API calls is:
|
https://inkless.co.uk/api |
|
All endpoints use POST Unlike REST APIs that use GET for reads, every Inkless endpoint - including status checks and downloads - uses HTTP POST. Parameters are always passed as a JSON request body, never as URL query strings. |
Prerequisites and Project Setup
Before you begin, you will need:
-
Node.js v16 or later installed
-
An Inkless account - sign up free and retrieve your API key from the developer settings. Your key will be in the format ink_live_key.secret
-
At least one document template and one email template configured in your Inkless dashboard. Note their integer IDs - you will need them in every API call.
-
Basic familiarity with Express and async/await patterns
Initialise a new project and install the required packages:
|
npm init -y npm install axios dotenv express |
Create a .env file in your project root to store credentials. Never commit this file to version control - add it to your .gitignore immediately.
|
# .env INKLESS_API_KEY=ink_live_key.your_secret_here INKLESS_WEBHOOK_SECRET=your_webhook_secret_here PORT=3000
# IDs from your Inkless dashboard INKLESS_EMAIL_TEMPLATE_ID=1 INKLESS_DOC_TEMPLATE_ID=42 |
Create a config module that loads and validates these values at startup:
|
// config.js require('dotenv').config();
const required = ['INKLESS_API_KEY', 'INKLESS_WEBHOOK_SECRET', 'INKLESS_EMAIL_TEMPLATE_ID', 'INKLESS_DOC_TEMPLATE_ID']; required.forEach(key => { if (!process.env[key]) { throw new Error(`Missing required environment variable: ${key}`); } });
module.exports = { apiKey: process.env.INKLESS_API_KEY, webhookSecret: process.env.INKLESS_WEBHOOK_SECRET, baseUrl: 'https://inkless.co.uk/api', emailTemplateId: parseInt(process.env.INKLESS_EMAIL_TEMPLATE_ID, 10), docTemplateId: parseInt(process.env.INKLESS_DOC_TEMPLATE_ID, 10), port: process.env.PORT || 3000, }; |
Create a shared Axios instance that injects your API key on every request:
|
// inkless.js const axios = require('axios'); const { apiKey, baseUrl } = require('./config');
const inkless = axios.create({ baseURL: baseUrl, headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, });
module.exports = inkless; |
|
Security reminder Add .env to your .gitignore before your first commit. API keys in the format ink_live_key.secret are sensitive credentials. Rotate your key from the Inkless dashboard immediately if it is ever exposed in version control or application logs. |
Send a Signature Request
A single POST to /api/send_link creates the envelope and dispatches invitation emails to all recipients. There is no separate "create then send" step - everything happens in one call.
Recipient fields
|
Field |
Type |
Required |
Description |
|---|---|---|---|
|
recipient_number |
integer |
Yes |
Unique identifier within this envelope (1, 2, 3...) |
|
name |
string |
Yes |
Full name of the recipient |
|
|
string |
Yes |
Email address where the invitation is sent |
|
routing_order |
integer |
No |
Signing order when enforce_routing_order is true |
|
read_only |
boolean |
No |
Recipient receives a copy but does not sign |
|
send_link_sms |
boolean |
No |
Also deliver the signing link via SMS |
sendDocument() function
The following sends a document for signature. The document template must already exist in your Inkless dashboard. Optionally pass a local file path to use a dynamically generated PDF instead of the default template content.
|
// inklessService.js const fs = require('fs'); const path = require('path'); const inkless = require('./inkless'); const { emailTemplateId, docTemplateId } = require('./config');
async function sendDocument(recipients, pdfPath = null) { const document = { document_template_id: docTemplateId, recipient_numbers: recipients.map(r => r.recipient_number), };
// Optionally replace the template file with a dynamic PDF if (pdfPath) { document.replacement_file_base64 = fs.readFileSync(pdfPath).toString('base64'); document.replacement_file_name = path.basename(pdfPath); }
const { data } = await inkless.post('/send_link', { email_template_id: emailTemplateId, enforce_routing_order: true, recipients, documents: [document], });
// Store both tokens immediately - needed for all follow-up calls return { envelopeToken: data.envelope_token, documentToken: data.documents[0].document_token, remaining: data.remaining, }; }
module.exports = { sendDocument }; |
Call sendDocument() from your Express route:
|
// routes/sign.js const express = require('express'); const router = express.Router(); const { sendDocument } = require('../inklessService'); const db = require('../db'); // your database module
router.post('/send', async (req, res) => { const { signerName, signerEmail, managerName, managerEmail } = req.body;
const recipients = [ { recipient_number: 1, routing_order: 1, name: signerName, email: signerEmail }, { recipient_number: 2, routing_order: 2, name: managerName, email: managerEmail }, ];
try { const result = await sendDocument( recipients, './contracts/employment-contract.pdf' // optional - omit to use template default );
// Persist tokens - essential for downloads and status checks later await db.signingRequests.create({ envelopeToken: result.envelopeToken, documentToken: result.documentToken, });
res.json({ ok: true, balance: result.remaining });
} catch (err) { if (err.response?.status === 402) { return res.status(402).json({ error: 'Insufficient Inkless balance' }); } res.status(500).json({ error: err.message }); } });
module.exports = router; |
|
Test mode To test your integration without consuming credits or sending real emails, use api@inkless.co.uk as the recipient email address. Inkless returns a realistic mock response so you can verify your code handles tokens, stores them, and triggers webhook processing - all without spending any signing credit. |
API Endpoint Reference
All endpoints are POST and share the same base URL (https://inkless.co.uk/api). Parameters are always sent as a JSON request body.
|
Endpoint |
Method |
Key Parameters |
Purpose |
|---|---|---|---|
|
/send_link |
POST |
email_template_id, recipients, documents |
Create envelope and send signing invitations |
|
/get_envelope_status |
POST |
envelope_token |
Check signing progress per document and recipient |
|
/download_signed |
POST |
document_token |
Download completed signed PDF (base64 response) |
|
/download_archive |
POST |
document_token |
Download court bundle ZIP (signed PDF + audit log) |
|
/download_audit_log |
POST |
document_token |
Download NDJSON audit log of all signing events |
|
/resend_envelope_links |
POST |
envelope_token |
Resend invitations to recipients with pending signatures |
|
/get_balance |
POST |
(none required) |
Check remaining signing credit |
Handle Webhook Events
Rather than polling /api/get_envelope_status, register a webhook endpoint so Inkless notifies your server the moment each event occurs. Key events to handle include document_signed (one recipient has signed), document_complete (all signatures collected on a document), and envelope_complete (all documents in the envelope are fully signed).
Inkless signs each webhook request with HMAC-SHA256. The signature is Base64-encoded and sent in the X-Inkless-Signature header. Always verify it before processing the payload.
|
// webhook.js const crypto = require('crypto'); const { webhookSecret } = require('./config');
function verifySignature(rawBody, signature) { const expected = crypto .createHmac('sha256', webhookSecret) .update(rawBody) .digest('base64'); // Inkless uses Base64, not hex
return crypto.timingSafeEqual( Buffer.from(signature, 'base64'), Buffer.from(expected, 'base64') ); }
module.exports.webhookHandler = (req, res) => { const signature = req.get('X-Inkless-Signature');
if (!verifySignature(req.rawBody, signature)) { return res.status(401).json({ error: 'Invalid signature' }); }
// Acknowledge receipt immediately before any async processing res.status(200).json({ received: true });
const { event, document_token } = req.body;
if (event === 'document_complete') { downloadAndArchive(document_token).catch(console.error); }
if (event === 'envelope_complete') { notifyStakeholders(document_token).catch(console.error); }
if (event === 'document_signed') { updateSigningStatus(document_token).catch(console.error); } }; |
Register the route in your Express app, capturing the raw request body before the JSON parser processes it - otherwise the HMAC calculation will not match:
|
// server.js require('dotenv').config(); const express = require('express'); const { webhookHandler } = require('./webhook'); const signRoutes = require('./routes/sign');
const app = express();
// Capture raw body BEFORE json() middleware for webhook verification app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
app.use('/sign', signRoutes); app.post('/webhooks/inkless', webhookHandler);
app.listen(process.env.PORT, () => { console.log(`Server running on port ${process.env.PORT}`); }); |
|
Respond within 5 seconds Inkless expects a 200 OK response promptly. Always acknowledge receipt first, then process the event asynchronously using setImmediate(), a job queue such as Bull or BullMQ, or a background worker. Slow responses can cause Inkless to retry the webhook, resulting in duplicate processing. |
|
Webhook verification endpoint When you register your webhook URL in the Inkless dashboard, Inkless will send a GET request to your endpoint and expect a plain-text response containing your verification_secret value. Add a separate GET route that returns this string at HTTP 200 to complete registration. |
Download Completed Documents and Audit Logs
Once the document_complete event fires, retrieve the signed PDF and audit trail using the document_token stored from the original send call. Inkless returns files as Base64-encoded strings in the response body - decode them before writing to disk.
Inkless provides three download options, each using the document_token:
-
POST /api/download_signed - the completed signed PDF
-
POST /api/download_archive - a court bundle ZIP containing the signed PDF and all supporting documentation
-
POST /api/download_audit_log - the tamper-evident audit log in NDJSON format, recording every signing event with timestamps
|
// download.js const fs = require('fs'); const path = require('path'); const inkless = require('./inkless');
async function downloadSigned(documentToken) { const { data } = await inkless.post('/download_signed', { document_token: documentToken, });
// Inkless returns the file nested as data.file.content_base64 const pdfBytes = Buffer.from(data.file.content_base64, 'base64');
const dir = './signed-documents'; if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const filePath = path.join(dir, `${documentToken}.pdf`); fs.writeFileSync(filePath, pdfBytes); return filePath; }
async function downloadArchive(documentToken) { const { data } = await inkless.post('/download_archive', { document_token: documentToken, });
const zipBytes = Buffer.from(data.file.content_base64, 'base64'); const filePath = `./signed-documents/${documentToken}-archive.zip`; fs.writeFileSync(filePath, zipBytes); return filePath; }
async function downloadAuditLog(documentToken) { const { data } = await inkless.post('/download_audit_log', { document_token: documentToken, });
const logBytes = Buffer.from(data.file.content_base64, 'base64'); const filePath = `./signed-documents/${documentToken}-audit.ndjson`; fs.writeFileSync(filePath, logBytes); return filePath; }
async function downloadAndArchive(documentToken) { const [pdfPath, archivePath] = await Promise.all([ downloadSigned(documentToken), downloadArchive(documentToken), ]); console.log('Signed PDF:', pdfPath); console.log('Archive:', archivePath); }
module.exports = { downloadSigned, downloadArchive, downloadAuditLog, downloadAndArchive }; |
|
Data minimisation and retention UK GDPR requires that personal data is not retained longer than necessary. When you download and store completed documents, document your legal basis and retention schedule. Delete or anonymise records once the retention period expires. The Inkless audit log captures signer IP addresses and timestamps, so treat it as personal data under your privacy notice. |
Check Envelope Status
If you need to query the current state of a signing request - for example, to display progress in a dashboard - use POST /api/get_envelope_status with the envelope_token. The response includes completion status per document, per-recipient timestamps, and whether each party has viewed, signed, or has a link pending expiry.
|
async function getEnvelopeStatus(envelopeToken) { const { data } = await inkless.post('/get_envelope_status', { envelope_token: envelopeToken, }); return data; // { envelope, documents, recipients } }
// Example Express route app.get('/sign/status/:envelopeToken', async (req, res) => { try { const status = await getEnvelopeStatus(req.params.envelopeToken); res.json({ envelope: status.envelope, documents: status.documents, recipients: status.recipients, }); } catch (err) { res.status(404).json({ error: 'Not found' }); } }); |
Advanced Features and Best Practices
The Inkless API supports several features beyond the basic send-and-sign flow. See the Inkless developer documentation for the full reference.
-
Multiple documents per envelope: Pass multiple objects in the documents array to bundle several documents into a single signing workflow.
-
Sequential vs parallel signing: Set enforce_routing_order: true and assign different routing_order values for sequential signing. Recipients with the same routing_order value sign in parallel.
-
SMS delivery: Set send_link_sms: true on a recipient to deliver their signing link via SMS as well as email.
-
Resending invitations: Call POST /api/resend_envelope_links with the envelope_token to resend invitations to all recipients with pending signatures.
-
Balance monitoring: Call POST /api/get_balance to check your remaining credit before sending, or monitor the remaining field in every send_link response.
|
async function resendLinks(envelopeToken) { const { data } = await inkless.post('/resend_envelope_links', { envelope_token: envelopeToken, }); return data; // { ok, envelope_token, status, resent, notified_email } }
async function getBalance() { const { data } = await inkless.post('/get_balance'); return data; // { ok, company_id, balance, as_of } } |
On the technical side:
-
Always store envelope_token and document_token in your database immediately after a successful send - they are the only way to retrieve, download, or check the status of an envelope later
-
Handle 402 responses (insufficient funds) explicitly with an alert to whoever manages your Inkless account balance
-
Use a job queue for webhook-triggered downloads so your endpoint returns 200 OK within the required window
-
Validate recipient data before sending - a 422 response includes a detailed errors array identifying which fields failed
Error Handling, Testing, and Security Best Practices
Structured error handling
Wrap every API call in a try-catch block and distinguish between client errors (4xx) and service errors (5xx). Return meaningful messages to your application layer without exposing internal details.
|
async function safeSendDocument(recipients, pdfPath) { try { return await sendDocument(recipients, pdfPath); } catch (err) { if (err.response) { const { status, data } = err.response; if (status === 400) throw new Error('Invalid request: ' + data.error); if (status === 401) throw new Error('API key invalid or missing'); if (status === 402) throw new Error('Insufficient Inkless balance'); if (status === 422) throw new Error('Validation failed: ' + JSON.stringify(data.errors)); } throw err; } } |
Testing with Jest and nock
Use the nock library to mock Inkless HTTP calls in your test suite. This avoids hitting live endpoints and consuming credits during automated tests.
|
const nock = require('nock'); const { sendDocument } = require('./inklessService');
describe('sendDocument', () => { it('returns tokens on success', async () => { nock('https://inkless.co.uk') .post('/api/send_link') .reply(200, { envelope_token: 'env_abc123', documents: [{ document_token: 'doc_xyz456', }], remaining: 42, });
const result = await sendDocument([ { recipient_number: 1, routing_order: 1, name: 'Alice', email: 'api@inkless.co.uk' }, ]);
expect(result.envelopeToken).toBe('env_abc123'); expect(result.documentToken).toBe('doc_xyz456'); expect(result.remaining).toBe(42); }); }); |
Security checklist
-
Store API keys in environment variables only - never hardcode them or commit them to source control
-
Always verify webhook signatures using HMAC-SHA256 with Base64 digest before processing any payload
-
Use HTTPS for your webhook endpoint
-
Implement idempotency in your webhook handler - the same event may be delivered more than once
-
Handle 402 responses with an alert to whoever manages your Inkless balance
-
Validate file types and sizes server-side before sending documents to the API
Real-World Example: Employee Onboarding in a Node.js HR Portal
Consider a UK-based SME that has built an internal HR portal in Node.js. When a new employee is hired, the system needs to send an employment contract to both the new hire and an HR manager, in that order, and archive the completed document in their records system.
Here is how that workflow looks with the Inkless integration:
-
Generate the contract. The HR portal creates a personalised PDF using a template engine, filling in the employee name, role, start date, and salary.
-
Send for signature. The portal calls sendDocument() with the new hire as recipient_number 1 (routing_order 1) and the HR manager as recipient_number 2 (routing_order 2). With enforce_routing_order: true, the manager only receives their invitation once the employee has signed.
-
Store tokens. Both the envelope_token and document_token returned by the API are saved to the database immediately - these are the only keys to all future calls.
-
Receive webhook. When the HR manager completes their signature, the envelope_complete event fires. The webhook handler calls downloadAndArchive() with the document_token and writes the signed PDF to the employee record.
-
Audit trail retained. The full audit log is downloaded alongside the signed PDF - timestamps, IP addresses, and authentication events - satisfying employment law record-keeping requirements.
Because Inkless charges per document rather than per user, the HR portal pays only for actual signings - not for a fixed monthly seat allocation that sits unused between hiring rounds. See the Inkless features page for more on template and workflow capabilities.
Compliance Considerations for UK Developers
Any e-signature integration that involves UK residents automatically engages UK GDPR and, for most legal contracts, the Electronic Communications Act 2000 and the UK version of eIDAS. For a full treatment, see the Inkless guide to GDPR-compliant e-signatures.
You are the data controller
When your application collects a signer's name, email address, IP address, and timestamps as part of the signing process, your organisation becomes a data controller under UK GDPR. You must have a lawful basis for that processing - typically legitimate interests or contract performance - and you must provide signers with clear privacy information before they sign.
UK eIDAS and signature tiers
UK eIDAS recognises three tiers of electronic signature: simple, advanced, and qualified. The vast majority of everyday contracts - NDAs, service agreements, employment contracts, consent forms - are valid with a simple electronic signature, which is what Inkless provides by default. Qualified signatures, requiring in-person identity verification and a hardware token, are only needed for a narrow set of transactions such as certain land registry filings. For more detail, see the UK government guidance on electronic signatures.
Deeds and witnessed signatures
Deeds require a witness, and witnessing has specific rules in a digital context. Inkless supports witnessed electronic signatures within the requirements of the Electronic Communications Act 2000. See the Inkless guide to witnessing deeds electronically for the detailed requirements.
Data minimisation and retention
Only collect the personal data you genuinely need. If your signing flow captures IP addresses and device data as part of the audit trail, disclose this in your privacy notice. Set retention schedules for completed documents and delete them when the purpose for which they were collected has been fulfilled.
|
Compliance note Do not treat Inkless's compliance certifications as a substitute for your own data protection obligations. Inkless acts as a data processor; your organisation remains the controller responsible for notifying signers, maintaining records of processing activities, and handling subject access requests. |
Frequently Asked Questions
What is a Node.js e-signature API?
A Node.js e-signature API is a REST API that you call from a Node.js application to create, send, and manage electronic signing requests programmatically. Rather than using a standalone signing platform, you embed the signing workflow directly into your application, maintaining full control over the user experience.
Do I need an official Node.js SDK to use the Inkless API?
No. The Inkless API is a standard REST API, so you can call it using any HTTP client such as Axios or Node's built-in fetch. The Axios-based integration shown in this guide works fully today.
How do I test without sending real emails or spending credits?
Use api@inkless.co.uk as the recipient email address. Inkless returns a realistic mock response in test mode, so you can verify that your code correctly handles tokens, stores them, and triggers webhook processing - all without consuming any signing credit.
Can multiple people sign the same document?
Yes. Add multiple objects to the recipients array, each with a unique recipient_number. Set enforce_routing_order: true and assign different routing_order values for sequential signing - each person is only notified once the previous signer has completed. Assign the same routing_order to multiple recipients for parallel signing.
What happens if an envelope_token or document_token is lost?
There is no API endpoint to retrieve a token after the fact, so it is essential to store both tokens in your database immediately after a successful POST /api/send_link response. If a token is genuinely lost, contact Inkless support - do not attempt to re-send the document and create a duplicate envelope.
How does billing work?
Inkless charges per document sent, not per envelope or per user. The send_link response always shows documents_billed, spent, and remaining so you can track usage in real time. Handle 402 responses explicitly to alert whoever manages your Inkless balance before credit runs out.
How should I handle API rate limits?
Implement exponential backoff when you receive a 429 response. For high-volume use cases, queue signing requests using a job library such as Bull or BullMQ and process them at a controlled rate.
Conclusion
Integrating e-signature functionality into a Node.js application with Inkless is straightforward: a single API key, one POST call to send a document, a webhook handler to receive completion events, and token-based calls to download the signed PDF and audit log. There are no OAuth flows, no complex SDK trees, and no subscription commitments.
For UK developers in particular, the combination of UK data hosting, built-in eIDAS compliance, court-ready audit trails, and pay-as-you-go pricing makes Inkless a practical choice that scales with your product without locking you into a fixed monthly allocation.
The code in this guide is based on the Inkless API documentation. Always refer to the developer docs for the latest endpoint details.
Sign up for a free Inkless account to retrieve your API key and start testing with the sandbox email address, or contact the Inkless team if you have questions about a specific integration scenario.