Introduction
The Inkless API lets you programmatically create and send signing links (“envelopes”) based on your email templates and document templates, with signing order, merge fields, auditing, and expiry. To get started you must create an API Token, to do this you can browse to Admin -> Company then click on the API Details Tab, Once their type a description in the input box and click "Create Token". Copy the token and store it in a secure place as this will not be shown again and will require you to revoke the token and create a new one if lost.
email_template_id and all document_template_id values must belong to the same company as the token.
API Endpoints
Use the same base path for all API requests. Each action is called by posting to its route under /api.
https://dev.inkless.co.uk/api
Example routes:
/api/send_link,
/api/get_documents,
/api/get_emails,
/api/download_signed,
/api/download_audit_log
Authentication
To access the API, you must include your API token in the Authorization header:
Header
Authorization: Bearer YOUR_API_TOKEN
API Token Format
The API token must follow the format:
ink_live_key.secret
keyis a unique identifier for the key (8-16 alphanumeric characters)secretis the secret key (20+ alphanumeric characters)
Authentication Flow
When you send a request with the API token in the Authorization header, the following steps occur:
- The token is extracted and validated against the expected format.
- If the token does not match the expected format, a 401 Unauthorized error is returned.
- The system checks if the token is valid and not revoked. If the token is revoked or invalid, a 401 Unauthorized error is returned.
- The system checks if the token has expired. If the token is expired, a 401 Unauthorized error is returned.
- The system verifies that the provided HMAC matches the stored HMAC. If it does not match, a 401 Unauthorized error is returned.
- If all checks pass, the request proceeds and the token data is returned for use in subsequent operations.
Response Codes
The following error codes may be returned during the authentication process:
Rate Limiting
Each API token is subject to rate limiting based on the following parameters:
- Limit: The maximum number of requests allowed within a specified time window.
- Window: The time period within which requests are counted.
If the rate limit is exceeded, a 429 Too Many Requests error will be returned. The rate_limit_limit and rate_limit_window values are provided as part of the token data.
Last Used IP
The IP address associated with the API token is updated each time the token is used. The system tracks this and logs the IP in case of misuse or troubleshooting.
Get Balance
/api/get_balance
Each document sent (after being combined) will deduct from your overall balance.
For example: If 1 envelope with 2 documents priced at £0.70 is sent, it will cost £1.40. If the payload has a combine: 1 for both documents, they will be combined into one document, costing you only £0.70.
Response Codes
Response Example
{
"ok":true,
"company_id":127,
"balance":"£0.50",
"as_of":"2025-10-13T15:51:23+00:00"
}
Errors
If the company does not exist or the API token is invalid, a 404 Not Found error will be returned with the message "company not found".
How to Request the Balance
To retrieve the balance for a company, send a POST request to /api/get_balance with the API token in the Authorization header. The request will return the company's wallet balance and the date and time the balance was last updated.
curl -sS -X POST "https://dev.inkless.co.uk/api/get_balance" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json"
$h = @{ Authorization = "Bearer YOUR_API_TOKEN" }
Invoke-RestMethod -Method Post -Uri "https://dev.inkless.co.uk/api/get_balance" `
-Headers $h -ContentType "application/json"
const res = await fetch("https://dev.inkless.co.uk/api/get_balance", {
method:"POST",
headers:{Authorization:"Bearer YOUR_API_TOKEN","Content-Type":"application/json"}
});
console.log(await res.json());
<?php
$ch = curl_init("https://dev.inkless.co.uk/api/get_balance");
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer YOUR_API_TOKEN","Content-Type":"application/json"],
]);
echo curl_exec($ch); curl_close($ch);
import requests, json
r = requests.post("https://dev.inkless.co.uk/api/get_balance",
headers={"Authorization":"Bearer YOUR_API_TOKEN","Content-Type":"application/json"})
print(r.json())
Limitations
The endpoint currently does not allow you to directly modify your balance. It only provides the current balance associated with the authenticated company.
Get Document Templates
/api/get_documents
This endpoint retrieves the document templates associated with your company. It returns a list of available templates for your account. If you're working in the sandbox environment, predefined templates will be returned.
Response Codes
Response Example
{
"ok":true,
"documents":[
{
"id":1,
"name":"template 1"
},
{
"id":2,
"name":"template 2"
}
]
}
How to Request Document Templates
To retrieve document templates, send a POST request to /api/get_documents with your API token in the Authorization header. If you're working in the sandbox environment, you will receive predefined templates.
curl -sS -X POST "https://dev.inkless.co.uk/api/get_documents" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json"
$h = @{ Authorization = "Bearer YOUR_API_TOKEN" }
Invoke-RestMethod -Method Post -Uri "https://dev.inkless.co.uk/api/get_documents" `
-Headers $h -ContentType "application/json"
const res = await fetch("https://dev.inkless.co.uk/api/get_documents", {
method:"POST",
headers:{Authorization:"Bearer YOUR_API_TOKEN","Content-Type":"application/json"}
});
console.log(await res.json());
<?php
$ch = curl_init("https://dev.inkless.co.uk/api/get_documents");
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer YOUR_API_TOKEN","Content-Type":"application/json"],
]);
echo curl_exec($ch); curl_close($ch);
import requests, json
r = requests.post("https://dev.inkless.co.uk/api/get_documents",
headers={"Authorization":"Bearer YOUR_API_TOKEN","Content-Type":"application/json"})
print(r.json())
Get Email Templates
/api/get_emails
This endpoint retrieves the email templates associated with your company. It returns a list of available email templates for your account. If you're working in the sandbox environment, predefined templates will be returned.
Response Codes
Response Example
{
"ok":true,
"templates":[
{
"id":1,
"name":"document 1"
},
{
"id":2,
"name":"document 2"
}
]
}
How to Request Email Templates
To retrieve email templates, send a POST request to /api/get_emails with your API token in the Authorization header. If you're working in the sandbox environment, you will receive predefined templates.
curl -sS -X POST "https://dev.inkless.co.uk/api/get_emails" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json"
$h = @{ Authorization = "Bearer YOUR_API_TOKEN" }
Invoke-RestMethod -Method Post -Uri "https://dev.inkless.co.uk/api/get_emails" `
-Headers $h -ContentType "application/json"
const res = await fetch("https://dev.inkless.co.uk/api/get_emails", {
method:"POST",
headers:{Authorization:"Bearer YOUR_API_TOKEN","Content-Type":"application/json"}
});
console.log(await res.json());
<?php
$ch = curl_init("https://dev.inkless.co.uk/api/get_emails");
curl_setopt_array($ch, [
CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer YOUR_API_TOKEN","Content-Type":"application/json"],
]);
echo curl_exec($ch); curl_close($ch);
import requests, json
r = requests.post("https://dev.inkless.co.uk/api/get_emails",
headers={"Authorization":"Bearer YOUR_API_TOKEN","Content-Type":"application/json"})
print(r.json())
Send Secure Links (Envelope)
/api/send_link
This endpoint creates an envelope, prepares document outputs for the referenced recipients, creates secure signing links, sends the first routing group immediately, and returns the created recipient/document records.
Authentication uses the Authorization: Bearer ... header. The request body must be JSON.
Short-lived API tokens are supported on this endpoint. If you send a short-lived bearer token, the API rotates it on successful use and returns the replacement token in the X-New-Api-Token response header. Clients using short-lived tokens must store and use the latest returned token on the next request.
API test mode: if any recipient email in the request is api@inkless.co.uk, the endpoint returns a predefined mock success response and does not create a real envelope, document, or send.
curl -sS -X POST "https://dev.inkless.co.uk/api/send_link" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email_template_id": 21,
"client_reference_id": "REF-1001",
"email_reminders": true,
"email_completed_pdf": false,
"enforce_routing_order": true,
"require_us_esign_consent": false,
"recipients": [
{
"recipient_number": 1,
"routing_order": 1,
"name": "Joe Blogs",
"email": "joe@example.com",
"phone": "447841939203",
"send_otp_sms": false,
"send_link_sms": false,
"read_only": false
},
{
"recipient_number": 2,
"routing_order": 2,
"name": "Manager",
"email": "manager@example.com",
"send_otp_sms": true,
"send_link_sms": true,
"read_only": false
}
],
"documents": [
{
"document_template_id": 5,
"recipient_numbers": [1, 2],
"merge": {
"first_name": "Joe",
"start_date": "2026-03-11"
}
}
]
}'
$body = @{
email_template_id = 21
client_reference_id = "REF-1001"
email_reminders = $true
email_completed_pdf = $false
enforce_routing_order = $true
require_us_esign_consent = $false
recipients = @(
@{
recipient_number = 1
routing_order = 1
name = "Joe Blogs"
email = "joe@example.com"
phone = "447841939203"
send_otp_sms = $false
send_link_sms = $false
read_only = $false
},
@{
recipient_number = 2
routing_order = 2
name = "Manager"
email = "manager@example.com"
send_otp_sms = $true
send_link_sms = $true
read_only = $false
}
)
documents = @(@{
document_template_id = 5
recipient_numbers = @(1, 2)
merge = @{ first_name = "Joe"; start_date = "2026-03-11" }
})
} | ConvertTo-Json -Depth 10 -Compress
Invoke-RestMethod -Method POST -Uri "https://dev.inkless.co.uk/api/send_link" `
-Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } `
-ContentType "application/json" -Body $body
const res = await fetch("https://dev.inkless.co.uk/api/send_link", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
email_template_id: 21,
client_reference_id: "REF-1001",
email_reminders: true,
email_completed_pdf: false,
enforce_routing_order: true,
require_us_esign_consent: false,
recipients: [
{
recipient_number: 1,
routing_order: 1,
name: "Joe Blogs",
email: "joe@example.com",
phone: "447841939203",
send_otp_sms: false,
send_link_sms: false,
read_only: false
},
{
recipient_number: 2,
routing_order: 2,
name: "Manager",
email: "manager@example.com",
send_otp_sms: true,
send_link_sms: true,
read_only: false
}
],
documents: [{
document_template_id: 5,
recipient_numbers: [1, 2],
merge: { first_name: "Joe", start_date: "2026-03-11" }
}]
})
});
console.log(await res.json());
<?php
$payload = [
"email_template_id" => 21,
"client_reference_id" => "REF-1001",
"email_reminders" => true,
"email_completed_pdf" => false,
"enforce_routing_order" => true,
"require_us_esign_consent" => false,
"recipients" => [
[
"recipient_number" => 1,
"routing_order" => 1,
"name" => "Joe Blogs",
"email" => "joe@example.com",
"phone" => "447841939203",
"send_otp_sms" => false,
"send_link_sms" => false,
"read_only" => false
],
[
"recipient_number" => 2,
"routing_order" => 2,
"name" => "Manager",
"email" => "manager@example.com",
"send_otp_sms" => true,
"send_link_sms" => true,
"read_only" => false
]
],
"documents" => [[
"document_template_id" => 5,
"recipient_numbers" => [1, 2],
"merge" => [
"first_name" => "Joe",
"start_date" => "2026-03-11",
]
]]
];
$ch = curl_init("https://dev.inkless.co.uk/api/send_link");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_TOKEN",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
curl_close($ch);
import json
import requests
payload = {
"email_template_id": 21,
"client_reference_id": "REF-1001",
"email_reminders": True,
"email_completed_pdf": False,
"enforce_routing_order": True,
"require_us_esign_consent": False,
"recipients": [
{
"recipient_number": 1,
"routing_order": 1,
"name": "Joe Blogs",
"email": "joe@example.com",
"phone": "447841939203",
"send_otp_sms": False,
"send_link_sms": False,
"read_only": False
},
{
"recipient_number": 2,
"routing_order": 2,
"name": "Manager",
"email": "manager@example.com",
"send_otp_sms": True,
"send_link_sms": True,
"read_only": False
}
],
"documents": [{
"document_template_id": 5,
"recipient_numbers": [1, 2],
"merge": {
"first_name": "Joe",
"start_date": "2026-03-11"
}
}]
}
r = requests.post(
"https://dev.inkless.co.uk/api/send_link",
headers={
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
data=json.dumps(payload)
)
print(r.json())
Request Payload Schema
Root Schema
| Field | Type | Required | Description |
|---|---|---|---|
email_template_id | integer | Yes | ID of the email template to use. |
recipients | array | No | Envelope-level recipients. Use numbered recipient_number keys like 1, 2, 3. Recommended for multi-document sends. |
documents | array | Yes | Documents to prepare and send. |
client_reference_id | string | No | Your external reference stored against the envelope. |
reference_id | string | No | Alias of client_reference_id. |
email_reminders | boolean | No | Override company default reminder emails. |
email_completed_pdf | boolean | No | Override company default completed-PDF emails. |
enforce_routing_order | boolean | No | When true, only the lowest routing group is sent immediately. |
require_us_esign_consent | boolean | No | Require U.S. e-sign consent for recipients in the envelope. |
Documents Schema
| Field | Type | Required | Description |
|---|---|---|---|
document_template_id | integer | Yes | Document template ID. |
recipient_numbers | array<integer> | Yes | Envelope recipient numbers assigned to this document, for example [1,2,3]. |
merge | object | No | Template merge values for the document. |
combine | integer | No | Documents with the same combine value are merged together. |
require_one | boolean | No | Marks the document as either/or. |
replacement_file_name | string | No | Filename for the replacement upload. |
replacement_file_base64 | string | No | Base64 file content used as a replacement source for the template. |
recipients | array | No | Legacy per-document recipient format. Still accepted for backward compatibility. |
Envelope Recipients Schema
| Field | Type | Required | Description |
|---|---|---|---|
recipient_number | integer | Yes | Unique recipient identifier within the envelope. Use a different value for each person. |
routing_order | integer | No | Signing order for this recipient. Recipients with the same routing order sign in parallel. Defaults to recipient_number. |
name | string | Yes | Recipient name. |
email | string | Yes | Recipient email address. |
phone | string | No | Recipient phone number for SMS link or SMS OTP. |
send_link_sms | boolean | No | Send the signing link by SMS instead of email. |
send_otp_sms | boolean | No | Require OTP by SMS for this recipient. |
read_only | boolean | No | Create a view-only recipient. |
override_next_signer | boolean | No | Allow this signer to supply the details for a later signer in the envelope. |
override_target_recipient | integer | No | Recipient number of the later signer whose details this signer is allowed to provide. |
Example JSON Payload
{
"email_template_id": 21,
"client_reference_id": "REF-1001",
"email_reminders": true,
"email_completed_pdf": false,
"enforce_routing_order": true,
"require_us_esign_consent": false,
"recipients": [
{
"recipient_number": 1,
"routing_order": 1,
"name": "Joe Blogs",
"email": "joe.blogs@example.com",
"phone": "447841939203",
"send_otp_sms": false,
"send_link_sms": false,
"read_only": false,
"override_next_signer": true,
"override_target_recipient": 2
},
{
"recipient_number": 2,
"routing_order": 2,
"name": "",
"email": "",
"phone": "",
"send_otp_sms": false,
"send_link_sms": false,
"read_only": false
},
{
"recipient_number": 3,
"routing_order": 3,
"name": "Manager",
"email": "manager@example.com",
"phone": "447700900456",
"send_otp_sms": true,
"send_link_sms": true,
"read_only": false
}
],
"documents": [
{
"document_template_id": 125,
"recipient_numbers": [1, 2, 3],
"combine": 1,
"require_one": false,
"merge": {
"first_name": "Joe",
"middle_name": "",
"last_name": "Lowe",
"client_full_name": "Joe Blogs",
"date_of_birth": "1988-04-20",
"current_address": "10 High Street, Leeds, LS1 2AB",
"previous_address": "Flat 2, 14 Old Road, Leeds, LS11 3CD",
"previous_name": "",
"email_address": "joe.blogs@example.com",
"contact_number": "07841939203",
"lender_name": "Example Bank plc",
"registration_number": "AB12 CDE",
"todays_date": "2026-03-11",
"claim_id": "CLM-123456",
"client_id": "C-987654"
}
}
]
}
Successful Response
{
"ok": true,
"envelope_id": 1234,
"envelope_token": "abcdef1234567890",
"documents_billed": 1,
"documents": [
{
"document_template_id": 125,
"document_token": "94c82c53c6fef99e6b35cda0e9bf1f420cf1d7a5c71c0acdac02c556dcbbcbef"
}
],
"secure_links": [
{
"recipient_number": 1,
"secure_link_id": 271,
"recipient_name": "Joe Blogs",
"recipient_email": "joe.blogs@example.com",
"expires_at": "2026-03-14 12:00:00"
},
{
"recipient_number": 2,
"secure_link_id": 272,
"recipient_name": "Manager",
"recipient_email": "manager@example.com",
"expires_at": "2026-03-14 12:00:00"
}
],
"spent": "£0.70",
"remaining": "£9.30",
"client_reference_id": "REF-1001"
}
Response Headers
| Header | When Present | Description |
|---|---|---|
X-New-Api-Token | When a short-lived bearer token is used | The rotated replacement API token. Persist this value and use it on the next request. |
API Test Response
{
"ok": true,
"test_mode": true,
"message": "send_link test response",
"envelope_id": 0,
"envelope_token": "test_envelope_token",
"documents_billed": 1,
"documents": [
{
"document_template_id": 125,
"document_token": "test_document_token_1"
}
],
"secure_links": [
{
"recipient_number": 1,
"secure_link_id": 0,
"recipient_name": "API Test Recipient",
"recipient_email": "api@inkless.co.uk",
"expires_at": "2026-03-12 12:00:00"
}
],
"spent": "£0.00",
"remaining": "£0.00",
"client_reference_id": "REF-1001"
}
Error Responses
See the shared Error Handling section below for the current API error list, including the exact errors returned by /api/send_link.
Resend Envelope Links
/api/resend_envelope_links
This endpoint resends signing links for an envelope using the envelope token.
It targets all remaining unsigned recipients. If signing order is enabled, it only resends to the current active routing batch.
Expired links in that target set are reactivated as part of the resend.
curl -sS -X POST "https://dev.inkless.co.uk/api/resend_envelope_links" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"envelope_token": "abcdef1234567890"
}'
$body = @{
envelope_token = "abcdef1234567890"
} | ConvertTo-Json -Compress
Invoke-RestMethod -Method Post -Uri "https://dev.inkless.co.uk/api/resend_envelope_links" `
-Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } `
-ContentType "application/json" -Body $body
const res = await fetch("https://dev.inkless.co.uk/api/resend_envelope_links", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
envelope_token: "abcdef1234567890"
})
});
console.log(await res.json());
<?php
$payload = [
"envelope_token" => "abcdef1234567890"
];
$ch = curl_init("https://dev.inkless.co.uk/api/resend_envelope_links");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_TOKEN",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
curl_close($ch);
import requests
payload = {
"envelope_token": "abcdef1234567890"
}
r = requests.post(
"https://dev.inkless.co.uk/api/resend_envelope_links",
headers={
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
json=payload
)
print(r.json())
Request Payload Schema
| Field | Type | Required | Description |
|---|---|---|---|
envelope_token | string | Yes | The envelope token returned when the envelope was created. |
Successful Response
{
"ok": true,
"envelope_token": "abcdef1234567890",
"status": "resent",
"resent": 2,
"notified_email": 2,
"charged_p": 0
}
No Pending Recipients Response
{
"ok": true,
"envelope_token": "abcdef1234567890",
"status": "skipped_no_pending",
"resent": 0
}
Error Responses
| HTTP | Error | When |
|---|---|---|
400 | envelope_token required | The request body does not include envelope_token. |
401 | unauthorized | The bearer token is missing, invalid, or expired. |
404 | not found | The envelope token does not belong to the authenticated company. |
Download Signed Document
/api/download_signed
Returns the signed PDF for a document token as base64 JSON.
Authentication uses the Authorization: Bearer ... header. The request body must be JSON. Short-lived bearer tokens are supported and may return a replacement token in X-New-Api-Token.
Request Payload
{
"document_token": "your_document_token"
}
Successful Response
{
"ok": true,
"file": {
"filename": "personal-guarantee-signed.pdf",
"mime": "application/pdf",
"size": 350170,
"sha256": "2a4bd9a8d0a2b8eda8b076cb1db9e94b777dc6e7c1c653032c70f0568e296165",
"content_base64": "BASE64_ENCODED_CONTENT"
}
}
curl -sS -X POST "https://dev.inkless.co.uk/api/download_signed" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"document_token":"your_document_token"}'
$body = @{ document_token = "your_document_token" } | ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Post -Uri "https://dev.inkless.co.uk/api/download_signed" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://dev.inkless.co.uk/api/download_signed", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({ document_token: "your_document_token" })
});
console.log(await res.json());
<?php
$payload = ["document_token" => "your_document_token"];
$ch = curl_init("https://dev.inkless.co.uk/api/download_signed");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_TOKEN",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
curl_close($ch);
import requests
payload = {"document_token": "your_document_token"}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post("https://dev.inkless.co.uk/api/download_signed", headers=headers, json=payload)
print(r.json())
Error Responses
| HTTP | Error | Meaning |
|---|---|---|
| 404 | not_found | The document token does not exist for the authenticated company. |
| 409 | not_signed | The document exists but is not signed yet. |
| 404 | signed_file_not_found | The document record exists but the signed PDF could not be found in storage. |
| 500 | read_failed | The signed file could not be read after retrieval. |
| 401 | auth errors | Bearer token is missing, expired, revoked, or invalid. |
Download Document Archive
/api/download_archive
Builds and returns the court bundle ZIP for a single document token as base64 JSON.
Authentication uses the Authorization: Bearer ... header. The request body must be JSON. Short-lived bearer tokens are supported and may return a replacement token in X-New-Api-Token.
Request Payload
{
"document_token": "your_document_token"
}
Successful Response
{
"ok": true,
"file": {
"filename": "court-bundle-94c82c53.zip",
"mime": "application/zip",
"size": 1213940,
"sha256": "b52c96bebe60885f34063ef5706c340e052614ef29deffff1a6b5af2c0dd5e69",
"content_base64": "BASE64_ENCODED_CONTENT"
}
}
curl -sS -X POST "https://dev.inkless.co.uk/api/download_archive" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"document_token":"your_document_token"}'
$body = @{ document_token = "your_document_token" } | ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Post -Uri "https://dev.inkless.co.uk/api/download_archive" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://dev.inkless.co.uk/api/download_archive", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({ document_token: "your_document_token" })
});
console.log(await res.json());
<?php
$payload = ["document_token" => "your_document_token"];
$ch = curl_init("https://dev.inkless.co.uk/api/download_archive");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_TOKEN",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
curl_close($ch);
import requests
payload = {"document_token": "your_document_token"}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post("https://dev.inkless.co.uk/api/download_archive", headers=headers, json=payload)
print(r.json())
Error Responses
| HTTP | Error | Meaning |
|---|---|---|
| 400 | server_error | The bundle could not be built or the token was invalid for this company. |
| 400 | server_error + message | The response includes a user-friendly message when bundle generation fails. |
| 500 | read_failed | The built ZIP could not be read before encoding. |
| 401 | auth errors | Bearer token is missing, expired, revoked, or invalid. |
Download Audit Log
/api/download_audit_log
Returns the audit log for a single document token as base64 JSON.
Authentication uses the Authorization: Bearer ... header. The request body must be JSON. Short-lived bearer tokens are supported and may return a replacement token in X-New-Api-Token.
Request Payload
{
"document_token": "your_document_token"
}
Successful Response
{
"ok": true,
"file": {
"filename": "your_document_token.ndjson",
"mime": "application/x-ndjson",
"size": 12458,
"sha256": "2a4bd9a8d0a2b8eda8b076cb1db9e94b777dc6e7c1c653032c70f0568e296165",
"content_base64": "BASE64_ENCODED_CONTENT"
}
}
The audit file type depends on what is stored for that document. Common values are .ndjson, .jsonl, .json, .log, or .txt.
curl -sS -X POST "https://dev.inkless.co.uk/api/download_audit_log" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"document_token":"your_document_token"}'
$body = @{ document_token = "your_document_token" } | ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Post -Uri "https://dev.inkless.co.uk/api/download_audit_log" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://dev.inkless.co.uk/api/download_audit_log", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({ document_token: "your_document_token" })
});
console.log(await res.json());
<?php
$payload = ["document_token" => "your_document_token"];
$ch = curl_init("https://dev.inkless.co.uk/api/download_audit_log");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_TOKEN",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
curl_close($ch);
import requests
payload = {"document_token": "your_document_token"}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post("https://dev.inkless.co.uk/api/download_audit_log", headers=headers, json=payload)
print(r.json())
Error Responses
| HTTP | Error | Meaning |
|---|---|---|
| 404 | not_found | The document token does not exist for the authenticated company. |
| 404 | audit_log_not_found | The document exists but no audit log file could be found in storage. |
| 500 | read_failed | The audit log file could not be read after retrieval. |
| 401 | auth errors | Bearer token is missing, expired, revoked, or invalid. |
Download Envelope Archive
/api/download_envelope_archive
Builds and returns the envelope bundle ZIP as base64 JSON.
Authentication uses the Authorization: Bearer ... header. The request body must be JSON. Short-lived bearer tokens are supported and may return a replacement token in X-New-Api-Token.
Request Payload
{
"envelope_token": "your_envelope_token"
}
Successful Response
{
"ok": true,
"file": {
"filename": "envelope_94c82c53c6fe.zip",
"mime": "application/zip",
"size": 1536000,
"sha256": "3b6c5d9a3b5c9e89fabae0234567890abcdef1234567890abcdef1234567890",
"content_base64": "BASE64_ENCODED_CONTENT"
},
"included": [
{
"document_token": "94c82c53c6fef99e6b35cda0e9bf1f420cf1d7a5c71c0acdac02c556dcbbcbef",
"document_name": "Personal Guarantee",
"inner_file": "01-personal-guarantee.zip",
"size": 1213940,
"sha256": "b52c96bebe60885f34063ef5706c340e052614ef29deffff1a6b5af2c0dd5e69"
}
],
"skipped": []
}
curl -sS -X POST "https://dev.inkless.co.uk/api/download_envelope_archive" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"envelope_token":"your_envelope_token"}'
$body = @{ envelope_token = "your_envelope_token" } | ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Post -Uri "https://dev.inkless.co.uk/api/download_envelope_archive" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://dev.inkless.co.uk/api/download_envelope_archive", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({ envelope_token: "your_envelope_token" })
});
console.log(await res.json());
<?php
$payload = ["envelope_token" => "your_envelope_token"];
$ch = curl_init("https://dev.inkless.co.uk/api/download_envelope_archive");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_TOKEN",
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
curl_close($ch);
import requests
payload = {"envelope_token": "your_envelope_token"}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post("https://dev.inkless.co.uk/api/download_envelope_archive", headers=headers, json=payload)
print(r.json())
Error Responses
| HTTP | Error | Meaning |
|---|---|---|
| 400 | missing_company_or_envelope_token | The company context or envelope token was missing. |
| 500 | read_failed | The built envelope ZIP could not be read before encoding. |
| 409 | no_complete_documents | No complete documents were available to include in the envelope bundle. |
| 400 | Envelope not found or other thrown message | The envelope was invalid or bundle generation failed before reading. |
| 401 | auth errors | Bearer token is missing, expired, revoked, or invalid. |
Get Envelope Status
/api/get_envelope_status
Returns a single envelope record with its documents and recipients for one envelope token.
Authentication uses the Authorization: Bearer ... header. The request body must be JSON. Short-lived bearer tokens are supported and may return a replacement token in X-New-Api-Token.
Request Payload
{
"envelope_token": "your_envelope_token"
}
Successful Response
{
"ok": true,
"envelope": {
"id": 1234,
"token": "abcdef1234567890",
"status": "completed",
"created_at": "2026-03-11 12:00:00",
"total_documents": 1,
"total_recipients": 2
},
"documents": [
{
"document_token": "94c82c53c6fef99e6b35cda0e9bf1f420cf1d7a5c71c0acdac02c556dcbbcbef",
"document_name": "Personal Guarantee",
"pending": 0,
"complete": true,
"can_replace": false
}
],
"recipients": [
{
"name": "Joe Blogs",
"email": "joe@example.com",
"phone_number": null,
"used": 1,
"expires_at": "2026-03-14 12:00:00",
"access_link_date_time": "2026-03-11 12:05:00",
"status": "signed",
"status_label": "Signed"
},
{
"name": "Manager",
"email": "manager@example.com",
"phone_number": null,
"used": 1,
"expires_at": "2026-03-14 12:00:00",
"access_link_date_time": "2026-03-11 12:07:00",
"status": "signed",
"status_label": "Signed"
}
]
}
curl -sS -X POST "https://dev.inkless.co.uk/api/get_envelope_status" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"envelope_token":"your_envelope_token"}'
$body = @{ envelope_token = "your_envelope_token" } | ConvertTo-Json -Depth 10
Invoke-RestMethod -Method Post -Uri "https://dev.inkless.co.uk/api/get_envelope_status" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://dev.inkless.co.uk/api/get_envelope_status", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({ envelope_token: "your_envelope_token" })
});
console.log(await res.json());
<?php
$payload = [
"envelope_token" => "your_envelope_token"
];
$ch = curl_init("https://dev.inkless.co.uk/api/get_envelope_status");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
],
CURLOPT_POSTFIELDS => json_encode($payload),
]);
echo curl_exec($ch);
curl_close($ch);
import requests
payload = {"envelope_token": "your_envelope_token"}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post("https://dev.inkless.co.uk/api/get_envelope_status", headers=headers, json=payload)
print(r.json())
Error Responses
| HTTP | Error | Meaning |
|---|---|---|
| 400 | missing_company_or_envelope_token | The company context or envelope token was missing. |
| 404 | envelope_not_found | The envelope token does not exist for the authenticated company. |
| 500 | server_error | Status lookup failed unexpectedly. |
| 401 | auth errors | Bearer token is missing, expired, revoked, or invalid. |
Error Handling
We return a non-2xx HTTP status and a JSON body with ok: false and an error key. Your client should check both the HTTP status and the response body.
Validation failures may also include an errors array, and some server-side failures include a user-facing message.
{
"ok": false,
"error": "validation_error",
"errors": [
"email_template_is_required",
"documents[0].document_template_id_is_required"
]
}
{
"ok": false,
"error": "server_error",
"message": "Server error. Please try again."
}
Common API Errors
| HTTP | Error | Meaning |
|---|---|---|
401 | auth errors | Bearer token is missing, expired, revoked, malformed, or invalid. |
422 | validation_error | The request body failed validation. Inspect the errors array for field-specific failures. |
400 | missing_company_or_envelope_token | The request did not include a required envelope_token or the authenticated token had no company context. |
404 | envelope_not_found | The envelope token does not exist for the authenticated company. |
404 | not_found | The document token does not exist for the authenticated company. |
409 | not_signed | The document exists but has not been fully signed yet. |
404 | signed_file_not_found | The document exists, but the signed PDF could not be found in storage. |
400 | Company contact email not found | Company configuration is incomplete for sending. |
404 | email_template_not_found_for_this_company <id> | The email template does not belong to the authenticated company. |
400 | no_documents_to_send | No valid document outputs were created after planning. |
402 | insufficient_funds | Wallet balance is too low for the requested send. |
402 | document_debit_failed | Document billing failed after planning completed. |
500 | document_planning_failed | Document preparation or recipient grouping failed. |
500 | envelope_create_failed | The envelope record could not be created. |
500 | secure_link_create_failed | One or more secure links could not be created. |
500 | read_failed | A generated file or bundle could not be read before encoding. |
400 | server_error | A general server-side failure occurred. Some endpoints also include a human-readable message. |
409 | no_complete_documents | No completed documents were available to include in an envelope bundle. |
Company Webhooks
Inkless can POST JSON to your company webhook URL when subscribed events occur. Webhook deliveries are queued and sent asynchronously.
Available Events
| Event | When It Fires |
|---|---|
otp_sent | An OTP is sent for a signing session. |
otp_verified | An OTP is successfully verified. |
document_signing_link_sent | A recipient is sent a signing link for the envelope. |
link_viewed | A signing link is opened. |
link_resent | A signing link is resent to a recipient. |
document_signed | A recipient signs a document. |
document_complete | The document has finished processing and the archive is ready. |
envelope_complete | All documents in the envelope have completed processing. |
sms_sent | An SMS message is accepted/submitted by the SMS provider. |
sms_delivered | An SMS message is reported as delivered. |
sms_failed | An SMS message fails. |
email_delivered | An email is reported as delivered. |
email_opened | An email is opened. |
email_clicked | An email link is clicked. |
email_bounced | An email bounces. |
email_rejected | An email is rejected. |
Example Payload
{
"event": "document_signed",
"secure_link_id": 271,
"document_token": "a1b2c3d4...",
"timestamp": "2026-03-11T23:25:31Z"
}
Webhook Verification
When you create or update a webhook, Inkless verifies the endpoint by POSTing a JSON payload containing a verification_secret. Your endpoint must respond with that exact token as plain text and HTTP 200.
{
"verification_secret": "abc123...",
"provider": "inkless",
"webhook_id": 42,
"timestamp": "2026-03-11T23:25:31Z"
}
Signing
Webhook requests are signed with HMAC-SHA256 so your endpoint can verify that the request genuinely came from Inkless and that the request body was not altered in transit.
The webhook secret itself is not sent in the request. Inkless stores the secret when you configure the webhook, computes a signature from the exact raw request body, and sends only the derived signature in the headers.
| Header | Description |
|---|---|
X-Inkless-Signature | Base64-encoded HMAC-SHA256 signature of the raw request body, computed using your webhook secret. |
X-Inkless-Alg | Signature algorithm identifier. Currently always SHA256. |
Your endpoint should recompute the HMAC using the exact raw request body and your stored secret, then compare it to X-Inkless-Signature. If they match, the webhook is authentic. If they do not match, reject the request.
Quick Verification Examples
Use the exact raw request body bytes, not a re-serialized JSON object. Compute HMAC-SHA256, base64-encode it, then compare it to X-Inkless-Signature.
# BODY_FILE contains the exact raw request body
# HEADER_SIG is the X-Inkless-Signature header value
# SECRET is your webhook secret
computed=$(openssl dgst -sha256 -hmac "$SECRET" -binary BODY_FILE | openssl base64 -A)
if [ "$computed" = "$HEADER_SIG" ]; then
echo "valid"
else
echo "invalid"
fi
$secret = "YOUR_WEBHOOK_SECRET"
$rawBody = [System.IO.File]::ReadAllBytes("body.json")
$headerSig = $headers["X-Inkless-Signature"]
$hmac = [System.Security.Cryptography.HMACSHA256]::new([Text.Encoding]::UTF8.GetBytes($secret))
$computed = [Convert]::ToBase64String($hmac.ComputeHash($rawBody))
if ($computed -eq $headerSig) {
"valid"
} else {
"invalid"
}
const crypto = require("crypto");
const secret = "YOUR_WEBHOOK_SECRET";
const rawBody = req.bodyRaw; // exact raw body Buffer
const headerSig = req.get("X-Inkless-Signature");
const computed = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("base64");
if (computed !== headerSig) {
throw new Error("Invalid webhook signature");
}
<?php
$secret = 'YOUR_WEBHOOK_SECRET';
$rawBody = file_get_contents('php://input');
$headerSig = $_SERVER['HTTP_X_INKLESS_SIGNATURE'] ?? '';
$computed = base64_encode(hash_hmac('sha256', $rawBody, $secret, true));
if (!hash_equals($computed, $headerSig)) {
http_response_code(401);
exit('Invalid signature');
}
import base64
import hmac
import hashlib
secret = b"YOUR_WEBHOOK_SECRET"
raw_body = request.get_data() # exact raw bytes
header_sig = request.headers.get("X-Inkless-Signature", "")
computed = base64.b64encode(
hmac.new(secret, raw_body, hashlib.sha256).digest()
).decode("ascii")
if not hmac.compare_digest(computed, header_sig):
raise ValueError("Invalid webhook signature")
Configure company webhooks in Company Settings. Use HTTPS endpoints only.
Changelog
- 2025-09-04 — Added docs for
get_credits,list_email_templates,list_document_templates, andget_artifact. Kept layout, added PHP/Python examples, generic tab & copy handling. - 2025-09-04 — Initial docs for
send_link. Credits check & debit, ownership checks, audit logging, first-batch mailing.