Skip to content

Portal API

The Portal API lets your app create and manage signing processes. You provide the document and the list of participants; esigna sends the invitation emails, collects the signatures through its own signing flow, and tells your app what happened through webhooks.

Scopes required

Endpoints on this page require the portal/read or portal/write scope. See Authorization for how to get a token.

All paths below are relative to https://api.esigna.is.

How it works

  1. Your app creates a signing process with a document and a list of participants.
  2. esigna emails an invitation to each participant, with a link to the document.
  3. Participants sign in esigna's signing page with their electronic ID.
  4. esigna notifies your app of each event (a participant signed or declined, the process completed) through webhooks.
  5. When everyone has signed, your app downloads the signed document.

The Portal API works with the signing processes your account creates through the API, with any of its API clients. Documents your staff send from the esigna web app are not visible through the API and send no webhooks.


Creating a signing process

POST /api/v1/portal/signing-processes (requires portal/write)

Example request

{
    "file": {
        "name": "contract.pdf",
        "contentBase64": "JVBERi0xLjQK..."
    },
    "participants": [
        {
            "name": "Jón Jónsson",
            "email": "jon@example.is",
            "kennitala": "0101302989",
            "role": "Signer"
        },
        {
            "name": "Anna Sigurðardóttir",
            "email": "anna@example.is",
            "role": "Viewer"
        }
    ],
    "signatureLocation": "BottomRight",
    "signaturePage": 1
}

Example response: 201 Created

The Location header holds the new process's address, /api/v1/portal/signing-processes/{signingProcessId}. The body is the process as in Getting a process's status: its participants, each with their participantId and the signingUrl you can send them yourself.

{
    "signingProcessId": "c93e6bf4-a256-4b97-9abb-c7650cdc87f5",
    "status": "Waiting",
    "createdAt": "2026-10-09T12:00:00Z",
    "participants": [
        {
            "participantId": "d4e5f6a7-b8c9-0123-def0-123456789012",
            "name": "Jón Jónsson",
            "email": "jon@example.is",
            "role": "Signer",
            "status": "Waiting",
            "hasCompleted": false,
            "hasDeclined": false,
            "declineReason": null,
            "signingUrl": "https://app.esigna.is/sp/c93e6bf4-a256-4b97-9abb-c7650cdc87f5?token=Xk4p..."
        },
        {
            "participantId": "e5f6a7b8-c9d0-1234-ef01-234567890123",
            "name": "Anna Sigurðardóttir",
            "email": "anna@example.is",
            "role": "Viewer",
            "status": "None",
            "hasCompleted": false,
            "hasDeclined": false,
            "declineReason": null,
            "signingUrl": "https://app.esigna.is/sp/c93e6bf4-a256-4b97-9abb-c7650cdc87f5?token=7Rq2..."
        }
    ]
}

Fields

Field Type Required Description
file.name string Yes The file name, ending in .pdf: at most 100 characters, no / or \. It is shown in the invitation emails.
file.contentBase64 string Yes The document, a PDF, encoded in Base64.
participants array Yes At least one participant must have the Signer role. See below.
signatureLocation string Yes The corner where signature stamps go: TopLeft, TopRight, BottomLeft or BottomRight.
signaturePage integer No The page the stamps go on, counting from 1. Defaults to 1. The page must exist in the document.

The whole request can be up to about 30 MB. Because the document is Base64 inside JSON, that allows a PDF of up to about 22 MB.

Participants

Each participant gets an invitation email from esigna with a link to review the document, and to sign it if they are a signer.

Field Type Required Description
participants[].name string Yes Full name, at most 256 characters.
participants[].email string Yes Email address the invitation is sent to, at most 256 characters. Each participant needs their own address.
participants[].role string Yes Signer or Viewer.
participants[].kennitala string No Icelandic national ID, 10 digits without a hyphen. If set, only the person with this kennitala can sign: esigna compares it with the kennitala from the signer's electronic ID and refuses to sign if they differ.
participants[].phoneNumber string No Phone number, at most 7 digits, digits only. Stored and returned in webhook events; not used for signing.
Role Description
Signer Signs the document.
Viewer Gets the document to read but does not sign. Viewers don't affect the process status.

Signature stamps

Each signer's stamp goes in the corner you choose, on the page you choose. Later stamps are placed next to earlier ones, avoiding existing signatures and the page's content. A page has room for a limited number of stamps; if there are more signers than the page can hold, the request is refused and nothing is created. Viewers get no stamp and don't count.

Errors

Status When
400 with errors A field is missing, too long, not valid Base64, not an email address, not a plain .pdf file name, or not one of the listed values; two participants share an email address; a kennitala isn't 10 digits; no participant has the Signer role. The response is a validation problem naming the fields.
400 "Document could not be read" The file is not a readable PDF, or it is encrypted.
400 "Signature page does not exist" signaturePage is not a page of the document.
400 "Too many participants for the page" The page has no room for that many stamps.
400 "Payment method required" The account has no payment method, and its free signatures don't cover this process's signers.
400 "Storage quota exceeded" The account's document storage is full.
401 / 403 See Authorization.

