e-Signature.eu REST API — electronic signatures (SES, AES, QES)
1. Overview
The e-Signature.eu REST API lets your application send documents for electronic signature (simple SES, advanced AES or qualified QES under eIDAS) without using our website. You send the PDF and the signers; we handle the identity verification, the signature and the follow-up; you get the signed document back through the API or a callback.
| Base URL | https://api.e-signature.eu/v3/signature/ |
This documentation describes API v3. Already using API v2 (https://api.e-signature.eu/v2/signature/)? See the Changelog.
2. Authentication
Your credentials are available in My account > Signatures API once you are logged in on e-signature.eu:
- User ID: sent in the URL as the
useridparameter of every call; - API token: sent in the
x-api-keyHTTP header of every call.
curl -X GET "https://api.e-signature.eu/v3/signature/requestBalance?userid=123" \
-H "x-api-key: YOUR_API_TOKEN"
Keep your token secret: it gives full access to your account through the API. Calls must be made from your server, never from a browser or a mobile app.
3. Conventions
Requests and responses
- HTTPS only. Request bodies are JSON (
Content-Type: application/json). - Every response is JSON, in one of these two forms:
{ "success": true, "result": { ... } }
{ "success": false, "error": "Readable error message" }
Limits
| Limit | Value |
|---|---|
| Requests | 10 per minute per account and IP address (HTTP 403 beyond, retry after one minute) |
| Document | One PDF per request, 30 MB maximum, not encrypted / not password protected |
| Download of signed documents | 30 days after completion (see Retention) |
4. Credits
Requests are paid with your prepaid e-signature.eu credits. The price of a method can be retrieved with getMethods (it takes your specific prices into account). Your balance is returned by requestBalance and in every request object (credits_balance).
Automatic credit recharge
To avoid running out of credits, you can let your account recharge itself automatically. It is set up once in My account > Signatures API, not through the API:
- Choose a threshold (in credits) and the credit pack to order.
- Save a payment method with our payment provider Mollie (card, or SEPA Direct Debit through Bancontact, iDEAL…). A verification payment of €0.01 is requested once.
- Whenever credits are debited and your balance falls below the threshold, an order for the chosen pack is created and paid automatically with the saved payment method. The invoice is sent to you by email.
| Automatic recharge | |
|---|---|
| Trigger | A debit of credits (e.g. a new request) that brings the balance below the threshold |
| Credits added | As soon as the payment is confirmed: immediately by card, after a few working days by SEPA Direct Debit |
| Recharges in progress | One at a time |
| Safety limit | A maximum number of automatic recharges per 30 days |
| Payment failure | The order is marked as failed, automatic recharge is turned off and you are informed by email |
You do not have enough credits) and does not trigger a recharge: choose a threshold higher than the cost of your largest requests, and keep a margin when paying by SEPA Direct Debit. Your balance remains available at any time through requestBalance and credits_balance.5. How a signature request works
- You create the request with the PDF, the signers and the accepted signing methods. The credits are debited.
- Each signer receives an email with a personal signing link (or you send the
signature_linkyourself whensend_emailisfalse). - The signer verifies his identity with one of the accepted methods (itsme®, Evrotrust, Veriff, OTP…) and signs.
- When everybody signed, the request is
finished: the signed PDF (PAdES) is available throughgetRequestand sent in the callback.
Signing order
By default all signers sign in parallel. To define an order, set sign_first: true on the signers of the first group: they are invited immediately, possibly several of them in parallel. The other signers are invited only when the whole first group has signed. Two groups at most.
Two integration modes
| Mode | How it works |
|---|---|
Email mode (send_email: true, default) |
We send the signing invitations, reminders and completion emails to the signers on your behalf. |
Embedded mode (send_email: false) |
No email is sent when the request is created: the signature_link of each signer is returned in the response, and you redirect your users to it from your own interface. |
send_email: false, a reminder or a cancellation triggered from the online dashboard sends an email to the pending signers.Do not poll the API
Do not call getRequest repeatedly to follow the status of a request: rely on the callbacks, sent automatically at each change. Excessive calls trigger an automatic throttling that temporarily suspends the processing of your requests.
6. Create a signature request
{
"title": "Sales agreement 2026-114",
"lang": "en",
"methods": [
"itsme-qes-signature",
"evrotrust-signature"
],
"signature_place": "position_auto",
"reminder": 3,
"send_email": true,
"email_notification": false,
"note_html": "Please sign the agreement before Friday.",
"signature_redirect": "https://www.example.com/thank-you",
"external_reference": "QUOTE-2026-114",
"metadata": {
"crm_deal_id": 8812
},
"callback_url": "https://erp.example.com/hooks/esignature",
"signers": [
{
"firstname": "Ann",
"lastname": "Peeters",
"email": "[email protected]",
"phone": "+32475111222",
"sign_first": true
},
{
"firstname": "Marc",
"lastname": "Dubois",
"email": "[email protected]",
"sign_first": false
}
],
"pdf_file_base64": "JVBERi0xLjcKJeLjz9MK..."
}
Signing methods
| Method | Level | Coverage | Price |
|---|---|---|---|
itsme-qes-signature — itsme® |
QES (qualified) | 32 European countries | 1 credit / signer |
evrotrust-signature — Evrotrust |
QES (qualified) | 69 countries | 1 credit / signer |
veriff-advanced-signature — Veriff |
AES (advanced) | 198 countries | 0.7 credit / signer |
otp-email-non-qualified — OTP by email or SMS |
SES (simple) | Worldwide | 0.4 credit / signer |
Prices are the standard prices: getMethods returns the methods available and the prices for your account. Only a qualified electronic signature (QES) is legally equivalent to a handwritten signature under the eIDAS Regulation. otp-email-non-qualified covers both email and SMS: the code is sent by SMS when the signer has a phone, otherwise by email, at the same price.
Body fields
| Field | Required | Type | Description |
|---|---|---|---|
title |
yes | string | Name of the document. Letters, digits and dashes are kept; a suffix -1, -2… is added if the name already exists in your account. |
lang |
yes | string | Language of the emails and signing pages: fr, nl, en, de, es. |
methods |
yes | array | Accepted signing methods, see Signing methods. When several are accepted, each signer chooses. |
signers |
yes | array | Signers, see below. |
pdf_file_base64 |
yes | string | The PDF, base64 encoded. One file, 30 MB maximum, not encrypted. |
signature_place |
no | string | position_auto (default) or position_manual, see Signature placement. |
reminder |
no | number | Automatic reminder every N days, 0 to 14. Default 0 (no reminder). |
send_email |
no | boolean | Default true (email mode). With false (embedded mode), no invitation nor completion email is sent: send the signature_link yourself. Can not be combined with sign_first: true. |
email_notification |
no | boolean | Default false. With true, you receive an email at each signature. |
note_html |
no | string | Message added to the emails sent to the signers (email mode). HTML tags are accepted, e.g. Note with <strong>HTML</strong>. |
signature_redirect |
no | string (URL) | Page shown to the signer after signing. Must be a valid URL (empty strings are refused). Default: confirmation page on e-signature.eu. |
external_reference |
no | string | Your own identifier (quote, contract…), max 255 characters. Returned in every response and callback. |
metadata |
no | object | Free JSON object, max 4 KB, returned as is. |
callback_url |
no | string (URL) | Callback URL for this request only (public HTTPS). Default: the URL of your account. |
Signer fields
| Field | Required | Type | Description |
|---|---|---|---|
lastname |
yes | string | |
email |
yes | string | Valid email address |
firstname |
no | string | |
phone |
no | string | International format starting with + and the country code (+32…); spaces are removed. An invalid number is ignored. With the OTP method, the code is sent by SMS when a phone number is given, otherwise by email. |
sign_first |
no | boolean | Default false. See Signing order. |
sign_page, sign_left, sign_bottom |
with position_manual |
number | See Signature placement. |
Cost
Price of the most expensive method in methods × number of signers. The credits are debited when the request is created. Credits of signers who did not sign are refunded when the request is cancelled or stopped.
Response 201 Created
{
"success": true,
"result": {
"id_sign": 4521,
"lang": "en",
"doc_name": "Sales-agreement-2026-114",
"send_email": true,
"from_api": true,
"signature_redirect": "https://www.example.com/thank-you",
"status_id": "pending",
"date_status": "2026-10-20 10:15:02",
"last_reminder": null,
"timezone": "Europe/Brussels",
"credits": 2,
"credits_balance": 38.4,
"signers": {
"1": {
"firstname": "Ann",
"lastname": "Peeters",
"email": "[email protected]",
"phone": "+32475111222",
"sign_first": true,
"status": "pending",
"signature_link": "https://www.e-signature.eu/en/sign/?key=…"
},
"2": {
"firstname": "Marc",
"lastname": "Dubois",
"email": "[email protected]",
"sign_first": false,
"status": "pending",
"signature_link": "https://www.e-signature.eu/en/sign/?key=…"
}
},
"external_reference": "QUOTE-2026-114",
"metadata": {
"crm_deal_id": 8812
}
}
}
7. Signature placement
position_auto (default)
Place the markers #SIG01, #SIG02… (also accepted: #SIG01#) as text in your document, where each signer must sign (#SIG01 = first signer of the signers array). The signature is placed at the top left of the marker. If the same marker appears several times, its first occurrence is used. A marker is skipped when its signature area would overlap a signature already placed (or an existing signature): the signer then takes the next free marker. A marker so close to the page edge that the signature has to be moved is only used as a last resort.
Document not signed yet
If the document contains no marker, or if the marker of a signer is missing, a signature page containing the markers is added at the end of the document and all signatures are placed on this page (6 signers on the first added page, then 10 per additional page).
Document already signed
No page can be added to a PDF that is already signed electronically: it would invalidate the existing signatures. With position_auto, the signatures are placed on the free markers, i.e. the markers whose signature area does not cover an existing signature. Free markers are assigned to the signers in order (#SIG01, #SIG02…).
position_manual.position_manual
Give the position of each signature, in centimetres, for each signer:
| Field | Value |
|---|---|
sign_page |
Page number, starting at 1 (integer) |
sign_left |
Distance from the left edge of the page to the left of the signature, in cm (decimals allowed) |
sign_bottom |
Distance from the bottom edge of the page to the top of the signature, in cm (decimals allowed) |
{ "lastname": "Peeters", "email": "[email protected]", "sign_page": 3, "sign_left": 2.5, "sign_bottom": 6 }
sign_left and sign_bottom give the top left corner of the signature. The signature is placed below and to the right of this point.
The limits depend on the actual size of the page and on the size of the visual stamp: the whole signature must stay at least 2 cm from every edge of the page. When several signing methods are accepted, the largest stamp is used. If a value is out of range, the error message gives the allowed range for that page.
A signature may not overlap another signature of the request, nor an existing signature when the document is already signed.
8. The signature request object
Returned by request, getRequest, cancel, skip, remind, delete and sent in the callbacks.
| Field | Description |
|---|---|
id_sign |
Identifier of the request |
doc_name |
Name of the document |
status_id |
pending in progress · finished signed by everybody · skipped stopped, partially signed · cancelled · deleted |
signers |
Object of the signers, by key ("1", "2"…) — see below |
lang, send_email, signature_redirect, timezone |
As defined at creation |
from_api |
true if the request was created through the API |
date_status |
Date of the last status change (time zone of the request) |
last_reminder |
Date of the last reminder, or null |
credits |
Credits of the request (reserved while pending, used once finished) |
credits_balance |
Current balance of your account |
pdf_file_base64 |
Signed PDF, when the request is no longer pending |
external_reference, metadata |
As sent at creation |
Signer
| Field | Description |
|---|---|
firstname, lastname, email, phone, sign_first |
As sent at creation |
status |
pending · signed · skipped |
signature_link |
Personal signing link, while the signer has not signed |
signing_time, signing_method |
Once signed: date and method used |
verified_firstname, verified_lastname |
Once signed: identity verified by the signing method |
{
"1": {
"firstname": "Ann",
"lastname": "Peeters",
"email": "[email protected]",
"status": "signed",
"signing_time": "2026-10-20 11:02:47",
"signing_method": "itsme-qes-signature",
"verified_firstname": "Ann",
"verified_lastname": "Peeters"
}
}
9. Other endpoints
All endpoints take userid in the URL and x-api-key in the headers. id_sign is passed in the URL.
List the requests
status_id (optional): pending, finished or skipped. external_reference (optional): only the requests with this reference.
{
"success": true,
"result": [
{
"id_sign": 4521,
"status_id": "pending",
"external_reference": "QUOTE-2026-114"
},
{
"id_sign": 4498,
"status_id": "finished"
}
]
}
Get a request
Returns the signature request object, with the signed PDF once the request is finished. To follow the status of a request, use the callbacks rather than repeated calls.
Send a reminder
Sends a reminder email to the signers with status: pending of the group currently invited (sign_first group first). Request pending only. Works also for requests created with send_email: false.
Stop a request (keep the signatures already made)
Possible when at least one signer has signed and at least one has not. The request becomes skipped, the partially signed PDF is available and the credits of the signers who did not sign are refunded.
Cancel a request
Possible as long as nobody has signed; to end a partially signed request, use skip. The response contains the request with status_id: cancelled; the request and its document are then removed from your dashboard, the signing links stop working and the credits are refunded.
Delete a request
Requests finished or skipped only. Deletes the documents and the request. The response contains the request one last time, with status_id: deleted: download the signed PDF before deleting.
Download the audit trail
Not available while the request is pending (HTTP 412). Returns the request object with audit_trail_base64: a PDF with the history of the request (dates, methods, verified identities).
Get the credit balance
{ "success": true, "result": { "credits_balance": 38.4 } }
List the signing methods
Methods accepted in methods, with the price in credits per signer for your account. lang (optional) translates the labels.
{
"success": true,
"result": [
{
"id": "itsme-qes-signature",
"label": "itsme® | QES",
"credits": 1
},
{
"id": "evrotrust-signature",
"label": "Evrotrust | QES",
"credits": 1
},
{
"id": "veriff-advanced-signature",
"label": "Veriff | AES",
"credits": 0.7
},
{
"id": "otp-email-non-qualified",
"label": "OTP by Email/SMS | SES",
"credits": 0.4
}
]
}
10. Callbacks (webhooks)
Define your callback URL in My account > Signatures API (HTTPS, publicly reachable). You can also set a callback_url per request. The callback is a POST with a JSON body containing the full current state of the request (same format as getRequest), sent for requests created through the API.
Event fields
| Field | Description |
|---|---|
event |
Event type (see below) |
event_id |
Unique identifier of the event (evt_…). Identical on every retry of the same event: use it to ignore duplicates. |
event_at |
Date of the event (UTC, ISO 8601) |
changes |
Signer status changes, e.g. [{"signer_key": 1, "from": "pending", "to": "signed"}] (only when a signer changed) |
external_reference, metadata |
As sent when the request was created |
Event types
| Event | When |
|---|---|
signer.signed |
A signer signed, the request is still in progress |
signer.declined, signer.skipped, signer.expired |
A signer declined, was skipped or expired |
request.completed |
All signers signed: the signed PDF is included (pdf_file_base64) |
request.stopped |
The request was stopped: the partially signed PDF is included |
request.cancelled |
The request was cancelled |
request.deleted |
The request was deleted |
request.expired |
The request expired |
request.declined |
Registered mail declined by the recipient |
request.updated |
Any other change: reminder sent, signer modified, new signing links… |
The body always contains the full current state: your integration can rely on status_id and on the status of each signer rather than on the event type alone.
Headers
| Header | Example |
|---|---|
X-Esig-Api-Version |
3 |
X-Esig-Event |
signer.signed |
X-Esig-Event-Id |
evt_4f0c2b9e1d7a3c55e8b1a2f0 |
X-Esig-Delivery-Attempt |
1, 2, … |
X-Esig-Signature |
t=1792407600,v1=5d41402abc4b2a76b9719d911017c592… |
Verifying the signature
Your signing secret is shown in My account > Signatures API, where it can also be regenerated. v1 is the HMAC-SHA256 of "<t>.<raw request body>" with your secret, hex encoded. Use the raw body, before any JSON parsing, and reject callbacks older than 5 minutes.
PHP
$body = file_get_contents( 'php://input' );
$header = $_SERVER['HTTP_X_ESIG_SIGNATURE'] ?? '';
preg_match( '/t=(\d+),v1=([a-f0-9]{64})/', $header, $m );
$valid = $m
&& abs( time() - (int) $m[1] ) <= 300
&& hash_equals( hash_hmac( 'sha256', $m[1] . '.' . $body, $secret ), $m[2] );
Node.js
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const m = /t=(\d+),v1=([a-f0-9]{64})/.exec(header || '');
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}
C# (.NET)
using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions;
static bool Verify(string rawBody, string header, string secret) {
var m = Regex.Match(header ?? "", @"t=(\d+),v1=([a-f0-9]{64})");
if (!m.Success || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(m.Groups[1].Value)) > 300) return false;
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var expected = Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(m.Groups[1].Value + "." + rawBody))).ToLowerInvariant();
return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(expected), Encoding.ASCII.GetBytes(m.Groups[2].Value));
}
Responses and retries
- Reply with HTTP 2xx as soon as the callback is received, and process it asynchronously if needed.
- On network error, timeout (30 s), HTTP 429 or 5xx, the callback is sent again every hour with the same
event_id, up to 24 attempts. Other 4xx answers are considered final. If the request changes again before a successful delivery, the next callback carries a new event and the full current state. The current state remains available at any time throughgetRequest. - Redirects are followed (the
POSTmethod is kept).localhostand private addresses can not be used.
11. Errors
Errors are returned with "success": false and a readable error message. Rely on the HTTP code for your logic: messages may be improved over time.
| HTTP code | Meaning |
|---|---|
| 200 / 201 | OK / request created |
| 400 | Missing or invalid field (the message names the field) |
| 400 | Missing id_sign, or invalid status_id filter |
| 401 | Invalid userid or x-api-key |
| 403 | Not enough credits, or request limit reached (10 per minute) |
| 404 | Unknown id_sign for this account |
| 412 | Action not possible in the current state (e.g. cancel after a signature, delete a pending request) |
Errors by endpoint
| Endpoint | HTTP code | Message | Cause |
|---|---|---|---|
| All endpoints | 401 | Please provide a valid x-api-key in headers & userid in url parameters. |
Invalid userid or x-api-key |
| All endpoints | 403 | Rate limit exceeded: 10 requests per minute is allowed. Please try again after a minute. |
More than 10 requests per minute |
request |
400 | See Validation messages | Missing or invalid field, PDF or position |
request |
403 | You do not have enough credits |
Balance lower than the cost of the request |
list |
400 | Incorrect value for status_id. |
Unknown status_id filter |
getRequest, remind, skip, cancel, delete, requestAuditTrail |
400 | please provide id_sign parameter. |
id_sign missing |
getRequest, remind, skip, cancel, delete, requestAuditTrail |
404 | invalid id_sign value. |
Unknown id_sign for this account |
remind |
412 | request can not be remind to user(s): wrong status |
The request is not pending |
skip |
412 | request can not be skipped: wrong status |
Nobody has signed yet, everybody has signed, or the request is not pending |
cancel |
412 | request can not be cancelled: someone already signed the PDF |
At least one signer has signed |
delete |
412 | request can not be deleted: wrong status_id |
The request is still pending |
requestAuditTrail |
412 | Can not download Audit Trail while the signature request is pending |
The request is still pending |
Validation messages on creation (request, HTTP 400)
The message names the field or the problem; … stands for a value of your request. Rely on the HTTP code in your code, and show the message to help correct the request.
Show all validation messages
Request body
Request body can not be empty
title, lang
Field 'title' must be of type string and can not be emptyField 'lang' is an invalid language code
signers
field 'signers' is omitted or is emptyFirstname, Lastname & email address must be of type string and can not be empty… is not an email addressfield 'sign_first' is not a boolean valuefield 'sign_first' can not be 'true' when field 'send_email' is 'false'
methods
You must set at least one signing method… is not a valid method
Options
Field 'reminder' should be of type numeric between 0 & 14 (days)Field 'send_email' should be of type booleanField 'email_notification' should be of type booleanField 'note_html' should be of type stringField 'signature_redirect' is not a valid url
external_reference, metadata, callback_url
Field 'external_reference' must be a stringField 'external_reference' can not exceed 255 charactersField 'metadata' must be a JSON objectField 'metadata' can not exceed 4096 bytesField 'callback_url' must be a publicly reachable HTTPS URL
Field 'pdf_file_base64' should be of type string and not emptyField 'pdf_file_base64' doesn't contains a base64 encoded stringField 'pdf_file_base64' doesn't contains a base64 encoded PDF fileYour file is too big > 30 MBCouldn't load your PDF documentPDF file is encrypted and password protected. Please unprotected your PDF file.Couldn't get pages from PDF document
Signature placement
field 'signature_place' is incorrectfield 'sign_page' is empty or is not an integerfield 'sign_left' is empty or is not a number (cm)field 'sign_bottom' is empty or is not a number (cm)Field 'sign_left' for signer … must be between … and … cm (page … is … x … cm, signature … x … cm, margin … cm)Field 'sign_bottom' for signer … must be between … and … cm (…)Field 'sign_page' for signer … is too big: the document has … page(s)Page … is too small for the signature of … (…)The signature of … on page … overlaps an existing signature of the document (…)The signatures of … and … overlap on page … (…)The existing signatures of the document could not be read: the position of the signature of … can not be checked
Document already signed (position_auto)
Your PDF file is already signed: pages can not be added without invalidating the existing signatures. It contains … free marker(s) for … signer(s): place the signatures manually (signature_place: position_manual).Your PDF file is already signed: pages can not be added without invalidating the existing signatures. The free markers could not be determined: place the signatures manually (signature_place: position_manual).
Processing
Couldn't extract markers from PDF document: …Unknown error: Request could not be saved into the Database- Error message returned by the signature provider (eID Easy)
12. Retention
Signed documents can be downloaded for 30 days after completion, through getRequest or the request.completed callback. The audit trail is kept for the period defined in our DPA. Archive the signed document and the audit trail on your side within 30 days.
13. Changelog
API v3 — differences with API v2
API v2 (https://api.e-signature.eu/v2/signature/) remains available and unchanged. Endpoints, parameters and authentication are identical in both versions: to move to API v3, replace the base URL with https://api.e-signature.eu/v3/signature/. You can switch endpoint by endpoint.
The previous URLs (https://www.e-signature.eu/wp-json/signature/ for API v2 and https://www.e-signature.eu/wp-json/esig/v3/signature/ for API v3) remain valid. We recommend using the api.e-signature.eu URLs.
| Topic | API v2 | API v3 |
|---|---|---|
| Base URL | /v2/signature/ |
/v3/signature/ |
| Your own references | — | New fields external_reference and metadata, returned in every response and callback; list filter by external_reference |
| Callback URL | One URL per account | Also per request (callback_url) |
| Callback content | Full state of the request | Full state of the request + event, event_id, event_at, changes |
| Callback security | — | HMAC-SHA256 signature in the X-Esig-Signature header |
| Callback retries | Every hour on network error, until received | Every hour on network error, timeout, HTTP 429 or 5xx, up to 24 attempts, same event_id |
Missing id_sign, invalid status_id filter |
HTTP 401 | HTTP 400 |
Unknown id_sign |
HTTP 401 | HTTP 404 |
| Audit trail of a pending request | HTTP 401 | HTTP 412 |
Manual placement: sign_left / sign_bottom |
Fixed limits for an A4 portrait page (0 to 15 cm / 4 to 28.7 cm), signature height 2.5 cm | Limits computed from the actual page size and the stamp size of the accepted methods, 2 cm margin; decimals accepted; the error message gives the allowed range |
| Manual placement: overlaps | Not checked | Refused if a signature overlaps another signature of the request or an existing signature of the document |
Manual placement: sign_page |
A page number one higher than the last page was not refused | Any page that does not exist is refused |
Callbacks follow the version used to create the request: a request created with API v2 keeps receiving API v2 callbacks.