For developers
API Documentation
The Signeur REST API lets your application create signature requests, monitor their status, and download locked PDFs. This documentation describes endpoints implemented today.
https://signeur.euAuthentication
All API calls require a Bearer token in the Authorization header. Tokens are hashed with SHA-256 and compared against the stored hash — plaintext is never persisted anywhere.
Example:
Authorization: Bearer sgn_live_xxx...Obtain an API key from the service administrator. Key format is sgn_live_<random>.
Error responses
All errors are returned as JSON with error and message fields. HTTP status codes follow conventions.
{
"error": "invalid_signer_email",
"message": "Field 'signer_email' is missing or not a valid email."
}| Code | Meaning |
|---|---|
400 | Bad request (validointi ei mene läpi) |
401 | Authorization-otsikko puuttuu tai Bearer-token virheellinen |
404 | Resurssia ei löytynyt tai se kuuluu toiselle organisaatiolle |
409 | Konflikti: pyyntö on jo allekirjoitettu / ei vielä allekirjoitettu |
410 | Resurssi on vanhentunut (linkki yli 30 päivää vanha) |
429 | Liian monta pyyntöä — odota Retry-After-otsikon ilmoittama aika |
500 | Palvelinvirhe (DB-, Storage- tai email-välitysvirhe) |
Rate limits
Per-organization rate limiting is not yet enforced. Use the API responsibly — misuse may lead to API key revocation. Volume-based caps will be added later.
Idempotency
All POST endpoints accept an optional `Idempotency-Key` header (any unique string, typically a UUID). When you retry a request with the same key, Signeur replays the original response instead of creating a duplicate. Keys are scoped per organization and retained for 24 hours.
POST /api/v1/signatures
Authorization: Bearer sgn_live_...
Idempotency-Key: 0d8a2c41-7e3f-4b9d-bc59-2c5fd8a91f4d
Content-Type: application/json
{ … }If you send the same key with a different request body, Signeur returns 409 idempotency_key_conflict. Replayed responses include an `Idempotent-Replayed: true` header so you can detect them client-side.
Endpoints
Create a signature request
Creates a new signature request (1–20 signers), stores the PDF in Storage, and sends an invitation email to each signer.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | yes | Title of the request (shown in the invitation email). |
message | string | no | Optional message shown to each signer. |
signers[] | array | yes | Array of 1–20 signer objects. At least one is required. |
signers[].email | string | yes | The signer's email address. |
signers[].name | string | no | The signer's name (shown on the audit page). |
signers[].locale | string | no | Per-signer email locale: 'en' | 'fi' | 'fr' | 'de'. Falls back to the top-level locale, then 'en'. |
locale | string | no | Default locale applied to signers that don't specify their own. Allowed values: 'en' | 'fi' | 'fr' | 'de'. |
document.filename | string | yes | Original filename of the PDF. |
document.content_base64 | string | yes | PDF content as a base64-encoded string (max 10 MB decoded). |
metadata | object | no | Free-form key-value object stored verbatim on the request. |
Backward compatibility: the legacy single-signer body { "signer_email", "signer_name" } is still accepted and treated as a one-element signers array. New integrations should use the signers[] array.
Request body
curl -X POST https://signeur.eu/api/v1/signatures \
-H "Authorization: Bearer sgn_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Order Agreement #1234",
"message": "Please review and sign.",
"locale": "en",
"signers": [
{ "email": "buyer@example.com", "name": "Anna Buyer" },
{ "email": "seller@example.com", "name": "Bob Seller", "locale": "fi" }
],
"document": {
"filename": "contract.pdf",
"content_base64": "JVBERi0xLjQK..."
},
"metadata": { "deal_id": "D-42" }
}'Response
{
"id": "6dc18911-f66b-43d6-9810-8de0b07bab04",
"status": "sent",
"expires_at": "2026-06-13T10:51:51.813+00:00",
"signers": [
{
"id": "9c0aa5ec-…",
"email": "buyer@example.com",
"name": "Anna Buyer",
"access_token": "29ErQSUr8Ov…",
"sign_url": "https://signeur.eu/sign/29ErQSUr8Ov…",
"email_sent": true,
"email_error": null,
"locale": "en"
},
{
"id": "f12d8b3c-…",
"email": "seller@example.com",
"name": "Bob Seller",
"access_token": "kpqDAuVy9-…",
"sign_url": "https://signeur.eu/sign/kpqDAuVy9-…",
"email_sent": true,
"email_error": null,
"locale": "fi"
}
]
}Get signature request status
Returns the current state of the request, the full signers array and the audit trail. Use for status polling.
Request body
curl https://signeur.eu/api/v1/signatures/<id> \
-H "Authorization: Bearer sgn_live_..."Response
{
"id": "6dc18911-f66b-43d6-9810-8de0b07bab04",
"status": "signed",
"title": "Order Agreement #1234",
"created_at": "2026-05-14T10:51:51.813Z",
"sent_at": "2026-05-14T10:51:52.012Z",
"signed_at": "2026-05-14T10:52:24.374Z",
"cancelled_at": null,
"expires_at": "2026-06-13T10:51:51.813Z",
"is_expired": false,
"signers": [
{
"id": "9c0aa5ec-…",
"email": "buyer@example.com",
"name": "Anna Buyer",
"access_token": "29ErQSUr8Ov…",
"sign_url": "https://signeur.eu/sign/29ErQSUr8Ov…",
"status": "signed",
"sent_at": "2026-05-14T10:51:52.012Z",
"opened_at": "2026-05-14T10:52:00.481Z",
"signed_at": "2026-05-14T10:52:24.374Z",
"declined_at": null,
"decline_reason": null,
"locale": "en"
}
],
"document_hash": "e5132c2fe36dfa219ec600b009d16ef38d20bf0849f7b01ba5ca6e22fec63b56",
"audit_trail": { "events": [ … ] },
"metadata": { "deal_id": "D-42" }
}Download signed PDF
Returns the locked PDF as binary. Returns 409 if the request is not yet signed.
Request body
curl https://signeur.eu/api/v1/signatures/<id>/document \
-H "Authorization: Bearer sgn_live_..." \
-o signed.pdfContent-Type: application/pdf
List signature requests
Returns a paginated list of the organization's signature requests with optional filters.
| Field | Type | Required | Description |
|---|---|---|---|
status | query | no | Filter by status: pending | sent | opened | signed | cancelled | declined | expired | awaiting_stamp | stamp_failed. |
created_from | query | no | Only return requests created at or after this ISO-8601 timestamp. |
created_to | query | no | Only return requests created before this ISO-8601 timestamp. |
limit | query | no | Page size, 1–100. Default 25. |
cursor | query | no | Opaque cursor returned by a previous response's next_cursor. Do not construct your own. |
Request body
curl "https://signeur.eu/api/v1/signatures?status=sent&limit=10" \
-H "Authorization: Bearer sgn_live_..."Response
{
"data": [
{
"id": "6dc18911-f66b-43d6-9810-8de0b07bab04",
"status": "sent",
"title": "Order Agreement #1234",
"signer_count": 2,
"created_at": "2026-05-14T10:51:51.813Z",
"sent_at": "2026-05-14T10:51:52.012Z",
"signed_at": null,
"cancelled_at": null,
"expires_at": "2026-06-13T10:51:51.813Z",
"is_expired": false
}
],
"has_more": true,
"next_cursor": "eyJjIjoiMjAyNi0wNS0xNFQxMDo1MTo1MS44MTNaIiwiaSI6IjZkYzE4OTExLi4uIn0"
}Cancel a signature request
Cancels a pending request, invalidates all remaining signer links and fires the signature_request.cancelled webhook.
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | no | Optional reason stored in the audit trail (max 500 chars). |
Request body
curl -X POST https://signeur.eu/api/v1/signatures/<id>/cancel \
-H "Authorization: Bearer sgn_live_..." \
-H "Content-Type: application/json" \
-d '{ "reason": "Sent to wrong recipient" }'Response
{
"id": "6dc18911-f66b-43d6-9810-8de0b07bab04",
"status": "cancelled",
"cancelled_at": "2026-05-17T08:14:02.913Z"
}Effect: status becomes 'cancelled', all unsigned signer links are invalidated and the signature_request.cancelled webhook is fired. The original PDF is preserved but no longer downloadable through /document.
List signers for a request
Returns all signers for one signature request with per-signer status, timestamps and audit trail. Use to render multi-signer progress dashboards.
Request body
curl "https://signeur.eu/api/v1/signatures/<id>/signers" \
-H "Authorization: Bearer sgn_live_..."Response
{
"data": [
{
"id": "9c0aa5ec-…",
"email": "buyer@example.com",
"name": "Anna Buyer",
"name_as_invited": "Anna Buyer",
"name_as_signed": "Anna H. Buyer",
"access_token": "29ErQSUr8Ov…",
"sign_url": "https://signeur.eu/sign/29ErQSUr8Ov…",
"status": "signed",
"position": 0,
"sent_at": "2026-05-14T10:51:52.012Z",
"opened_at": "2026-05-14T10:52:00.481Z",
"signed_at": "2026-05-14T10:52:24.374Z",
"declined_at": null,
"decline_reason": null,
"invalidated_at": null,
"ip_address": "87.92.91.57",
"user_agent": "Mozilla/5.0 (Macintosh; …) Safari/605.1.15",
"locale": "en",
"representing_as_invited": null,
"representing_as_signed": null,
"representing_role": null,
"representing_entity_id": null,
"audit_trail": { "events": [ … ] }
},
{
"id": "f12d8b3c-…",
"email": "seller@example.com",
"status": "pending",
"position": 1,
"...": "..."
}
]
}Resend invitation to a signer
Sends the invitation email to one signer again with a freshly rotated access code. The old code stops working immediately.
| Field | Type | Required | Description |
|---|---|---|---|
locale | string | no | Optional. Override the signer's email locale for this send and persist the change on the signer row. Allowed: 'en' | 'fi' | 'fr' | 'de'. |
Request body
curl -X POST https://signeur.eu/api/v1/signatures/<id>/signers/<signer_id>/resend \
-H "Authorization: Bearer sgn_live_..." \
-H "Content-Type: application/json" \
-d '{ "locale": "fi" }'Response
{
"ok": true,
"signer_id": "9c0aa5ec-…",
"email_sent": true,
"email_error": null,
"access_code_rotated_at": "2026-05-17T10:14:33.872Z"
}Effect: a new 6-digit access code is generated, stored as a bcrypt hash and emailed to the signer. The previously emailed code no longer authenticates the access-code gate. Attempt counters and any temporary lockouts are reset.
Download audit-trail PDF
Returns a standalone PDF containing the full audit record: request metadata, signer details with IPs and user agents, chronological event timeline, and PAdES signature info when the request is sealed. This PDF is informational and is NOT cryptographically signed — the sealed document itself is the legal evidence.
Request body
curl https://signeur.eu/api/v1/signatures/<id>/audit-trail.pdf \
-H "Authorization: Bearer sgn_live_..." \
-o audit-trail.pdfContent-Type: application/pdf · The PDF is generated on demand from the current state of the request and audit trail. Available regardless of request status.
List webhook endpoints
Returns all webhook endpoints configured for your organization. Secrets are not included in the listing — they are shown only once when the endpoint is created.
Request body
curl https://signeur.eu/api/v1/webhook-endpoints \
-H "Authorization: Bearer sgn_live_..."Response
{
"data": [
{
"id": "ep_a1b2c3d4-…",
"url": "https://example.com/hooks/signeur",
"event_types": ["signature_request.completed", "signer.signed"],
"description": "Production sync",
"disabled_at": null,
"created_at": "2026-05-17T10:00:00.000Z",
"updated_at": "2026-05-17T10:00:00.000Z"
}
]
}Create a webhook endpoint
Registers a new URL to receive webhook events. The response includes a freshly generated `secret` which is shown only this once and is used to verify HMAC signatures on incoming webhooks.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | yes | Target URL (must use https:// in production, http://localhost is allowed for development). |
event_types | string[] | no | Array of event types to receive. Empty array (or omitted) means receive all events. |
description | string | no | Optional short human-readable label for the endpoint (max 200 chars). |
Request body
curl -X POST https://signeur.eu/api/v1/webhook-endpoints \
-H "Authorization: Bearer sgn_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/signeur",
"event_types": ["signature_request.completed", "signer.signed"],
"description": "Production sync"
}'Response
{
"id": "ep_a1b2c3d4-…",
"url": "https://example.com/hooks/signeur",
"secret": "whsec_7f3a9c…",
"event_types": ["signature_request.completed", "signer.signed"],
"description": "Production sync",
"disabled_at": null,
"created_at": "2026-05-17T10:00:00.000Z",
"updated_at": "2026-05-17T10:00:00.000Z"
}Important: the secret is shown ONLY in this response. Signeur does not retain a plaintext copy. If you lose it, delete this endpoint and create a new one.
Update a webhook endpoint
Updates the URL, event_types, description or disabled state of an endpoint. All body fields are optional — only the provided ones are changed.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | no | Target URL (must use https:// in production, http://localhost is allowed for development). |
event_types | string[] | no | Array of event types to receive. Empty array (or omitted) means receive all events. |
description | string | no | Optional short human-readable label for the endpoint (max 200 chars). |
disabled | boolean | no | true = pause delivery without deleting the endpoint, false = resume. |
Delete a webhook endpoint
Permanently removes the endpoint. Events stop being delivered to it immediately. The secret is unrecoverable after deletion.
curl -X DELETE https://signeur.eu/api/v1/webhook-endpoints/<id> \
-H "Authorization: Bearer sgn_live_..."Webhooks
Signeur pushes real-time events to one or more URLs you configure for your organization whenever a signature request's state changes. Each event is signed with HMAC-SHA256 and delivered in parallel to all matching endpoints. This is more efficient than polling the GET endpoint.
Supported events
signature_request.created | A new signature request was created. Fires once per request, includes the full signers list. |
signature_request.sent | The invitation email batch was attempted for all signers. Includes per-signer email_sent status. |
signature_request.opened | A signer opened the link for the first time. Fires once per request when the first signer opens it. |
signer.signed | One signer successfully verified their OTP and signed. Fires N times for an N-signer request. |
signature_request.completed | All signers have signed and the document is sealed. Includes document_hash and download_url. |
signature_request.signed | Deprecated alias for signature_request.completed — fires at the same time. Use signature_request.completed for new integrations. |
signature_request.declined | A signer declined; the request was moved to status 'declined' and remaining signers were invalidated. |
signature_request.cancelled | The request was cancelled via the dashboard or POST /signatures/{id}/cancel. |
signature_request.expired | The request reached expires_at without all signers completing. Fires once per request when the daily expire-cron flips status to 'expired'. |
Payload format
// signer.signed — fires once per signer that finishes signing
{
"id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "signer.signed",
"created_at": "2026-05-14T10:52:24.374Z",
"data": {
"signature_request_id": "6dc18911-f66b-43d6-9810-8de0b07bab04",
"signer_id": "9c0aa5ec-…",
"signer_email": "buyer@example.com",
"signer_name": "Anna Buyer",
"signed_at": "2026-05-14T10:52:24.374Z",
"remaining_pending_signers": 1
}
}
// signature_request.completed — fires once when all signers are done
// and the PDF is sealed. signature_request.signed is a deprecated alias
// that fires at the same moment with the same payload.
{
"id": "evt_b2c3d4e5-f6a7-8901-bcde-f23456789012",
"type": "signature_request.completed",
"created_at": "2026-05-14T10:55:11.812Z",
"data": {
"signature_request_id": "6dc18911-f66b-43d6-9810-8de0b07bab04",
"status": "signed",
"signed_at": "2026-05-14T10:55:11.812Z",
"document_hash": "e5132c2fe36dfa…",
"download_url": "https://signeur.eu/api/v1/signatures/6dc18911-…/document"
}
}Signature verification
Each webhook is signed with HMAC-SHA256 in the X-Signeur-Signature header. Verify it to ensure the request genuinely came from Signeur.
Header format:
X-Signeur-Signature: t=1747212744,v1=abc123def456...Signed data:
<timestamp>.<raw json body>Verification example (Node.js):
import { createHmac } from "crypto";
// Verify webhook signature
const sig = req.headers["x-signeur-signature"];
const [t, v1] = sig.split(",").map(s => s.split("=")[1]);
const expected = createHmac("sha256", YOUR_WEBHOOK_SECRET)
.update(`${t}.${rawBody}`).digest("hex");
if (v1 !== expected) throw new Error("Invalid signature");Configuration
Configure endpoints in two ways: (a) Dashboard → Webhooks gives a UI to add, edit, pause or delete endpoints and pick which event types each one receives. (b) The REST API at /api/v1/webhook-endpoints lets you manage them programmatically. Each endpoint gets its own HMAC secret which is shown once at creation — store it securely. A legacy single-URL webhook on the organization is still honored as a fallback when no endpoints exist on the new table.
Delivery
When an event fires, Signeur looks up all enabled endpoints that subscribe to the event type and delivers the payload to each in parallel (5 second timeout per endpoint). Each attempt is logged separately to the signature request's audit trail with the endpoint id, HTTP status and any error. A retry queue is on the roadmap.
Coming soon
The following features are planned:
- → Automatic expiry detection that fires signature_request.expired (currently the status remains 'sent'/'opened' even after expires_at)
- → Webhook retry queue — currently each delivery is a single attempt; failed events do not retry
- → Idempotency-Key support on cancel and resend endpoints (currently only POST /signatures)
- → Webhook delivery history view in the dashboard