Skip to main content
API version 2026-10 is a clean break from 2026-09. Every list pages the same way, every error has the same shape, every write returns the object it changed, and a webhook delivers the same object the REST API returns. Version 2026-09 keeps working until its sunset date, October 1, 2027. From that date, requests on 2026-09 fail with 400 API_VERSION_SUNSET. You move one piece at a time, and anything you haven’t moved yet stays on 2026-09:
  1. Find what affects you.
  2. Send the new version on your requests.
  3. Move your webhook endpoints.
  4. Confirm nothing relies on the default.
  5. Switch the organization default.
For how versions work, see API versioning. For every change in one list, see the changelog.

Upgrade with an AI assistant

To have a coding assistant such as Claude Code, Cursor, or Codex make the changes, give it the following prompt in your repository. The prompt lists every change an integration can hit, and points the assistant at the Markdown version of this guide for the examples:
Upgrade prompt
Review the changes, then run your tests against a sandbox organization with Sajn-Version: 2026-10 before you switch production traffic.

Before you begin

  • An API key with access to the workspace you integrate with.
  • A test environment, such as a sandbox organization, where you can run your integration against 2026-10.

Find what affects you

Go through What to change in your code, and list each endpoint, field, query parameter, and webhook event your integration uses that a change affects. The changelog has the complete list. The following changes affect almost every integration:

Send the new version on your requests

  1. In your test environment, send Sajn-Version: 2026-10 on every request:
    Replace API_KEY with your API key.
  2. Update your code for each change that affects it, and run your tests against 2026-10.
  3. Deploy the change to production.
The header takes precedence over the organization default, so your organization default stays on 2026-09 and other integrations on the same organization aren’t affected. If your integration is an OAuth app, a request without the header uses the app’s own version, never the organization default. For more information, see OAuth app version. We recommend keeping the Sajn-Version header in your code permanently. Your integration then never depends on the organization default.

Move your webhook endpoints

A webhook endpoint keeps its own version, so moving your requests doesn’t change your webhook deliveries. A 2026-10 delivery has a new body and new signature headers, so your receiver needs new code for it. To move an endpoint without dropping events, run a second endpoint next to it:
  1. Create a second endpoint for the same URL or a new one, with "apiVersion": "2026-10" and the same events:
    Store the secret from the response. Only this response and POST /api/v1/webhooks/:id/rotate-secret return it.
  2. Verify that your receiver handles the 2026-10 deliveries. Each delivery carries its version in the Sajn-Version header and the apiVersion field, so a receiver on one URL can tell the two endpoints’ deliveries apart.
  3. Delete the old endpoint with DELETE /api/v1/webhooks/{id}.
While both endpoints exist, every event is delivered twice, once per version. A 2026-09 delivery has no event id, so make your handler safe to run twice for one event. For example, check the document’s current status before you act on it. If your receiver already accepts both the 2026-09 and the 2026-10 deliveries, you can upgrade the endpoint in place instead. Send {"apiVersion": "2026-10"} in a PATCH /api/v1/webhooks/{id} request. Every delivery after the change uses 2026-10, including retries of earlier events. The endpoint keeps its secret, which Standard Webhooks libraries accept. To get a whsec_ secret, call POST /api/v1/webhooks/{id}/rotate-secret.

Confirm nothing relies on the default

  1. In the dashboard, go to Inställningar > Utvecklare.
  2. In the API-version section, check the Anrop de senaste 72 timmarna table. The Vald via column shows whether requests chose their version with the header (Header) or relied on the default (Organisationens standard).
A request from an OAuth app without the header also shows as Organisationens standard, with the app’s version. Switching the organization default doesn’t change the version that an OAuth app uses. The table covers only the last 72 hours. If you have jobs that run less often, check the table again after they’ve run. If 2026-09 requests from Organisationens standard remain, find the integration that sends them before you continue. Either update it for 2026-10, or make it send Sajn-Version: 2026-09 until you do.

Switch the organization default

