Introduction
The Inkless API lets your application create, send, track, and download digital signing requests. In Inkless, a send is usually called an envelope: one email template, one or more document templates, and one or more recipients moving through a signing flow.
A typical integration looks like this: fetch your templates, send an envelope, store the returned tokens, wait for completion, then download the final signed files or bundle.
Create API tokens from Admin > Company > API Details. Tokens are shown only once when created, so store them securely. If a token is lost or exposed, revoke it and issue a new one.
email_template_id, every
document_template_id, and every template helper request must belong to the same company as the
token you are sending with.
API Endpoints
Use the same base path for all API requests. Most endpoints accept JSON plus a bearer token. File upload endpoints use multipart/form-data instead.
https://inkless.co.uk/api
Sandbox hosts: https://sandbox.inkless.co.uk/api and https://sandbox.inkless.uk/api
Use a sandbox host when you want safe integration testing. In the current system, sandbox send-style endpoints
do not behave like production sends: /api/send_link rewrites recipient email addresses to the
authenticated company's contact_email and blanks recipient phone numbers, and
/api/send_id_check rewrites the recipient email address to that same company
contact_email.
Common routes:
/api/send_link,
/api/get_documents,
/api/get_emails,
/api/get_template_mapping,
/api/preview_documents,
/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 one of these formats:
ink_live_key.secret
ink_test_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 the token is short-lived, Inkless rotates it after a successful authenticated request and returns the replacement value in
X-New-Api-Token. - 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.
Short-Lived Tokens
Short-lived tokens are supported across authenticated API routes. When one is used successfully, your client should immediately replace its stored token with the value returned in the X-New-Api-Token response header.
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://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://inkless.co.uk/api/get_balance" `
-Headers $h -ContentType "application/json"
const res = await fetch("https://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://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"
],
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
import requests, json
r = requests.post("https://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://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://inkless.co.uk/api/get_documents" `
-Headers $h -ContentType "application/json"
const res = await fetch("https://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://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"
],
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
import requests, json
r = requests.post("https://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://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://inkless.co.uk/api/get_emails" `
-Headers $h -ContentType "application/json"
const res = await fetch("https://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://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"
],
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
import requests, json
r = requests.post("https://inkless.co.uk/api/get_emails",
headers={"Authorization":"Bearer YOUR_API_TOKEN","Content-Type":"application/json"})
print(r.json())
Template Tools
These endpoints support template inspection, one-off uploads, and rendered previews before you send an envelope.
Get Template Mapping
/api/get_template_mapping
Returns document metadata, merge fields, merge examples, recipient count, and the saved field mappings for a template.
{
"doc_id": 125
}
curl -sS -X POST "https://inkless.co.uk/api/get_template_mapping" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"doc_id": 125
}'
$headers = @{ Authorization = "Bearer YOUR_API_TOKEN" }
$body = @{
doc_id = 125
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "https://inkless.co.uk/api/get_template_mapping" `
-Headers $headers `
-ContentType "application/json" `
-Body $body
const payload = { doc_id: 125 };
const res = await fetch("https://inkless.co.uk/api/get_template_mapping", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
console.log(await res.json());
<?php
$payload = ["doc_id" => 125];
$ch = curl_init("https://inkless.co.uk/api/get_template_mapping");
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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
import requests
payload = {"doc_id": 125}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post(
"https://inkless.co.uk/api/get_template_mapping",
headers=headers,
json=payload
)
print(r.json())
{
"ok": true,
"document": {
"id": 125,
"name": "Personal Guarantee",
"file_name": "company/documents/template-125.docx",
"pdf_path": "company/documents/template-125.pdf",
"version": "3",
"mapping_version": "12",
"merge_fields": ["first_name", "last_name"],
"merge_examples": {
"first_name": "Joe",
"last_name": "Blogs"
},
"recipient_count": 2
},
"mappings": []
}
Preview Documents
/api/preview_documents
Builds PDF previews for one or more template documents, including overlay coordinates for mapped fields. Combined documents are returned as merged preview items.
{
"documents": [
{
"doc_id": 125,
"merge_fields": {
"first_name": "Joe",
"last_name": "Blogs"
},
"combine": 1,
"combine_key": "set-a"
}
]
}
curl -sS -X POST "https://inkless.co.uk/api/preview_documents" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"documents": [
{
"doc_id": 125,
"merge_fields": {
"first_name": "Joe",
"last_name": "Blogs"
},
"combine": 1,
"combine_key": "set-a"
}
]
}'
$headers = @{ Authorization = "Bearer YOUR_API_TOKEN" }
$body = @{
documents = @(
@{
doc_id = 125
merge_fields = @{
first_name = "Joe"
last_name = "Blogs"
}
combine = 1
combine_key = "set-a"
}
)
} | ConvertTo-Json -Depth 6
Invoke-RestMethod -Method Post -Uri "https://inkless.co.uk/api/preview_documents" `
-Headers $headers `
-ContentType "application/json" `
-Body $body
const payload = {
documents: [
{
doc_id: 125,
merge_fields: {
first_name: "Joe",
last_name: "Blogs"
},
combine: 1,
combine_key: "set-a"
}
]
};
const res = await fetch("https://inkless.co.uk/api/preview_documents", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
console.log(await res.json());
<?php
$payload = [
"documents" => [
[
"doc_id" => 125,
"merge_fields" => [
"first_name" => "Joe",
"last_name" => "Blogs"
],
"combine" => 1,
"combine_key" => "set-a"
]
]
];
$ch = curl_init("https://inkless.co.uk/api/preview_documents");
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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
import requests
payload = {
"documents": [
{
"doc_id": 125,
"merge_fields": {
"first_name": "Joe",
"last_name": "Blogs"
},
"combine": 1,
"combine_key": "set-a"
}
]
}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post(
"https://inkless.co.uk/api/preview_documents",
headers=headers,
json=payload
)
print(r.json())
{
"ok": true,
"items": [
{
"name": "Combined Document",
"page_count": 4,
"pdf_base64": "BASE64_ENCODED_PDF",
"overlays": {
"1": [
{
"x": 10,
"y": 20,
"w": 25,
"h": 8,
"type": "signature",
"source": "recipient_1_signature",
"label": "recipient_1_signature"
}
]
}
}
]
}
Utilities
These helper endpoints expose company send settings, recipient counts, and wallet pricing checks used by the current send flow.
Get Company Settings
/api/get_company_settings
Returns the default send behaviour for the authenticated company.
curl -sS -X POST "https://inkless.co.uk/api/get_company_settings" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json"
$headers = @{ Authorization = "Bearer YOUR_API_TOKEN" }
Invoke-RestMethod -Method Post -Uri "https://inkless.co.uk/api/get_company_settings" `
-Headers $headers `
-ContentType "application/json"
const res = await fetch("https://inkless.co.uk/api/get_company_settings", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
});
console.log(await res.json());
<?php
$ch = curl_init("https://inkless.co.uk/api/get_company_settings");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_TOKEN",
"Content-Type: application/json"
],
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
import requests
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post(
"https://inkless.co.uk/api/get_company_settings",
headers=headers
)
print(r.json())
{
"ok": true,
"email_reminders": 1,
"email_completed_pdf": 0,
"enforce_signing_order": 1
}
Wallet Precheck
/api/wallet_precheck
Calculates the projected cost of a send before submission, including document groups, SMS usage, and identity verification charges.
{
"doc_groups": 2,
"sms_otp_recips": 1,
"sms_link_recips": 1,
"identity_verification_recips": 0
}
curl -sS -X POST "https://inkless.co.uk/api/wallet_precheck" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"doc_groups": 2,
"sms_otp_recips": 1,
"sms_link_recips": 1,
"identity_verification_recips": 0
}'
$headers = @{ Authorization = "Bearer YOUR_API_TOKEN" }
$body = @{
doc_groups = 2
sms_otp_recips = 1
sms_link_recips = 1
identity_verification_recips = 0
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "https://inkless.co.uk/api/wallet_precheck" `
-Headers $headers `
-ContentType "application/json" `
-Body $body
const payload = {
doc_groups: 2,
sms_otp_recips: 1,
sms_link_recips: 1,
identity_verification_recips: 0
};
const res = await fetch("https://inkless.co.uk/api/wallet_precheck", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
console.log(await res.json());
<?php
$payload = [
"doc_groups" => 2,
"sms_otp_recips" => 1,
"sms_link_recips" => 1,
"identity_verification_recips" => 0
];
$ch = curl_init("https://inkless.co.uk/api/wallet_precheck");
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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
import requests
payload = {
"doc_groups": 2,
"sms_otp_recips": 1,
"sms_link_recips": 1,
"identity_verification_recips": 0
}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post(
"https://inkless.co.uk/api/wallet_precheck",
headers=headers,
json=payload
)
print(r.json())
{
"ok": true,
"needed_p": 80,
"have_p": 930,
"short_p": 0,
"after_p": 850,
"needed": "£0.80",
"have": "£9.30",
"short": "£0.00",
"after": "£8.50",
"doc_groups": 2,
"sms_otp_recips": 1,
"sms_link_recips": 1,
"identity_verification_recips": 0,
"doc_p": "£0.70",
"otp_p": "£0.05",
"link_p": "£0.05",
"identity_verification_p": "£0.00",
"per_doc": "£0.70",
"per_sms": "£0.05",
"per_identity_verification": "£1.00",
"note": "Doc cost capped at £1.50 for 1-5 docs."
}
Recipient Count
/api/recipient_count
Returns the configured recipient count for a document template.
{
"doc_id": 125
}
curl -sS -X POST "https://inkless.co.uk/api/recipient_count" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"doc_id": 125
}'
$headers = @{ Authorization = "Bearer YOUR_API_TOKEN" }
$body = @{
doc_id = 125
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri "https://inkless.co.uk/api/recipient_count" `
-Headers $headers `
-ContentType "application/json" `
-Body $body
const payload = { doc_id: 125 };
const res = await fetch("https://inkless.co.uk/api/recipient_count", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
console.log(await res.json());
<?php
$payload = ["doc_id" => 125];
$ch = curl_init("https://inkless.co.uk/api/recipient_count");
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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
import requests
payload = {"doc_id": 125}
headers = {
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
}
r = requests.post(
"https://inkless.co.uk/api/recipient_count",
headers=headers,
json=payload
)
print(r.json())
{
"ok": true,
"recipient_count": 2
}
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.
Sandbox behaviour: when this endpoint is called on https://sandbox.inkless.co.uk, Inkless rewrites all recipient email addresses to the authenticated company's contact_email before sending. Recipient phone numbers are also blanked in that environment.
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://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://inkless.co.uk/api/send_link" `
-Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } `
-ContentType "application/json" -Body $body
const res = await fetch("https://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://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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
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://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. |
allow_embed | boolean | No | When true, the returned embed_link can be used inside a third-party iframe, but only from origins whitelisted in Company Settings. If false or omitted, iframe embedding remains blocked for that link. |
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,
"allow_embed": true,
"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",
"verify_url": "https://sign.inkless.co.uk/verify/recipient-token-1",
"embed_link": "https://sign.inkless.co.uk/verify/recipient-token-1?embed=1"
},
{
"recipient_number": 2,
"secure_link_id": 272,
"recipient_name": "Manager",
"recipient_email": "manager@example.com",
"expires_at": "2026-03-14 12:00:00",
"verify_url": "https://sign.inkless.co.uk/verify/recipient-token-2",
"embed_link": "https://sign.inkless.co.uk/verify/recipient-token-2?embed=1"
}
],
"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",
"verify_url": "https://sign.inkless.co.uk/verify/test-recipient-token-1",
"embed_link": "https://sign.inkless.co.uk/verify/test-recipient-token-1?embed=1"
}
],
"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.
End-to-End Example
This example shows the simplest production flow to build first: send an envelope, store the returned identifiers, wait for completion, then download the final bundle.
1. Send The Envelope
Before you call /api/send_link, make sure you already know which template IDs and recipients you want to use. At minimum, you need:
- A valid API token in the
Authorizationheader. - An
email_template_idthat belongs to the same company as the token. - At least one
document_template_idindocuments. - At least one recipient, either envelope-level or legacy per-document recipients.
- Any merge fields your template expects.
{
"email_template_id": 21,
"client_reference_id": "INV-10042",
"email_reminders": true,
"email_completed_pdf": false,
"enforce_routing_order": true,
"recipients": [
{
"recipient_number": 1,
"routing_order": 1,
"name": "Joe Blogs",
"email": "joe@example.com",
"phone": "447700900123",
"send_otp_sms": false,
"send_link_sms": false
}
],
"documents": [
{
"document_template_id": 125,
"recipient_numbers": [1],
"merge": {
"first_name": "Joe",
"invoice_number": "INV-10042"
}
}
]
}
2. Store These Fields From The Response
Do not treat the send response as disposable. After a successful /api/send_link call, store the identifiers you will need later:
client_reference_idor your own foreign key, so you can map Inkless back to your record.envelope_token, which you need later for/api/get_envelope_statusand/api/download_envelope_archive.- Each document
document_token, which you need later for/api/download_signed,/api/download_archive, and/api/download_audit_log. - Optionally each recipient
secure_link_idif you want recipient-level tracking or resend operations.
{
"ok": true,
"envelope_id": 1234,
"envelope_token": "abcdef1234567890",
"documents": [
{
"document_template_id": 125,
"document_token": "94c82c53c6fef99e6b35cda0e9bf1f420cf1d7a5c71c0acdac02c556dcbbcbef"
}
],
"secure_links": [
{
"recipient_number": 1,
"secure_link_id": 271,
"recipient_email": "joe@example.com",
"verify_url": "https://sign.inkless.co.uk/verify/recipient-token-1"
}
],
"client_reference_id": "INV-10042"
}
{
"your_record_id": "INV-10042",
"client_reference_id": "INV-10042",
"envelope_token": "abcdef1234567890",
"documents": [
{
"document_template_id": 125,
"document_token": "94c82c53c6fef99e6b35cda0e9bf1f420cf1d7a5c71c0acdac02c556dcbbcbef"
}
],
"recipients": [
{
"recipient_number": 1,
"secure_link_id": 271
}
]
}
3. Wait For Completion Webhooks
Inkless can notify your webhook as the envelope progresses. For most integrations, the key event is envelope_complete, because that is the point where you can safely fetch the final envelope bundle.
{
"event": "envelope_complete",
"company_id": 1,
"envelope_id": 1234,
"envelope_token": "abcdef1234567890",
"timestamp": "2026-07-24T14:10:00Z"
}
In your webhook handler, match envelope_token back to the record you stored after /api/send_link, then trigger your download step from there.
4. Download The Final Bundle
Use the stored envelope_token to download the completed envelope ZIP from /api/download_envelope_archive. For most integrations, this is the simplest single result to store in your own system.
curl -sS -X POST "https://inkless.co.uk/api/download_envelope_archive" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"envelope_token":"abcdef1234567890"}'
const fs = require("fs");
const res = await fetch("https://inkless.co.uk/api/download_envelope_archive", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({ envelope_token: "abcdef1234567890" })
});
const json = await res.json();
if (!json.ok) throw new Error(json.error || "download failed");
fs.writeFileSync(json.file.filename, Buffer.from(json.file.content_base64, "base64"));
<?php
$payload = ["envelope_token" => "abcdef1234567890"];
$ch = curl_init("https://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),
]);
$response = curl_exec($ch);
if ($response === false) {
die("Curl error: " . curl_error($ch));
}
$json = json_decode($response, true);
curl_close($ch);
if (!empty($json["ok"])) {
file_put_contents($json["file"]["filename"], base64_decode($json["file"]["content_base64"]));
}
import base64
import requests
r = requests.post(
"https://inkless.co.uk/api/download_envelope_archive",
headers={
"Authorization": "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json",
},
json={"envelope_token": "abcdef1234567890"},
)
data = r.json()
if not data.get("ok"):
raise RuntimeError(data.get("error", "download failed"))
with open(data["file"]["filename"], "wb") as f:
f.write(base64.b64decode(data["file"]["content_base64"]))
5. Optional Per-Document Downloads
If you also stored the per-document document_token values, you can fetch individual outputs instead of, or as well as, the full envelope bundle:
/api/download_signedfor the signed PDF./api/download_archivefor the per-document court bundle ZIP./api/download_audit_logfor the audit log file.
Send ID Check Link
/api/send_id_check
This endpoint creates a verification-only envelope, charges one ID verification credit, queues the verification email, and returns the created envelope and secure link details.
Use this when you want to send a standalone identity check without attaching any documents.
Sandbox behaviour: when this endpoint is called on https://sandbox.inkless.co.uk, Inkless rewrites the recipient email address to the authenticated company's contact_email before sending.
curl -sS -X POST "https://inkless.co.uk/api/send_id_check" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Example",
"email": "jane@example.com",
"phone": "447700900123",
"client_reference_id": "IDCHK-1001"
}'
$body = @{
name = "Jane Example"
email = "jane@example.com"
phone = "447700900123"
client_reference_id = "IDCHK-1001"
} | ConvertTo-Json -Compress
Invoke-RestMethod -Method POST -Uri "https://inkless.co.uk/api/send_id_check" `
-Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } `
-ContentType "application/json" -Body $body
const res = await fetch("https://inkless.co.uk/api/send_id_check", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({
name: "Jane Example",
email: "jane@example.com",
phone: "447700900123",
client_reference_id: "IDCHK-1001"
})
});
const json = await res.json();
console.log(json);
$payload = [
'name' => 'Jane Example',
'email' => 'jane@example.com',
'phone' => '447700900123',
'client_reference_id' => 'IDCHK-1001',
];
$ch = curl_init("https://inkless.co.uk/api/send_id_check");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer YOUR_API_TOKEN",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
import requests
payload = {
"name": "Jane Example",
"email": "jane@example.com",
"phone": "447700900123",
"client_reference_id": "IDCHK-1001"
}
headers = {"Authorization": "Bearer YOUR_API_TOKEN"}
r = requests.post("https://inkless.co.uk/api/send_id_check", headers=headers, json=payload)
print(r.json())
Request Body
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Recipient name. |
email | string | Yes | Recipient email address. |
phone | string | Yes | Recipient phone number. |
client_reference_id | string | No | Optional reference stored against the secure link. |
allow_embed | boolean | No | When true, the returned embed_link is allowed to load inside a third-party iframe, but only from origins whitelisted in Company Settings. |
Success Response
{
"ok": true,
"status": "queued",
"message": "ID verification link queued successfully.",
"envelope_id": 1281,
"envelope_token": "0f4f7f8c5a0b4c2f9d68c2d4e65f99a0",
"secure_link_id": 2195,
"recipient_name": "Jane Example",
"recipient_email": "jane@example.com",
"expires_at": "2026-06-30 13:45:00",
"verify_url": "https://sign.inkless.co.uk/verify/...",
"embed_link": "https://sign.inkless.co.uk/verify/...?embed=1",
"spent": "£1.00",
"remaining": "£145.95",
"client_reference_id": "IDCHK-1001"
}
Follow-up Events
Verification-only sends use the same queued notification pipeline as standard sends.
- When the email is queued and sent, Inkless emits the
identity_verification_link_sentcompany webhook event. - Once the recipient completes the ID check, use
/api/download_envelope_archivewith the returnedenvelope_tokento fetch the verification evidence bundle.
{
"event": "identity_verification_link_sent",
"company_id": 1,
"envelope_id": 1281,
"envelope_token": "0f4f7f8c5a0b4c2f9d68c2d4e65f99a0",
"secure_link_id": 2195,
"channel": "email",
"recipient": {
"name": "Jane Example",
"email": "j***@e*****.com",
"phone": ""
},
"verify_url": "https://sign.inkless.co.uk/verify/...",
"embed_link": "https://sign.inkless.co.uk/verify/...?embed=1",
"timestamp": "2026-06-25T14:10:00Z"
}
Errors
| HTTP | error | Meaning |
|---|---|---|
| 401 | unauthorized | API token missing or invalid. |
| 402 | insufficient_funds | Not enough balance to send the ID check. |
| 402 | identity_verification_debit_failed | The ID verification charge could not be applied. |
| 422 | validation_error | Required fields were missing or invalid. |
| 500 | envelope_create_failed | The envelope could not be created. |
| 500 | secure_link_create_failed | The secure link could not be created. |
| 500 | notification_queue_failed | The verification email could not be queued. |
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://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://inkless.co.uk/api/resend_envelope_links" `
-Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } `
-ContentType "application/json" -Body $body
const res = await fetch("https://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://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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
curl_close($ch);
import requests
payload = {
"envelope_token": "abcdef1234567890"
}
r = requests.post(
"https://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://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://inkless.co.uk/api/download_signed" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://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://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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
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://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://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://inkless.co.uk/api/download_archive" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://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://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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
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://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://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://inkless.co.uk/api/download_audit_log" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://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://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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
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://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://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://inkless.co.uk/api/download_envelope_archive" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://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://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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
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://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://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://inkless.co.uk/api/get_envelope_status" -Headers @{ Authorization = "Bearer YOUR_API_TOKEN" } -ContentType "application/json" -Body $body
const res = await fetch("https://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://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),
]);
$response = curl_exec($ch);
if ($response === false) {
echo "Curl error: " . curl_error($ch);
} else {
echo $response;
}
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://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
When a request fails, Inkless returns 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 JSON body.
Some failures include extra detail. Validation failures may include an errors array, and some server-side failures also include a human-readable 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 | The bearer token is missing, expired, revoked, malformed, or invalid. |
422 | validation_error | The request body was understood, but one or more fields failed validation. Check the errors array for the fields to fix. |
400 | missing_company_or_envelope_token | A required envelope_token was missing, or the authenticated token had no usable 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 | The company is missing required send configuration. |
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 request planning. |
402 | insufficient_funds | The company wallet balance is too low for the requested send. |
402 | document_debit_failed | Cost calculation succeeded, but the wallet debit failed when Inkless tried to charge the send. |
500 | document_planning_failed | Inkless could not finish document preparation or recipient grouping. |
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 Inkless encoded the response. |
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. |
identity_verification_link_sent | A verification-only ID check link is sent to a recipient. |
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
- 2026-07-24 Updated the developer API docs to match the current router and controller, added coverage for template tools and utility endpoints, documented short-lived token rotation, and removed duplicated sections.
- 2026-06-25 Added public docs for
send_id_check,resend_envelope_links, anddownload_envelope_archive.