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:
- Your server uploads the PDF (optional) and opens the session with your API key.
- Your server gives the session
urlto your browser code. It never gives the API key. - 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.
- Your server gets the
creation_session.completedwebhook, 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. |