In the API-version section, select 2026-10 in the version list, and then click Byt version. Requests without the Sajn-Version header use 2026-10 from then on. Switching the default doesn’t change the version of existing webhook endpoints. A webhook endpoint created through the API without apiVersion gets the version of that request, so Sajn-Version: 2026-10 gives a 2026-10 endpoint.

What to change in your code

Each section shows a 2026-09 request or response, then the same one on 2026-10. Examples are shortened to the fields that matter. Two rules apply to every endpoint:
  • Every response property is always present, and null when it has no value. In 2026-09, many were left out.
  • In a PATCH request, a field you leave out is unchanged, and null clears it.

Lists and pagination

Every list returns { data, hasMore, nextCursor } and pages by cursor. page and perPage are removed and return 400 VALIDATION_FAILED. In 2026-09, each list had its own array key and page numbers:
In 2026-10, pass limit (1 to 100, default 25) and the previous nextCursor as cursor:
To read every page, keep the other parameters unchanged and stop when hasMore is false:
Also change the following:
  • total: returned only when you pass include=total. totalPages is removed.
  • Lists that returned everything: GET /api/v1/folders, /document-categories, /workspaces, /companies, /documents/:id/activity, and /documents/:id/comments are paginated. If you read one response, read every page.
  • Lists bounded by a parent: a document’s parties, fields, signatures, files, links, reminders, and delegations, GET /api/v1/documents/:id/field-values, a template’s parties and fields, /roles, /permissions, and the /helpers lists return every item in one response, as { data, hasMore: false, nextCursor: null }. They take no limit or cursor.
  • Default order: lists sort by createdAt, newest first. GET /api/v1/documents sorted by updatedAt, and GET /api/v1/forms and /forms/:id/submissions by updatedAt ascending. To keep an order you depend on, pass orderBy and orderDirection.

Query strings

Send query values as plain strings. In 2026-09, the API JSON-decoded each value, so some clients quoted strings:
In 2026-10, send the value as is. A quoted value matches the quotes literally:
The following rules apply to every query parameter:
  • Booleans take true or false.
  • Dates take an ISO 8601 date, such as 2026-10-01, or a date-time, such as 2026-10-01T08:00:00Z. A date-time without an offset is UTC.
  • Filters that take several values, such as status and tagId, take a comma-separated list (status=PENDING,COMPLETED) or a repeated parameter.
  • An unknown query parameter returns 400 VALIDATION_FAILED, so a misspelled filter fails instead of returning every result.
  • A filter that references another resource ends in Id and takes several values: tagId, templateId, companyId, roleId, and createdById.
  • ALL is no longer a filter value. To include every value, leave the filter out.
  • To list top-level documents or templates, pass folderId=root.
  • Mutable resources take createdAfter, createdBefore, updatedAfter, and updatedBefore. Each includes its boundary.
The following query parameters are renamed: GET /api/v1/identity-checks returns every check when you leave out createdAfter; in 2026-09, it returned only the last seven days. The externalId filter on GET /api/v1/documents matches the whole value, case-sensitively. For a partial match, use query.

Errors

Every error response has the same shape, with code from a closed list. For every code, see Errors. In 2026-09, a validation error looks like the following:
In 2026-10, the same error looks like the following:
To update your error handling, do the following:
  • Branch on code, never on the status or message.
  • Show your users userMessage. It’s always present, and usually Swedish. message is English text for developers.
  • Log requestId instead of errorId. It matches the Sajn-Request-Id response header.
  • On 404 NOT_FOUND, read resource for the type of the missing resource, such as document. A path that matches no endpoint returns ROUTE_NOT_FOUND.
  • Map the old codes to the new ones:
  • Expect the following status changes:
401 UNAUTHORIZED means only that the token is missing, invalid, expired, or revoked. A duplicate unique value returns 409 ALREADY_EXISTS. Retry 503 UPSTREAM_UNAVAILABLE with exponential backoff.

Documents and parties

Full objects from writes

