Skip to main content
Embedding v2 is in beta. Embed sessions and SDK 1.0 can change in ways that break your integration before the first stable release, estimated for November 2026. Embedding v1, described in Embedded signing, is stable.
In this guide, you embed the sajn signing page in your application, so a party signs without leaving it. Your server creates an embed session for one party of a sent document. Your page gives the session’s client secret to the sajn embed SDK, and the SDK shows the signing page in an iframe.
This page describes SDK 1.0 and embed sessions. If you embed with SDK 0.x and the party’s signing token, see Migrate from embed v1.
sajn has the following packages, all at version 1.0.0-beta.1 under the beta tag on npm. Pin the exact version: a later beta can break your integration.
  • @sajn/embed-js: a function for any web page. See JavaScript.
  • @sajn/embed-react: a React component for React 18 and later. See React.
  • @sajn/embed-vue: a Vue component for Vue 3.3 and later. See Vue.

How an embed session works

  1. The party opens a page in your application.
  2. The SDK calls your fetchClientSecret function, which asks your server for a client secret.
  3. Your server calls POST /api/v1/embed-sessions with your API key and returns the clientSecret from the response.
  4. The SDK opens https://app.sajn.se/embed/v2/{id} in an iframe and hands the client secret to the frame over postMessage. The frame redeems the secret once.
  5. The party signs in the frame, and the SDK calls your onSigned callback.
  6. Your server confirms the signature with the document.party.signed webhook.
The session decides what the frame can do: its features, the pages that can show it, and its appearance. The browser can change only the cosmetic appearance and the language. The party’s signing token never reaches the browser.

Before you begin

  • Embedded signing is part of the Enterprise plan, and sajn turns it on for each organization. Being on Enterprise doesn’t turn it on. To get started, contact [email protected]. If you build on sajn to send documents to your own customers, you also need a partner license.
  • A sandbox has embedded signing turned on when it’s created, so you can build and test there first.
  • Store an API key in the SAJN_API_KEY environment variable on your server. The key needs write access to documents. Sessions need an API key: an OAuth app can’t create them. To create a key, go to Inställningar > Utvecklare > API-nycklar (Settings > Developer > API keys) in the sajn app.
  • Send API version 2026-10 in the Sajn-Version header. The embed session endpoints don’t exist in 2026-09, which returns 404 ROUTE_NOT_FOUND for them.

Add your embed domains

After sajn turns on embedding, the organization’s developer settings get an Inbäddning (Embedding) tab. The domain list belongs to the organization, so every workspace in it shares the list.
  1. In the sajn app, go to Inställningar > Utvecklare > Inbäddning (Settings > Developer > Embedding).
  2. Enter each domain whose pages show the signing page, such as app.example.com, and press Enter.
  3. Click Spara ändringar (Save changes).
The list follows these rules:
  • *.example.com matches every subdomain of example.com, at any depth, but not example.com itself. To allow both, add both.
  • Only pages served over HTTPS can show the frame. localhost can’t be added. To test from your computer, expose your local app on a public HTTPS domain, such as through a tunnel, and add that domain.
  • An empty list allows no pages, and the API refuses to create sessions.
  • sajn checks the list on every request, so removing a domain stops live sessions on it at once.

Embed the signing page

1

Create and send the document on your server

Create the document from a template, fill in the template’s party and values, and send it, all in one request. Set the party’s delivery to [], so sajn doesn’t also email an invitation:
  • templatePartyId is the ID of a party in the template’s parties, from GET /api/v1/templates/TEMPLATE_ID. The document’s copy of that party keeps its signature boxes and settings, and the details you send replace its own. Without templatePartyId, the parties you send replace the template’s parties and their signature boxes are removed.
  • fieldValues fills in values by key, in the same shape as values in Fill in values on a document. To list a template’s keys, create a draft and call GET /api/v1/documents/DOCUMENT_ID/field-values.
  • send: true sends the document after sajn creates it. You can create a session only for a sent document.
The response is the document with the status PENDING. Store its id, and the id of the party whose ref is customer in parties.
2

Create an embed session on your server

Add an endpoint to your server that your page calls. The endpoint checks who the user is, creates a session for that user’s party, and returns only the clientSecret:
The response looks like this:
The SDK calls your endpoint again each time a session ends, so create a new session on every call. Never cache a client secret, log it, or put it in a URL. The session id isn’t a secret.
3

Show the signing page

In your page, give the SDK a container and a fetchClientSecret function that calls your endpoint:
Give the container a height. For more information, see Size the frame. For React and Vue, see the framework pages.
4

Confirm the signature on your server

onSigned runs in the browser, so use it to update the screen, not as proof. Confirm the signature with the document.party.signed webhook event. The event means that this party’s signature is recorded. To act on the sealed PDF, wait for document.completed.

Session lifetime

A session ends at the first of the following:
  • The frame didn’t redeem the client secret within 10 minutes of creation. clientSecretExpiresAt in the response is this limit.
  • The frame had no activity for an hour. A frame in a hidden tab doesn’t keep its session active.
  • 24 hours passed since creation. expiresAt in the response is this limit.
  • You revoked the session with DELETE /api/v1/embed-sessions/{id}, which returns { "id": "emb_…", "revoked": true }.
  • The API key that created the session was revoked or rotated.
When a session ends, the frame tells the SDK, and the SDK calls fetchClientSecret again for a new session. The party keeps their progress: a new session for a party who signed shows the signed state. If sessions end three times within one minute, the SDK stops and raises session_expired.
Revoking a session doesn’t stop the SDK from asking for a new one. To stop an embed for good, make your endpoint refuse the request, or call destroy() in the page.

