REST API

Creation sessions

A creation session is one eSoppari page on which a person of your app reviews and sends one signing request. The person needs no eSoppari account.

The flow has four steps:

  1. Your server uploads the PDF (optional) and opens the session with your API key.
  2. Your server gives the session url to your browser code. It never gives the API key.
  3. The person reviews the document and the signers on the page, and clicks Send to signers. The page is either a full page (hosted) or a frame in your app (embedded): see Embedding and hosted pages.
  4. Your server gets the creation_session.completed webhook, with the id of the signing request that the send created.

The placeholders in the examples

Placeholder What to put there
$ESOPPARI_KEY Your API key, from API keys in the admin portal. Keep it on your server, in an environment variable or a secret store. See Authentication.
https://api.esoppari.fi The API origin of your eSoppari service.
<document id> The id that POST /v1/documents gave for your PDF. See Quickstart.
<session id> The id of the session, from the create answer.
https://app.example.com/... Pages of your own app.

Open a session

curl -X POST https://api.esoppari.fi/v1/creation-sessions \
  -H "Authorization: Bearer $ESOPPARI_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ui_mode": "hosted",
    "success_url": "https://app.example.com/contracts/done?session={SESSION_ID}",
    "cancel_url": "https://app.example.com/contracts",
    "document_id": "<document id>",
    "title": "Lease agreement, flat 114",
    "signers": [{"name": "Anna Korhonen", "email": "anna@example.com"}]
  }'

Fields of the session

Only the first three are required.

Field Type What it does
ui_mode hosted or embedded Required. hosted: the person goes to an eSoppari page. embedded: the page shows in a frame of your app.
success_url string, https, 2048 characters max Required. Where a hosted page sends the person after the send. An absolute https URL with no user name or password in it. {SESSION_ID} in it becomes the session id, so your page knows which session ended.
cancel_url string, as success_url Required. Where a hosted page sends the person after Cancel. {SESSION_ID} works the same way. An embedded page does not navigate: it calls your callbacks instead.
embed_origin string Embedded only, and then required: one of your embed origins, such as https://app.example.com. Only that origin can frame the page. A hosted session must not set it.
document_id uuid The PDF that the person signs. The page shows it, and the person cannot replace it. Omit it to let the person upload a PDF on the page, 20 times at most.
expires_in_minutes integer, 5 to 60, default 60 How long the page stays open. After it, the page shows "This page is no longer open".
locale en, fi or sv The language of the page. Omit it to use the language of the person's browser.
allow_edit_signers boolean, default true true: the person can add, remove and change the signers. false: the page shows your signers and cannot change them, and then signers is required.

Fields of the request

The session also takes the fields of a signing request create. They are the prefill, and the request that the send creates gets them.

Field Type What it does
title string, 200 max The title of the request. The person can change it.
signers array, 1 to 50 The prefill of the signer rows. See Signers.
language en, fi or sv The language of the document.
description string, 2000 max A description of the request.
external_reference string, 200 max Your own id for the request, for example a case number. It shows in lists and need not be unique.
group_id uuid The group that can see the request. Omit it to use the default group of the API key.
order_type sequential or parallel sequential invites the signers by order; parallel invites them all at once.
expires_on date, YYYY-MM-DD The last day the signers can sign.
reminder_interval_days integer, 0 to 30 Days between reminders. 0 sends none.
processing_basis consent, contract, legal_obligation, vital_interests, public_task or legitimate_interests The GDPR Art. 6(1) ground that the signer notice states in your name.
processing_purpose string, 500 max Why you process the signers' data, in the signer notice.
retention_category string The code of one of your active retention categories.
legal_basis string, 64 max The retention ground that an erasure weighs.
completion_delivery copy, notice or none What the signers get when everyone signed: the signed PDF, a notice, or nothing.

A field that you omit takes the default of the group, then of your organisation, as a create does. The page shows the last day to sign and the reminders, and cannot change any field of this table except title and signers.

Signers

Each row of signers:

Field Type What it does
name string, 256 max Required. The signer's full name.
email string, 320 max Required. Where the invitation goes. Each signer needs their own address.
order integer, 1 to 50 The signing position for sequential. Equal numbers sign in parallel. Defaults to the position in the list.
required_eid_country two letters, ISO 3166-1, such as FI The signer must use an eID of this country. Empty means any.
language en, fi or sv The language of the signer's emails and pages.
message string, 2000 max A note in the invitation email.
capacities array, 10 max What the signer signs as. Empty means personally.

Each item of capacities:

Field What it does
type personal, or representative for a signer who signs for an organisation.
organisation_name Required for representative: the organisation, such as Acme Oy. 256 characters max.
organisation_id Optional: its business ID, such as 1234567-8. 64 characters max.

What the person can change. On the page, the person edits the name, the email and the capacities of each signer, and can add and remove rows. A signer whose email stays the same keeps required_eid_country, language and message. The rows keep your order only while the list of emails stays the same and in the same order; after any other change, each row signs at its position on the page. A new row gets the defaults.

The answer

A 201 with the session. Only this answer has url and client_secret, so keep url on your server for the browser of this person.

{
  "id": "<session id>",
  "status": "open",
  "ui_mode": "hosted",
  "url": "https://create.esoppari.fi/c/<session id>#cs_...",
  "client_secret": "cs_...",
  "success_url": "https://app.example.com/contracts/done?session={SESSION_ID}",
  "cancel_url": "https://app.example.com/contracts",
  "allow_edit_signers": true,
  "expires_at": "2026-10-16T10:00:00Z",
  "created_at": "2026-10-16T09:00:00Z"
}
Field What it is
id The session id. Use it with GET /v1/creation-sessions/{id} and to match webhooks.
url The page, with the secret of the session after #. Give it to the browser of the one person who sends: anyone with it can send for you until the session closes. Do not log it.
client_secret The same secret on its own: cs_, the session id and a random part. You need it only if you build the page URL yourself; url already has it.
status open now. See The life of a session.
expires_at When the page closes, in UTC.
embed_origin Set for an embedded session only.

Errors

Status code Cause
401 invalid_api_key and the others of Authentication No key, or a key that is revoked or expired.
422 validation_failed One or more fields. Each item of errors has a pointer and a code, as below.
invalid_value at /ui_mode or /locale A value outside its list.
url_invalid at /success_url or /cancel_url Not an absolute https URL.
out_of_range at /expires_in_minutes Below 5 or above 60.
embed_origin_required, embed_origin_not_allowed, embed_origin_unknown, invalid_value at /embed_origin Embedded with no origin, hosted with an origin, an origin that is not in your list, or a value that is not an origin.
document_not_found at /document_id No such upload, or it expired.
signers_required at /signers allow_edit_signers is false with no signers.
The field codes of a create See Errors.
422 creation_session_limit_reached Your organisation has 100 open sessions. A session no longer counts after it is sent, cancelled or expired.

The life of a session

status Means
open The page is open.
completed The person sent the request. signing_request_id names it.
cancelled The session was cancelled. cancel_reason says why.
expired expires_at passed before a send.

A session sends once. A reload of the page keeps the prefill and the uploaded PDF, not the changes that the person did not send.

cancel_reason:

Value What cancelled it
person The person clicked Cancel.
api_key_revoked You revoked the API key that opened it.
embed_origin_removed You removed its embed origin.
request_erased Its request was erased. This one sends no webhook.

When the API key expires, the page of its open sessions closes at once, but their status stays open until expires_at, and then becomes expired.

Know that it was sent

Trust the creation_session.completed webhook, not the redirect to success_url or the browser callback: a person can open success_url themselves.

Webhook When data
creation_session.completed The person sent the request. id (the session), signing_request_id
creation_session.cancelled The session was cancelled. id, reason
creation_session.expired The session expired before a send. id

To check by hand, read the session:

curl https://api.esoppari.fi/v1/creation-sessions/<session id> \
  -H "Authorization: Bearer $ESOPPARI_KEY"

It gives the fields of the create answer without url and client_secret, plus signing_request_id once it is completed, cancel_reason once it is cancelled, and closed_at once it is closed. A session of another organisation, or no session, is a 404 creation_session_not_found. 30 days after a session closes, eSoppari deletes it, and a read gives that 404 too.

Embed origins

An embed origin is the scheme, the host and the port of the page of your app that shows the frame: https://app.example.com, or https://app.example.com:8443. It has no path. It must be https, and it cannot be an IP address or have a wildcard. Your organisation can have 10.

Add one on the API keys page of the admin portal, or:

curl -X POST https://api.esoppari.fi/v1/embed-origins \
  -H "Authorization: Bearer $ESOPPARI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"origin": "https://app.example.com"}'
Call Answer
GET /v1/embed-origins 200, data: each origin and created_at.
POST /v1/embed-origins 201. 409 embed_origin_exists if you have it, 422 embed_origin_limit_reached at 10, 422 validation_failed with invalid_value at /origin for a value that is not an origin.
DELETE /v1/embed-origins?origin=https://app.example.com 204. The open sessions on it are cancelled at once with embed_origin_removed. 404 embed_origin_not_found if you do not have it.