Every write returns the object it changed. POST /api/v1/documents returns the document in the same shape as GET /api/v1/documents/:id. Read the document ID from id, and each party ID from parties[].id. In 2026-09, the response is a summary:
In 2026-10, the response is the document:
fields is null unless you pass expand=fields. The following endpoints also return the document: PATCH /api/v1/documents/:id, POST /api/v1/documents/:id/send, /withdraw, and /extend-expiration, POST /api/v1/documents/:id/tags, and DELETE /api/v1/documents/:id/tags/:tagId. DELETE /api/v1/documents/:id returns the document with deletedAt set. Deleting a party, a file, a document link, a contact, a custom field, a member, an invitation, or a role returns { "id": "...", "deleted": true }. Each document in GET /api/v1/documents includes tags and parties, with each party’s id, name, email, role, signingStatus, and signedAt. You can tell who has signed without reading each document.

Parties and companies

A party nests its company in company, on documents and templates alike, and countries use ISO 3166-1 alpha-2 codes everywhere. The PARALLEL or SEQUENTIAL setting is documentMeta.signingMode, so signingOrder only means a party’s position. In 2026-09, a POST /api/v1/documents request looks like the following:
In 2026-10:
A party in a response has company with id, name, orgNumber, and role, or null for a private individual. company.id replaces companyId. A request that sends companyName, companyOrgNumber, companyRole, or documentMeta.signingOrder fails with 400 VALIDATION_FAILED. In a PATCH request to a party, "company": null clears the company. The same company object applies to template parties, on POST /api/v1/templates/:id/parties, PATCH /api/v1/templates/:id/parties/:partyId, and PUT /api/v1/forms/:id/template/parties, and in their responses. A document party has createdAt, the time when the party was added to the document. A template party keeps createdAt and updatedAt. On a sent document, PATCH /api/v1/documents/:id/parties/:partyId changes the name, email, and phone of a party who hasn’t signed. Any other property fails with 409 INVALID_STATE.

Files

GET /api/v1/documents/:id/download/:fileType is removed. In 2026-09:
In 2026-10, get one file by type (ORIGINAL, SIGNED, or JOURNAL):
To list every file that’s ready, call GET /api/v1/documents/:id/files, which returns { data: [...] }. A file that isn’t produced yet returns 409 INVALID_STATE instead of 404. POST /api/v1/files returns the file object, the same as GET /api/v1/files/:id, plus uploadUrl and key. Read the ID from id instead of fileId. POST /api/v1/files/:id/confirm returns the file.

Send and approval

POST /api/v1/documents/:id/send no longer submits a document for approval. When the caller needs an approval first, it returns 409 APPROVAL_REQUIRED, never 202. To request an approval, see Approvals.

Other document changes

Closed enums are uppercase everywhere, for example member status (ACTIVE), login session status (COMPLETED), form question kind (NAME), document link origin (MANUAL), and documentStyle.theme fonts (OPEN_SANS). Document and identity check statuses spell CANCELLED.

Fields