Create session request

POST /api/v1/embed-sessions takes the following body: The endpoint returns the following errors, in addition to the common errors:

Features

sajn enforces features on the server, so the browser can’t turn them on. A feature that you leave out keeps its default.

Appearance

You can set appearance on the session and in the SDK. The SDK’s values replace the session’s values key by key at the top level, so an SDK colors object replaces the session’s whole colors object. Custom CSS isn’t supported. Text must stay readable, so sajn checks two color pairs for a contrast ratio of at least 4.5:1: foreground on background, and primaryForeground on primary. If colorScheme isn’t dark and you set only one color of a pair, sajn checks it against its light default for the other color. A session request that fails the check returns 400 VALIDATION_FAILED. An SDK appearance that fails the check is ignored as a whole, and the frame keeps its current look. The consent text, the party’s identity, the wording of the sign buttons, and the document itself keep their styles under any appearance.

Size the frame

The sign view is static: the document, and under it a sign section that is always visible. The frame shows no dialogs or overlays, so it works the same at any size.
  • fixed (default): the frame fills its container and scrolls inside. Give the container a height, at least 600px on phones. If the container has no height of its own, the SDK sets the frame to fallbackHeight, which is 600px by default.
  • inline: the frame grows to the height of its content, and your page scrolls. Don’t give the container a fixed height.
A document that requires the party to read it in full always uses fixed, because the read check needs a frame that scrolls.

Signing methods in the frame

Swedish BankID runs inside the frame, by QR code or on the same device. Click-to-sign and drawn signatures also run in the frame. The providers of the following eIDs refuse to run inside a frame: MitID, Norwegian BankID, FTN, iDIN, and qualified electronic signatures (QES). For these, the frame opens the provider in a new window when the party clicks the sign button. The frame updates itself when the signature lands, even if the window doesn’t report back. If the browser blocks the window, the button turns into a link to the same page. If the document has an access check (openVerification), the party passes it inside the frame before the document loads, the same as on the hosted page. Your application has usually signed the user in already, so consider leaving openVerification set to null for embedded documents. A party’s own signVerification always applies before they sign.

Events

Events are hints for your page. They carry documentId, partyId, and externalId, never a credential. Confirm outcomes on your server with webhooks. EmbedState has the following fields:

Error codes

onError receives one of the following codes: For session_not_found, session_expired, and session_revoked from the frame, onError runs first, then onSessionExpired, and then the SDK starts a new session. Don’t show a final error for these codes. For invalid_appearance, the frame keeps working. For every other code, the frame shows its own message, and your page decides what to do next.

Troubleshooting

Fix a blank frame

The SDK raises bootstrap_timeout when the frame doesn’t answer. Usually the browser refused to show the frame, and the frame stays blank. Open the browser console and look for a frame-ancestors or frame-src error. Then check the following causes:
  • The page’s origin isn’t in the session’s origins. The origin must match exactly: the scheme, the host, and the port. https://www.example.com and https://example.com are different origins, and *.example.com doesn’t cover example.com. If you leave out origins, check the organization’s embed domains instead.
  • Your page is inside another frame. Every page in the frame chain must be allowed. Add the outer pages’ origins to ancestorOrigins.
  • Your page’s own Content Security Policy blocks the frame. If your page sends a Content-Security-Policy header, its frame-src or child-src must allow https://app.sajn.se.
  • The page isn’t served over HTTPS. The frame never loads in a page served over http://.
  • The client secret is old. A secret from a session that ended more than 24 hours ago gets a refused frame. Create a new session on every fetchClientSecret call.
  • The host option points at the wrong sajn host. Leave host out to use https://app.sajn.se. Sandboxes use the same host.
  • The network is slow, or something blocks the frame, such as a browser extension or a corporate proxy. To give a slow network more time, raise bootstrapTimeoutMs.

Fix other problems

  • The frame shows an error message: read the code in onError and see Error codes.
  • The frame is cut off or has no height: in fixed mode, give the container a height. In inline mode, don’t.
  • An eID window doesn’t open: the browser blocked it. The party can use the link that the button turns into.
  • POST /api/v1/embed-sessions fails: log code and requestId, and see Errors.

Embed in a mobile app

In a React Native app, use @sajn/embed-react-native. It runs the signing page in a web view and opens eID pages and the BankID app for you. In Swift or Kotlin, the same guide shows how to wire a web view by hand. If you’d rather not embed, show your page in the system browser: SFSafariViewController or ASWebAuthenticationSession on iOS, or Custom Tabs on Android. Every eID works there as on any web page.

Accessibility

The signing frame targets WCAG 2.2 level AA. sajn tests it with axe in light and dark mode: the signing view, form fields, the menu, the reject dialog, view mode, and the receipt after signing. The party can sign with the keyboard alone, and focus is always visible. Your appearance settings take part in this. sajn rejects a text color and background pair below a 4.5:1 contrast ratio, both on the session and in update().

Security

  • The party’s signing token never reaches the browser. The frame redeems the client secret once and keeps a short-lived credential in memory.
  • Only the pages in the session’s origins and ancestorOrigins can show the frame, and only while they match the organization’s embed domains. The page that talks to the frame must be in origins.
  • Rotating or deleting the API key that created a session ends the session.
  • The SDK and the frame check the origin of every message, and post only to each other’s origin.
  • Features and origins can’t change from the browser.

Next steps

JavaScript

Use createSigningEmbed in any web page.

React

Use the React component.

Vue

Use the Vue component.

Migrate from embed v1

Move from SDK 0.x and signing tokens to embed sessions.