The titled 400s are problem details with title and detail.


Getting a process's status

GET /api/v1/portal/signing-processes/{signingProcessId} (requires portal/read)

Example response

{
    "signingProcessId": "c93e6bf4-a256-4b97-9abb-c7650cdc87f5",
    "status": "Waiting",
    "createdAt": "2026-10-09T12:00:00Z",
    "participants": [
        {
            "participantId": "d4e5f6a7-b8c9-0123-def0-123456789012",
            "name": "Jón Jónsson",
            "email": "jon@example.is",
            "role": "Signer",
            "status": "Waiting",
            "hasCompleted": false,
            "hasDeclined": false,
            "declineReason": null,
            "signingUrl": "https://app.esigna.is/sp/c93e6bf4-a256-4b97-9abb-c7650cdc87f5?token=Xk4p..."
        },
        {
            "participantId": "e5f6a7b8-c9d0-1234-ef01-234567890123",
            "name": "Anna Sigurðardóttir",
            "email": "anna@example.is",
            "role": "Viewer",
            "status": "None",
            "hasCompleted": false,
            "hasDeclined": false,
            "declineReason": null,
            "signingUrl": "https://app.esigna.is/sp/c93e6bf4-a256-4b97-9abb-c7650cdc87f5?token=7Rq2..."
        }
    ]
}

Status values

The process's status is worked out from its signers; viewers don't count.

Status Meaning
Waiting At least one signer has not signed yet.
Completed Every signer has signed. The signed document can be downloaded.
Declined At least one signer declined to sign.
Expired A signer's invitation is more than 7 days old and the process isn't finished.

A participant's own status uses the same values, and is None for viewers.

A process that has been deleted, that belongs to another account, or that was sent from the esigna web app returns 404 Not Found.

Each participant's signingUrl is the link in their invitation email. You can send it to them yourself, for example by taking them straight there from your app, instead of waiting for them to open the email. They open it, log in with their electronic ID, and sign (a viewer reads the document).

  • Set the participant's kennitala. Whoever opens the link and logs in first takes the participant's place. With a kennitala, only that person can.
  • Treat the link like a password: give it only to that participant, and don't log it.
  • An unopened link is valid for 7 days. Once the participant has logged in through it, signingUrl is their page without the one-time token, and they log in to reach it. It is null after they sign or decline, and if the invitation expired unopened.
  • esigna still sends the invitation email.
  • In the sandbox the link leads nowhere: there is no signing page. Use the sandbox endpoints to sign or decline.

Downloading the document

GET /api/v1/portal/signing-processes/{signingProcessId}/document (requires portal/read)

Returns the document as it is now: once the status is Completed, that is the signed document with every signature.

Example response

1
2
3
4
{
    "fileName": "contract.pdf",
    "contentBase64": "JVBERi0xLjQK..."
}

Deleting a signing process

DELETE /api/v1/portal/signing-processes/{signingProcessId} (requires portal/write)

Participants can no longer open or sign the document, and the process is no longer returned by the API. This can't be undone.

Returns 204 No Content, or 404 Not Found if there is no such process.


Receiving events

Rather than polling the status, subscribe to webhooks and esigna will call your endpoint when something happens:

Event When
participant.signed A participant signed the document.
participant.declined A participant declined to sign.
process.completed Every signer has signed. Download the signed document when you get it.

An expired process has no event; check its status if you need to know.

esigna emails the participants (the invitation, and a link to the signed document when everyone has signed), but nobody at your account: for a process created through the API, the webhooks and the status are how your app learns that a participant declined or that the process completed.


Testing in the sandbox

The sandbox, https://sandbox-api.esigna.is, works like production with your sandbox credentials, with two differences: nobody signs with a real electronic ID, and no emails are sent: no invitations, no completion emails and no webhook alerts to your admins. Instead, your app answers for the participants, and everything that follows (the status, the signed document, the webhooks) is the same as in production.

  1. Create a sandbox user for each identity you want to sign with: POST /api/v1/sandbox/users with fullName, kennitala and phone. Each of your sandbox users needs its own kennitala and phone number. The response's uuid identifies the user. See Sandbox users for all the fields.
  2. Create a signing process as above, and get the participants' participantIds from its status.
  3. Sign or decline as a participant:
Endpoint Body Result
POST /api/v1/sandbox/portal/signing-processes/{signingProcessId}/participants/{participantId}/sign { "sandboxUserId": "..." } The participant signs with that sandbox user's identity.
POST /api/v1/sandbox/portal/signing-processes/{signingProcessId}/participants/{participantId}/decline { "reason": "..." } The participant declines, with that reason (at most 256 characters).

Both need portal/write and return 204 No Content. They return 404 for a process, participant or sandbox user that isn't yours, and 400 when:

  • the participant is a viewer, or has already signed or declined;
  • the participant was created with a kennitala and the sandbox user has a different one;
  • the sandbox user is set to refuse (action: Cancel) or to wait before answering (delayInSecondsBeforeAction): use one with action: Success and no delay.

Signing answers 423 Locked while another participant's signature on the same document is in progress; try again when it has finished.