Fields have one write path for structure and one for values. To create fields, send { "fields": [...] }, also for one field. In 2026-09, the body could be a single field:
In 2026-10:
The response has the created fields in data, each in the shape of GET /api/v1/documents/:id/fields/:fieldId. To fill in a value, use PATCH /api/v1/documents/:id/field-values. In 2026-09, you could address a FORM subfield by key in the field path:
In 2026-10, the field path takes an ID only, and values go through field values:
The response reports each value. In 2026-09, it had a top-level success, and an error was a Swedish string. In 2026-10, each result has success and an error object or null:
A failed result’s error is { code, message, userMessage }, with code NOT_FOUND, INVALID_STATE, or VALIDATION_FAILED. The field values are in data instead of values. Also change the following:
  • Removed: PATCH /api/v1/documents/:id/fields, which updated FORM subfields by key, and the key property on field create and update requests. To find the field that holds a key, call GET /api/v1/documents/:id/fields?key=KEY.
  • Field values list: GET /api/v1/documents/:id/field-values returns data instead of values, with partyId instead of signerId, kind FORM, PDF_ACROFORM, or PDF_PLACED, and filledBy SENDER or SIGNER.
  • Responses: PATCH on a field and the placed-fields endpoints return the field. DELETE on a field returns { id, deleted: true }.
  • Names inside fieldMeta: signerId is partyId, selectionSignerId is selectionPartyId, and visibilityRule.customInputId is visibilityRule.customFieldId. This includes upsert on the placed-fields endpoints, initialFields on POST /api/v1/templates, and blocks.
  • Enums inside fieldMeta: every value is uppercase, such as kind INPUT, inputType SIGNATURE, fillSource SIGNER, style.align LEFT, and a FORM subfield type such as DATEPICKER. A lowercase value fails with 400 VALIDATION_FAILED.
  • VAT in product tables: moms is vat, and pricesIncludeMoms, defaultMomsRate, showMomsBreakdown, and summaryLabels.moms are pricesIncludeVat, defaultVatRate, showVatBreakdown, and summaryLabels.vat. In productTables on GET /api/v1/documents/:id, a row’s moms and lineMoms are vat and lineVat, and momsAmount is vatAmount. Prices stay decimal amounts in the table’s currency.
For example, a box in a PDF field’s placedFields in 2026-09:
In 2026-10:

Approvals

Internal approval is its own resource. In 2026-09, you sent the document with an approver:
The response was 202 with the document in PENDING_APPROVAL. In 2026-10, request the approval:
To use the approvers already on the document, leave out approverIds. The response is the request:
The following endpoints replace the old ones. Each action returns the request: GET /api/v1/approval-requests filters by documentId, approverId, and status. A document has approvalRequestId, and the webhook events approval_request.created, approval_request.approved, approval_request.rejected, and approval_request.cancelled report changes.

Members, roles, and invites

A member has one shape everywhere, with the user ID in id. Adding a member and inviting someone are separate endpoints. In 2026-09, POST /api/v1/members with sendInvite invited someone new:
The response had userId, roleId, roleName, invited, and addedAt. In 2026-10, POST /api/v1/members only adds someone who is already in the organization, and returns 404 for any other email address. To invite someone new, call POST /api/v1/member-invites:
The response is the invitation:
Also change the following:
  • Member shape: POST, GET, and PATCH on /api/v1/members return { id, email, name, role, status, lastActiveAt, joinedAt }, where role is { id, name } and status is ACTIVE or INACTIVE.
  • Renamed endpoints: /api/v1/workspace-roles is /api/v1/roles, and /api/v1/workspace-permissions is /api/v1/permissions.
  • Resend: POST /api/v1/member-invites/:id/resend returns the invitation instead of { success: true }.
  • GET /api/v1/me: returns the user ID as id instead of userId.
  • User references: a user is { id, email, name }, such as invitedBy, requestedBy, and an identity check’s createdBy. The following references change shape:

Contacts and companies

The search endpoints fold into the lists, where every filter you pass must match. In 2026-09, a search matched any one filter:
In 2026-10:
GET /api/v1/contacts filters by email, phone, externalId, companyId, tagId, and query. GET /api/v1/companies filters by orgNumber, name, and query. The company registry returns basic.orgNumber instead of basic.orgNr. Also change the following:
  • GET /api/v1/companies/:id: returns the company without the { company } wrapper, and without contacts. To list a company’s contacts, call GET /api/v1/contacts?companyId=.
  • Contact address: a contact has addressLine1, addressLine2, postalCode, city, state, and country, which you can set when you create or update it. postalCode replaces zipCode, also on GET /api/v1/organization.
  • Contact company: a contact’s company is the full company object, with createdAt and updatedAt.
  • Clearing values: in PATCH /api/v1/contacts/:id, null clears email, phone, nationalId, externalId, and companyRole.

Forms, identity checks, and messages

