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.

Base URLhttps://signeur.eu
OpenAPI 3.1 spec

Authentication

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."
}
CodeMeaning
400Bad request (validointi ei mene läpi)
401Authorization-otsikko puuttuu tai Bearer-token virheellinen
404Resurssia ei löytynyt tai se kuuluu toiselle organisaatiolle
409Konflikti: pyyntö on jo allekirjoitettu / ei vielä allekirjoitettu
410Resurssi on vanhentunut (linkki yli 30 päivää vanha)
429Liian monta pyyntöä — odota Retry-After-otsikon ilmoittama aika
500Palvelinvirhe (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

POST/api/v1/signatures

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.

FieldTypeRequiredDescription
titlestringyesTitle of the request (shown in the invitation email).
messagestringnoOptional message shown to each signer.
signers[]arrayyesArray of 1–20 signer objects. At least one is required.
signers[].emailstringyesThe signer's email address.
signers[].namestringnoThe signer's name (shown on the audit page).
signers[].localestringnoPer-signer email locale: 'en' | 'fi' | 'fr' | 'de'. Falls back to the top-level locale, then 'en'.
localestringnoDefault locale applied to signers that don't specify their own. Allowed values: 'en' | 'fi' | 'fr' | 'de'.
document.filenamestringyesOriginal filename of the PDF.
document.content_base64stringyesPDF content as a base64-encoded string (max 10 MB decoded).
metadataobjectnoFree-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/api/v1/signatures/{id}

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" }
}
GET/api/v1/signatures/{id}/document

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.pdf

Content-Type: application/pdf

GET/api/v1/signatures

List signature requests

Returns a paginated list of the organization's signature requests with optional filters.

FieldTypeRequiredDescription
statusquerynoFilter by status: pending | sent | opened | signed | cancelled | declined | expired | awaiting_stamp | stamp_failed.
created_fromquerynoOnly return requests created at or after this ISO-8601 timestamp.
created_toquerynoOnly return requests created before this ISO-8601 timestamp.
limitquerynoPage size, 1–100. Default 25.
cursorquerynoOpaque 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"
}
POST/api/v1/signatures/{id}/cancel

Cancel a signature request

Cancels a pending request, invalidates all remaining signer links and fires the signature_request.cancelled webhook.

FieldTypeRequiredDescription
reasonstringnoOptional 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.

GET/api/v1/signatures/{id}/signers

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,
      "...": "..."
    }
  ]
}
POST/api/v1/signatures/{id}/signers/{signer_id}/resend

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.

FieldTypeRequiredDescription
localestringnoOptional. 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.

GET/api/v1/signatures/{id}/audit-trail.pdf

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.pdf

Content-Type: application/pdf · The PDF is generated on demand from the current state of the request and audit trail. Available regardless of request status.

GET/api/v1/webhook-endpoints

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"
    }
  ]
}
POST/api/v1/webhook-endpoints

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.

FieldTypeRequiredDescription
urlstringyesTarget URL (must use https:// in production, http://localhost is allowed for development).
event_typesstring[]noArray of event types to receive. Empty array (or omitted) means receive all events.
descriptionstringnoOptional 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.

PATCH/api/v1/webhook-endpoints/{id}

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.

FieldTypeRequiredDescription
urlstringnoTarget URL (must use https:// in production, http://localhost is allowed for development).
event_typesstring[]noArray of event types to receive. Empty array (or omitted) means receive all events.
descriptionstringnoOptional short human-readable label for the endpoint (max 200 chars).
disabledbooleannotrue = pause delivery without deleting the endpoint, false = resume.
DELETE/api/v1/webhook-endpoints/{id}

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.createdA new signature request was created. Fires once per request, includes the full signers list.
signature_request.sentThe invitation email batch was attempted for all signers. Includes per-signer email_sent status.
signature_request.openedA signer opened the link for the first time. Fires once per request when the first signer opens it.
signer.signedOne signer successfully verified their OTP and signed. Fires N times for an N-signer request.
signature_request.completedAll signers have signed and the document is sealed. Includes document_hash and download_url.
signature_request.signedDeprecated alias for signature_request.completed — fires at the same time. Use signature_request.completed for new integrations.
signature_request.declinedA signer declined; the request was moved to status 'declined' and remaining signers were invalidated.
signature_request.cancelledThe request was cancelled via the dashboard or POST /signatures/{id}/cancel.
signature_request.expiredThe 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