← Back to blog

How to Integrate E-Signing Into Your Node.js App (Step-by-Step)

April 3, 2026
How to Integrate E-Signing Into Your Node.js App (Step-by-Step)

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

email

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:

  1. Generate the contract. The HR portal creates a personalised PDF using a template engine, filling in the employee name, role, start date, and salary.

  2. 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.

  3. 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.

  4. 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.

  5. 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.