The following resources move or rename: For example, creating an identity check in 2026-09:
In 2026-10:
Also change the following:
  • Identity check token: the create response no longer returns token. verificationUrl carries it.
  • Identity check lists: items have the same fields as a check you get by ID, with audits, data, and verificationUrl set to null. createdBy is { id, email, name }.
  • Empty contact details: a create request that sets email or phone to an empty string fails with 400 VALIDATION_FAILED. Leave the field out instead.
  • Form responses: POST /api/v1/forms/:id/unpublish and /rotate-token return the form, PATCH /api/v1/forms/:id/template returns the template, and DELETE /api/v1/forms/:id returns { id, deleted: true }. POST /api/v1/forms returns 200 instead of 201.
  • Form publish issues: publishIssues[].message is English, and the Swedish text is in userMessage.
  • Messages and comments: a message’s text is body instead of content, in responses and in POST /api/v1/documents/:id/messages. Messages and comments name their author as author: { type, id, name, email }, instead of a comment’s separate authorType. A comment’s author is always present, and its id, name, and email are null for the AI assistant or an author who no longer exists.
  • Login sessions: POST /api/v1/login/sessions returns the session with the same fields as GET /api/v1/login/sessions/:id, plus loginUrl and expiresAt. In claims, a claim your client isn’t approved for is null. A session’s method is SE_BANKID instead of BANKID_SE, and so is method in login.* webhook events. The amr claim keeps BANKID_SE.

Money and limits

Every amount of money is { amount, currency }, with amount in the currency’s minor unit, such as öre for SEK. In 2026-09, GET /api/v1/limits looks like the following:
In 2026-10:
balance is in the organization’s currency, and the AI budget fields are always in SEK. An unlimited AI budget is null. bankIdSignatures is signatures.SE_BANKID. The aiTokens aliases and the cost fields quota.additionalBankidCost, quota.smsCost, and additionalCosts, which always returned 0, are removed.

Webhooks

The following changes apply to webhook endpoints on 2026-10. For the full event catalog, see Events.

Manage endpoints

A webhook has status (ENABLED, DISABLED, or PAUSED), pausedAt, and pauseReason. To resume a paused webhook, call POST /api/v1/webhooks/:id/reactivate. The secret is returned only when you create the webhook or rotate its secret. A secret you pass on create must be a Standard Webhooks secret: whsec_ followed by base64.

Event types

Event types are lowercase and dotted. Most names follow from the old ones, such as DOCUMENT_PARTY_SIGNED to document.party.signed and CONTACT_CREATED to contact.created. The following change more: document.completed means that the sealed, signed PDF is ready. It fires after sajn seals the PDF, also for a document that a member completes early in the dashboard. To act when every party has signed, before sealing, subscribe to document.fully_signed.

Envelope

A delivery is the event itself, the same object that GET /api/v1/events/:id returns. In 2026-09, a delivery looks like the following:
In 2026-10:
To update your receiver, do the following:
  • Switch on type instead of event.
  • Read the resource from data.object. It’s the object the REST API returns, without expand: a document event carries the document as GET /api/v1/documents/:id returns it, and a party event adds data.party.
  • Deduplicate on id. It’s the event ID, and it stays the same across retries, as does createdAt, which is when the event happened.
  • When you read past events from GET /api/v1/events, expect data.object to show the resource’s current state for an event that no 2026-10 endpoint subscribed to when it happened. If that resource no longer exists, the list leaves the event out.
  • Read who caused the event from actor: { type, id }, with type USER, API_KEY, OAUTH_APP, or SYSTEM, or null.
  • Read earlier values from data.previousAttributes on template.updated and document.expiration_extended, instead of changedFields and previousExpiresAt.
For each event’s data, see Payloads.

Signatures

Deliveries are signed according to Standard Webhooks. The webhook-id, webhook-timestamp, and webhook-signature headers replace X-Sajn-Signature, X-Sajn-Delivery, and X-Sajn-Environment, and X-Sajn-Secret isn’t sent. webhook-id is the event id. To verify a delivery, use a Standard Webhooks library with your endpoint’s secret, or the following code. Pass it the raw request body, before you parse it:
For more information, see Verify signatures.