Signaturios API
Signaturios is a developer-first REST API for legally-binding electronic signatures. POST a base64-encoded PDF with signer details; get back a hosted signing URL in one call. When the signer completes the flow, you receive a PAdES-signed PDF with a cryptographic audit trail — no SDK, no enterprise sales call, no per-envelope pricing.
Two signature tiers are supported out of the box: SES (email-verified consent, US ESIGN Act) and AES-eID (government-issued national eID, eIDAS Advanced Electronic Signature) with seven European identity methods.
https://esign-api-gt01.onrender.com
Authentication
All developer endpoints require an API key passed as a Bearer token.
Keys are prefixed esign_live_ and remain valid until revoked.
Get a key
Create a free account at /signup — your first API key is generated on the spot and shown once. Additional keys and revocation are available in the dashboard.
Send the key
Include it on every request to a developer endpoint:
Authorization: Bearer esign_live_...
$ curl https://esign-api-gt01.onrender.com/health \
-H "Authorization: Bearer esign_live_Ue0GGV8..."
{ "status": "ok", "service": "esign-api", "version": "0.1.0" }
Quickstart
From zero to a legally-binding signed PDF in four steps.
1. Create a signature request
POST the document (base64-encoded PDF), signer info, and the signature tier.
The response contains a signing_url — a hosted page where
the signer reviews and signs the document. Email it to them; you don't build any frontend.
$ curl -X POST https://esign-api-gt01.onrender.com/v1/signature-requests \
-H "Authorization: Bearer esign_live_..." \
-H "Content-Type: application/json" \
-d '{
"document_base64": "'"$(base64 -i contract.pdf)"'",
"document_title": "Service Agreement",
"signer": { "name": "Jane Doe", "email": "jane@example.com" },
"tier": "ses"
}'
import base64, requests
API_KEY = "esign_live_..."
BASE = "https://esign-api-gt01.onrender.com"
with open("contract.pdf", "rb") as f:
doc_b64 = base64.b64encode(f.read()).decode()
resp = requests.post(
f"{BASE}/v1/signature-requests",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"document_base64": doc_b64,
"document_title": "Service Agreement",
"signer": {"name": "Jane Doe", "email": "jane@example.com"},
"tier": "ses",
},
)
data = resp.json()
print("Signing URL:", data["signing_url"])
{
"request_id": "req_01HKx9mP3Qrt7wbA",
"signing_url": "https://esign-api-gt01.onrender.com/sign/req_01HKx9mP3...",
"status": "pending",
"tier": "ses"
}
2. Choose the signature tier
Set "tier" on the request body:
| tier value | Name | Identity verification | Legal standard |
|---|---|---|---|
ses (default) |
Simple Electronic Signature | Email link + consent checkbox + IP capture | US ESIGN Act · eIDAS SES |
aes_eid |
Advanced Electronic Signature | Government-issued national eID via OIDC | eIDAS AES |
For aes_eid, also set "eid_method"
to pre-select the signer's national eID. If omitted, the signer is shown a selector page
to choose their own.
eID methods (aes_eid tier)
Seven European national identity methods are supported. Pass the key as "eid_method":
| eid_method | eID provider | Country |
|---|---|---|
se_bankid | Swedish BankID | Sweden |
dk_mitid | Danish MitID | Denmark |
no_bankid | Norwegian BankID | Norway |
fi_ftn | Finnish Trust Network | Finland |
nl_idin | Dutch iDIN | Netherlands |
de_personalausweis | German Personalausweis | Germany |
se_freja | Swedish Freja eID | Sweden |
{
"document_base64": "JVBERi0x...",
"signer": { "name": "Erik Svensson", "email": "erik@example.se" },
"tier": "aes_eid",
"eid_method": "se_bankid"
}
3. Send the signing URL to your signer
Email signing_url to the signer. Signaturios serves the entire
hosted signing experience — you build nothing. For the SES tier, the signer reviews the
document inline and checks a consent box. For AES-eID, the signer authenticates through
their national eID provider before the signature is applied. The signed PDF is produced
automatically once the flow completes.
4. Poll for completion and retrieve the signed document
$ curl https://esign-api-gt01.onrender.com/v1/signature-requests/req_01HKx9mP3Qrt7wbA \
-H "Authorization: Bearer esign_live_..."
import time, base64
request_id = data["request_id"]
while True:
r = requests.get(
f"{BASE}/v1/signature-requests/{request_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
).json()
if r["status"] == "signed":
pdf = base64.b64decode(r["signed_document_base64"])
with open("signed.pdf", "wb") as f:
f.write(pdf)
print("Done. Audit trail:", r["audit_trail"])
break
time.sleep(5) # poll every 5 s
{
"request_id": "req_01HKx9mP3Qrt7wbA",
"status": "signed",
"tier": "ses",
"created_at": "2026-06-18T09:00:00Z",
"signer": { "name": "Jane Doe", "email": "jane@example.com" },
"signed_at": "2026-06-18T09:04:32Z",
"captured_ip": "203.0.113.42",
"signed_document_base64": "JVBERi0x...",
"audit_trail": {
"signer": {
"name": "Jane Doe",
"identifier": "jane@example.com",
"verification_method": "email_link",
"tier": "ESIGN (US) / eIDAS SES",
"ip_address": "203.0.113.42"
},
"intent_to_sign": true,
"consent_to_electronic": true,
"timestamp_utc": "2026-06-18T09:04:32Z",
"document": {
"sha256_before_signing": "a3b4c5d6e7f8a9b0...",
"sha256_after_signing": "c1d2e3f4a5b6c7d8..."
},
"signature_standard": "PAdES (ETSI EN 319 142) / ETSI.CAdES.detached"
}
}
signer.verification_method will be the display name
of the eID provider (e.g. "Swedish BankID"), and the signer's verified legal name
is used in place of the developer-supplied name.
Two additional fields — eid_method and country — are added
to the signer object.
API Reference
Create a signature request
Creates a pending signing request and returns a one-time signing_url
to deliver to the signer. The document and all signer metadata are stored server-side;
only the URL needs to be sent.
Request body
| Field | Type | Description | |
|---|---|---|---|
document_base64 |
string | Required | Base64-encoded PDF. Must begin with the PDF magic bytes (%PDF). |
signer.name |
string | Required | Signer's full name. Shown on the signing page and embedded in the audit trail. |
signer.email |
string | Required | Signer's email. Embedded in the audit trail for the SES tier. |
document_title |
string | Optional | Human-readable name displayed on the signing page. |
tier |
string | Optional |
"ses" (default) or "aes_eid".
Legacy value "aes_bankid" is accepted and silently remapped
to aes_eid + se_bankid for backward compatibility.
|
eid_method |
string | Optional |
Pre-selects the national eID for the aes_eid tier.
One of: se_bankid, dk_mitid, no_bankid,
fi_ftn, nl_idin, de_personalausweis,
se_freja. If omitted the signer chooses on the signing page.
|
anchor |
object | Optional |
Text-anchor placement. Embed anywhere in your PDF
and the visible signature appearance will be placed at that spot.
If the tag is not found the signature is applied invisibly at the default
position and audit_trail.anchor_placement.found will be
false. See Anchor placement for details.
|
Response — 201 Created
| Field | Description |
|---|---|
request_id | Opaque token — use this to poll status. |
signing_url | Hosted signing page URL to send to the signer. Single-use per signing session. |
status | Always "pending" at creation time. |
tier | Resolved tier: "ses" or "aes_eid". |
eid_method | Present only when an eid_method was specified or inferred. |
Get a signature request
Returns the current status of a signature request.
When status is "signed", the response
also includes the PAdES-signed PDF (base64) and the complete audit trail.
| Field | Present | Description |
|---|---|---|
request_id | Always | The request identifier. |
status | Always | "pending" or "signed". |
tier | Always | Signature tier used. |
created_at | Always | ISO 8601 UTC timestamp. |
signer | Always | Object with name and email. |
eid_method | AES only | National eID method key. |
signed_at | When signed | ISO 8601 UTC timestamp of signing. |
captured_ip | When signed | Signer's IP address at time of signing. |
signed_document_base64 | When signed | Base64-encoded PAdES-signed PDF. |
audit_trail | When signed | Full audit trail object (see Quickstart for complete schema). |
Anchor placement
Drop the text tag anywhere in your PDF
and pass an anchor object in your request. The visible
signature stamp is placed at that location, covering the tag.
The PAdES cryptographic signature is always applied to the whole document regardless.
Anchor placement works for both POST /v1/signature-requests
(hosted signing) and POST /v1/signatures (direct),
and for both SES and AES tiers.
Anchor object fields
| Field | Type | Default | Description |
|---|---|---|---|
text |
string | "" |
Exact text to find in the PDF. Case-sensitive. Must appear as a continuous run on one line. |
x_offset |
number | 0 |
Horizontal offset in points from the anchor's left edge. Positive = right. |
y_offset |
number | 0 |
Vertical offset in points from the anchor's bottom edge. Positive = up. |
width |
number | 200 |
Width of the signature appearance box in points (72 pt = 1 inch). |
height |
number | 60 |
Height of the signature appearance box in points. |
Behavior
- Tag found: Stamp is placed at the first occurrence.
audit_trail.anchor_placement.foundistrue. If multiple occurrences exist,occurrencesshows the count and anoteis added. - Tag not found: Signing proceeds with an invisible PAdES signature
at the default field position.
audit_trail.anchor_placement.foundisfalse. - No
anchorfield: Existing invisible-signature behavior. Noanchor_placementkey in the audit trail. Fully backward-compatible.
$ curl -X POST https://esign-api-gt01.onrender.com/v1/signature-requests \
-H "Authorization: Bearer esign_live_..." \
-H "Content-Type: application/json" \
-d '{
"document_base64": "...",
"signer": { "name": "Jane Doe", "email": "jane@example.com" },
"anchor": { "text": "", "width": 220, "height": 70 }
}'
resp = requests.post(
f"{BASE}/v1/signature-requests",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"document_base64": doc_b64,
"signer": {"name": "Jane Doe", "email": "jane@example.com"},
"anchor": {"text": "", "width": 220, "height": 70},
},
)
Direct signing (server-to-server)
Signs a PDF immediately and returns the signed document synchronously — no hosted signing page, no signer URL, no email. Use this when the signer's identity is already established in your system (e.g., they authenticated via your own login flow) and you need a programmatic SES signature embedded server-side.
POST /v1/signature-requests instead.
Request body
| Field | Type | Description | |
|---|---|---|---|
document_base64 |
string | Required | Base64-encoded PDF. |
signer.name |
string | Required | Signer name — embedded in the PAdES audit trail. |
signer.identifier |
string | Required | Email or unique identifier — embedded in the audit trail as identifier. |
anchor |
object | Optional | Text-anchor placement. Same schema as for POST /v1/signature-requests.
See Anchor placement. |
Response — 200 OK
| Field | Description |
|---|---|
status | Always "signed". |
signed_document_base64 | Base64-encoded PAdES-signed PDF. |
audit_trail | Full audit trail object (same schema as above). |
$ curl -X POST https://esign-api-gt01.onrender.com/v1/signatures \
-H "Authorization: Bearer esign_live_..." \
-H "Content-Type: application/json" \
-d '{
"document_base64": "'"$(base64 -i contract.pdf)"'",
"signer": {
"name": "Jane Doe",
"identifier": "jane@example.com"
}
}'
import base64, requests
with open("contract.pdf", "rb") as f:
doc_b64 = base64.b64encode(f.read()).decode()
resp = requests.post(
f"{BASE}/v1/signatures",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"document_base64": doc_b64,
"signer": {"name": "Jane Doe", "identifier": "jane@example.com"},
},
)
data = resp.json()
signed_pdf = base64.b64decode(data["signed_document_base64"])
Embedded signing
Embedded signing lets you mount the signing UI directly inside a
<div> on your own page, without
redirecting the user to an external hosted URL.
A lightweight iframe widget communicates with your app via
postMessage events so you can react to
completion without polling.
signing_url remains the default and is always returned.
Embedded mode is opt-in — add "embedded": true to your request to
also receive an embed_url.
Step 1 — Create an embedded session
Add "embedded": true and optionally
"allowed_origins" to your
POST /v1/signature-requests call.
The response includes a short-lived embed_url
(valid for 1 hour) alongside the usual signing_url.
| Field | Type | Description | |
|---|---|---|---|
embedded |
boolean | Optional | Set to true to receive an embed_url in the response. |
allowed_origins |
string[] | Optional |
Array of https:// origins allowed to embed the widget, e.g.
["https://app.yourdomain.com"].
Used to set Content-Security-Policy: frame-ancestors on the widget.
Omit to allow embedding from any origin (fine for development;
restrict in production to prevent clickjacking).
|
Response additions when embedded is true
| Field | Description |
|---|---|
embed_url | Short-lived URL for the signing widget. Pass this to SignaturiosEmbed.init(). Valid for 1 hour. |
embed_expires_at | ISO 8601 UTC expiry of the embed session. |
Step 2 — Include the embed SDK
Drop a container element and include the SDK. Call
SignaturiosEmbed.init() with your
embed_url. The widget mounts as an
auto-resizing iframe inside the container.
<!-- 1. A container for the signing widget -->
<div id="sign-widget" style="width:100%;max-width:720px;border:1px solid #e5e5ea;border-radius:8px;overflow:hidden"></div>
<!-- 2. Embed SDK (served from your API domain) -->
<script src="https://esign-api-gt01.onrender.com/static/js/esign-embed.js"></script>
<!-- 3. Mount the widget -->
<script>
var signer = SignaturiosEmbed.init({
container: '#sign-widget',
embedUrl: '', // from your server-side API response
onLoaded: function(e) {
console.log('Widget ready');
},
onSigned: function(e) {
console.log('Signed!', e.request_id, e.signed_at);
signer.unmount(); // remove the widget
showSuccessMessage(); // your own UI
},
onError: function(e) {
console.error('Error:', e.message);
},
onCancelled: function() {
console.log('Cancelled');
},
});
</script>
postMessage events
The SDK abstracts the raw events into callbacks, but you can also listen
directly on window if needed. All events have a
type field prefixed with esign:.
| Event type | Payload fields | Description |
|---|---|---|
esign:loaded |
— | Widget iframe has initialised and is ready. |
esign:signed |
request_id, signed_at |
Document was signed. signed_at is ISO 8601 UTC. |
esign:error |
message |
A signing error occurred. The widget may still be usable for retry. |
esign:cancelled |
— | User explicitly cancelled the signing session. |
esign:resize |
height (number, px) |
Widget content height changed. The SDK resizes the iframe automatically. |
esign:eid-popup-required |
url |
Internal: SDK opens eID auth in a popup. Handled automatically by the SDK. |
Refreshing an expired session
Embed sessions expire after 1 hour. If the signer hasn't completed by then, regenerate the session server-side and reinitialise the widget with the new URL.
| POST /v1/signature-requests/{id}/embed-session | |
|---|---|
| Auth | API key required |
| Body (optional) | { "allowed_origins": ["https://…"] } to update the allow-list |
| Returns | embed_url, embed_expires_at |
eID (AES) tier in embedded mode
The widget handles this automatically: when the user clicks an eID button,
the SDK opens the authentication in a pop-up window (not inside
the iframe). After the user completes eID verification in the pop-up, the window
closes and fires esign:signed directly to your page — no
polling needed.
You must allow pop-ups from your domain. Tell your users to permit pop-ups when prompted. For best UX, trigger the pop-up from a button click so browsers don't block it as an unsolicited pop-up.
The widget shows a "waiting for authentication" spinner while the pop-up is open. If the user closes the pop-up without completing, the widget resets and allows retry.
Domain allow-listing
Setting allowed_origins restricts which pages can
embed the widget using the browser's
Content-Security-Policy: frame-ancestors directive.
Browsers will block the iframe on any unlisted origin, preventing clickjacking attacks.
- For development: omit
allowed_origins— the widget can be embedded anywhere. - For production: set
allowed_origins: ["https://app.yourdomain.com"]. - Multiple origins are supported:
["https://app.yourdomain.com", "https://staging.yourdomain.com"].
Error responses
All errors use the same JSON envelope:
{
"error": {
"message": "Invalid or inactive API key.",
"status": 401
}
}
| Status | Cause |
|---|---|
400 |
Missing or invalid field — e.g. document_base64 is not valid base64,
does not decode to a PDF, unknown tier or eid_method,
or required fields (signer.name, signer.email) are absent.
|
401 |
Missing Authorization header, invalid key format, or revoked key.
Response includes WWW-Authenticate: Bearer realm="esign-api".
|
402 |
Free allowance exhausted and no payment method on file. Add billing at
/billing/setup. Test keys always bypass this check.
|
404 |
The request_id does not exist. |
503 |
The aes_eid tier is requested but the server's eID provider
credentials are not configured (IDURA_DOMAIN,
IDURA_CLIENT_ID, IDURA_CLIENT_SECRET).
|
Webhooks
Instead of polling GET /v1/signature-requests/{id}, register
a webhook endpoint and receive a signed HTTP POST the moment a document is signed.
Every delivery is HMAC-SHA256 signed so you can verify it originated from Signaturios.
Webhooks respect the live/test split: test-key requests trigger test webhooks; live-key requests trigger live webhooks. Register separate endpoints per environment in the dashboard.
Register an endpoint
Go to your dashboard, scroll to Webhook Endpoints, enter your HTTPS URL, choose the environment, and click Register endpoint. The signing secret is shown once — copy it to your secrets manager before leaving the page.
The endpoint must be publicly reachable and return a 2xx
response within 10 seconds. Signaturios will retry up to 5 times on failure (see
Retries).
Managing endpoints via the dashboard
Each registered endpoint shows its URL, environment badge, and a Disable button. Disabled endpoints stop receiving events; re-register if you need to replace one.
Event types
| Event | Fired when |
|---|---|
signature_request.completed |
The signer has completed the flow and the PAdES signature has been applied to the document. The full audit trail is included in the payload. |
GET /v1/signature-requests/{id} continues to return the
current status and audit trail at any time.
Payload shape
Signaturios POSTs a JSON body to your endpoint. The top-level envelope wraps
the full GET /v1/signature-requests/{id} object under
data, including the structured audit trail.
{
"event": "signature_request.completed",
"created": "2026-06-18T20:00:00Z",
"livemode": true,
"data": {
"request_id": "Voh6hSbwxDUCIMc30vb...",
"livemode": true,
"status": "signed",
"tier": "ses",
"created_at": "2026-06-18T19:58:00Z",
"signed_at": "2026-06-18T20:00:00Z",
"captured_ip": "185.1.2.3",
"signer": { "name": "Jane Doe", "email": "jane@example.com" },
"audit_trail": {
"document": { "sha256": "a3f...", "pages": 3 },
"signer": {
"name": "Jane Doe",
"identifier": "jane@example.com",
"verification_method": "email_link",
"tier": "ESIGN (US) / eIDAS SES",
"ip": "185.1.2.3"
},
"signed_at": "2026-06-18T20:00:00Z",
"signature": { "algorithm": "RSA-SHA256", "subfilter": "ETSI.CAdES.detached" }
},
"signed_document_base64": "JVBERi0x..."
}
}
Request headers
| Header | Value |
|---|---|
Content-Type |
application/json |
X-Signaturios-Signature |
sha256=<hex> — HMAC-SHA256 of the raw request body |
X-Signaturios-Timestamp |
ISO 8601 UTC timestamp of this delivery attempt |
User-Agent |
Signaturios-Webhooks/1.0 |
Verify the signature
Every delivery includes an X-Signaturios-Signature header of the
form sha256=<hex>.
The hex value is HMAC-SHA256 of the raw request body using your
endpoint's signing secret as the key.
Always verify this before processing the event.
Use constant-time comparison (hmac.compare_digest)
to prevent timing attacks.
import hmac, hashlib
def verify_esign_webhook(secret: str, body: bytes, header: str) -> bool:
"""Return True only if the delivery is authentic."""
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
# Flask handler example
@app.post("/webhooks/esign")
def handle_esign_webhook():
sig = request.headers.get("X-Signaturios-Signature", "")
if not verify_esign_webhook(WEBHOOK_SECRET, request.data, sig):
abort(401) # reject unknown origin
event = request.get_json()
if event["event"] == "signature_request.completed":
data = event["data"]
audit = data["audit_trail"]
signed_pdf = base64.b64decode(data["signed_document_base64"])
# ... store audit + PDF ...
return "", 200 # acknowledge within 10 s
Retries & delivery guarantees
If your endpoint returns a non-2xx response or does not respond within 10 seconds, Signaturios retries up to 5 attempts with exponential backoff:
| Attempt | Delay before this attempt |
|---|---|
| 1 | Immediately after the event |
| 2 | 10 seconds |
| 3 | 30 seconds |
| 4 | 60 seconds |
| 5 | 120 seconds |
Each attempt is logged (endpoint, event type, HTTP status, success/failure). After 5 failures the delivery is marked failed and no further retries occur. The HMAC signature is identical across all retries for the same event, so you can verify any attempt with the same code path.
To avoid double-processing, treat deliveries as at-least-once:
use data.request_id as an idempotency key in your handler.
GET /v1/signature-requests/{id} to reconcile any gaps.
Pricing & billing
Usage-based per-completed-signature pricing. You are only ever charged for work that actually happened — a signer opened the link, consented, and the PDF was cryptographically signed. Failed attempts, expired links, and test-mode signatures are always free.
| Tier | Price per completed signature | Trigger |
|---|---|---|
ses — Email consent (SES) |
€0.25 | Signer checks consent box & submits |
aes_eid — National eID (AES) |
€0.75 | eID provider confirms identity & PDF is signed |
Free allowance
Every developer account includes 10 completed live signatures free — enough to fully test your integration end-to-end with real documents before spending anything. The free allowance never expires.
Test mode is always free
Requests made with a esign_test_… key are never billed, never
counted against the free allowance, and never require a payment method. Use test keys
freely in CI, staging, and development.
What is never billed
- Signatures made with a
esign_test_…key - Pending or expired signing requests that were never completed
- Signing attempts that returned an error (PDF rejected, eID auth failed, etc.)
- API calls that returned 4xx / 5xx before signing began
How billing works
Add a payment method via your dashboard. Each completed live signature above the free allowance is posted as a Stripe invoice line-item immediately. Stripe collects all items into a monthly invoice and charges your card automatically. You can view past invoices and update your card via the Stripe Customer Portal (linked from the dashboard).
HTTP 402 Payment Required with the standard error
envelope and a link to /billing/setup.
Rate limits
Limits are enforced per API key. Test keys and live keys have independent
buckets. Signing pages (/sign/…) and the health endpoint are
not rate-limited so real signers are never blocked.
| Endpoint | Per minute | Per day |
|---|---|---|
POST /v1/signature-requests |
10 | 500 |
POST /v1/signatures |
10 | 500 |
GET /v1/signature-requests/{id} |
120 | — |
429 response
When a limit is exceeded the API returns HTTP 429 with the
standard error envelope and a Retry-After header (seconds
until the current window resets).
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json
{
"error": {
"message": "Rate limit exceeded: 10 per 1 minute.",
"status": 429
}
}
X-RateLimit-Limit, X-RateLimit-Remaining,
and X-RateLimit-Reset headers on every response so you can track consumption
proactively before hitting the limit.
To switch to Redis-backed limiting in production, set the
RATELIMIT_STORAGE_URI environment variable
(e.g. redis://localhost:6379).
How It Works & Legal Validity
Understanding the underlying mechanism helps you explain the legal strength of these signatures to your customers — and why they're materially stronger than a JPEG stamped onto a PDF.
Every document is signed using PAdES — PDF Advanced Electronic Signatures (ETSI EN 319 142), the same standard used by European governments and courts. The signature is a cryptographic proof embedded in the PDF's binary structure using CAdES-detached encoding (SHA-256). Any modification to the document after signing is mathematically detectable by any PDF reader — Adobe Acrobat, Preview, or any ETSI-compliant validator — without calling any external service.
Every signature embeds a structured audit trail directly in the signed PDF. It records: signer identity (name + email or masked eID reference), verification method, IP address, timestamp, explicit consent flags, and SHA-256 hashes of the document both before and after signing. The trail is part of the cryptographic proof — it cannot be modified without invalidating the signature. The document is self-contained and remains verifiable indefinitely.
The Electronic Signatures in Global and National Commerce Act (15 U.S.C.
§ 7001) grants electronic signatures the same legal weight as ink when the
signer demonstrates intent and consents to electronic execution. The SES tier satisfies
this by capturing explicit consent via checkbox, intent via a click-to-sign action,
the signer's email-verified identity, and a timestamp — all embedded in the PAdES
audit trail as intent_to_sign: true and
consent_to_electronic: true.
eIDAS Regulation (EU 910/2014) defines Advanced Electronic Signatures as requiring uniquely-identified signatories through government-issued credentials. The AES-eID tier routes the signer through their national eID provider (BankID, MitID, iDIN, FTN and others), then embeds their verified legal name, masked national ID reference, and eID method in the PAdES audit trail. This creates an AES under eIDAS Article 26 — the highest level below QES, and the strongest electronic signature accepted across EU member states without a qualified certificate.