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:
- Find what affects you.
- Send the new version on your requests.
- Move your webhook endpoints.
- Confirm nothing relies on the default.
- Switch the organization default.
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:Show the upgrade prompt
Show the upgrade prompt
Upgrade prompt
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:- Lists return
dataand page by cursor.pageandperPagereturn400. - Errors have one shape, with
codefrom a closed list anduserMessagefor your users. - Writes return the full object.
POST /api/v1/documentsreturns the document with its ID inid. - Parties nest their company, and countries are alpha-2 codes, such as
SE. - Webhooks deliver an event with a dotted
typeand the REST object indata.object, signed according to Standard Webhooks.
Send the new version on your requests
-
In your test environment, send
Sajn-Version: 2026-10on every request:ReplaceAPI_KEYwith your API key. -
Update your code for each change that affects it, and run your tests against
2026-10. - Deploy the change to production.
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. A2026-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:
-
Create a second endpoint for the same URL or a new one, with
"apiVersion": "2026-10"and the same events:Store thesecretfrom the response. Only this response andPOST /api/v1/webhooks/:id/rotate-secretreturn it. -
Verify that your receiver handles the
2026-10deliveries. Each delivery carries its version in theSajn-Versionheader and theapiVersionfield, so a receiver on one URL can tell the two endpoints’ deliveries apart. -
Delete the old endpoint with
DELETE /api/v1/webhooks/{id}.
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
- In the dashboard, go to Inställningar > Utvecklare.
- 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).
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, select2026-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 a2026-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
nullwhen it has no value. In2026-09, many were left out. - In a
PATCHrequest, a field you leave out is unchanged, andnullclears 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:
2026-10, pass limit (1 to 100, default 25) and the previous nextCursor as cursor:
hasMore is false:
total: returned only when you passinclude=total.totalPagesis removed.- Lists that returned everything:
GET /api/v1/folders,/document-categories,/workspaces,/companies,/documents/:id/activity, and/documents/:id/commentsare 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/helperslists return every item in one response, as{ data, hasMore: false, nextCursor: null }. They take nolimitorcursor. - Default order: lists sort by
createdAt, newest first.GET /api/v1/documentssorted byupdatedAt, andGET /api/v1/formsand/forms/:id/submissionsbyupdatedAtascending. To keep an order you depend on, passorderByandorderDirection.
Query strings
Send query values as plain strings. In2026-09, the API JSON-decoded each value, so some clients quoted strings:
2026-10, send the value as is. A quoted value matches the quotes literally:
- Booleans take
trueorfalse. - Dates take an ISO 8601 date, such as
2026-10-01, or a date-time, such as2026-10-01T08:00:00Z. A date-time without an offset is UTC. - Filters that take several values, such as
statusandtagId, 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
Idand takes several values:tagId,templateId,companyId,roleId, andcreatedById. ALLis 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, andupdatedBefore. Each includes its boundary.
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, withcode from a closed list. For every code, see Errors.
In 2026-09, a validation error looks like the following:
2026-10, the same error looks like the following:
-
Branch on
code, never on the status ormessage. -
Show your users
userMessage. It’s always present, and usually Swedish.messageis English text for developers. -
Log
requestIdinstead oferrorId. It matches theSajn-Request-Idresponse header. -
On
404 NOT_FOUND, readresourcefor the type of the missing resource, such asdocument. A path that matches no endpoint returnsROUTE_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:
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 incompany, 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:
2026-10:
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:
2026-10, get one file by type (ORIGINAL, SIGNED, or JOURNAL):
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:
2026-10:
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:
2026-10, the field path takes an ID only, and values go through field values:
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:
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 thekeyproperty on field create and update requests. To find the field that holds a key, callGET /api/v1/documents/:id/fields?key=KEY. - Field values list:
GET /api/v1/documents/:id/field-valuesreturnsdatainstead ofvalues, withpartyIdinstead ofsignerId,kindFORM,PDF_ACROFORM, orPDF_PLACED, andfilledBySENDERorSIGNER. - Responses:
PATCHon a field and the placed-fields endpoints return the field.DELETEon a field returns{ id, deleted: true }. - Names inside
fieldMeta:signerIdispartyId,selectionSignerIdisselectionPartyId, andvisibilityRule.customInputIdisvisibilityRule.customFieldId. This includesupserton the placed-fields endpoints,initialFieldsonPOST /api/v1/templates, and blocks. - Enums inside
fieldMeta: every value is uppercase, such askindINPUT,inputTypeSIGNATURE,fillSourceSIGNER,style.alignLEFT, and a FORM subfieldtypesuch asDATEPICKER. A lowercase value fails with400 VALIDATION_FAILED. - VAT in product tables:
momsisvat, andpricesIncludeMoms,defaultMomsRate,showMomsBreakdown, andsummaryLabels.momsarepricesIncludeVat,defaultVatRate,showVatBreakdown, andsummaryLabels.vat. InproductTablesonGET /api/v1/documents/:id, a row’smomsandlineMomsarevatandlineVat, andmomsAmountisvatAmount. Prices stay decimal amounts in the table’s currency.
placedFields in 2026-09:
2026-10:
Approvals
Internal approval is its own resource. In2026-09, you sent the document with an approver:
202 with the document in PENDING_APPROVAL. In 2026-10, request the approval:
approverIds. The response is 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 inid. Adding a member and inviting someone are separate endpoints.
In 2026-09, POST /api/v1/members with sendInvite invited someone new:
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:
-
Member shape:
POST,GET, andPATCHon/api/v1/membersreturn{ id, email, name, role, status, lastActiveAt, joinedAt }, whereroleis{ id, name }andstatusisACTIVEorINACTIVE. -
Renamed endpoints:
/api/v1/workspace-rolesis/api/v1/roles, and/api/v1/workspace-permissionsis/api/v1/permissions. -
Resend:
POST /api/v1/member-invites/:id/resendreturns the invitation instead of{ success: true }. -
GET /api/v1/me: returns the user ID asidinstead ofuserId. -
User references: a user is
{ id, email, name }, such asinvitedBy,requestedBy, and an identity check’screatedBy. The following references change shape:
Contacts and companies
The search endpoints fold into the lists, where every filter you pass must match. In2026-09, a search matched any one filter:
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 withoutcontacts. To list a company’s contacts, callGET /api/v1/contacts?companyId=.- Contact address: a contact has
addressLine1,addressLine2,postalCode,city,state, andcountry, which you can set when you create or update it.postalCodereplaceszipCode, also onGET /api/v1/organization. - Contact company: a contact’s
companyis the full company object, withcreatedAtandupdatedAt. - Clearing values: in
PATCH /api/v1/contacts/:id,nullclearsemail,phone,nationalId,externalId, andcompanyRole.
Forms, identity checks, and messages
The following resources move or rename:
For example, creating an identity check in
2026-09:
2026-10:
- Identity check token: the create response no longer returns
token.verificationUrlcarries it. - Identity check lists: items have the same fields as a check you get by ID, with
audits,data, andverificationUrlset tonull.createdByis{ id, email, name }. - Empty contact details: a create request that sets
emailorphoneto an empty string fails with400 VALIDATION_FAILED. Leave the field out instead. - Form responses:
POST /api/v1/forms/:id/unpublishand/rotate-tokenreturn the form,PATCH /api/v1/forms/:id/templatereturns the template, andDELETE /api/v1/forms/:idreturns{ id, deleted: true }.POST /api/v1/formsreturns200instead of201. - Form publish issues:
publishIssues[].messageis English, and the Swedish text is inuserMessage. - Messages and comments: a message’s text is
bodyinstead ofcontent, in responses and inPOST /api/v1/documents/:id/messages. Messages and comments name their author asauthor: { type, id, name, email }, instead of a comment’s separateauthorType. A comment’sauthoris always present, and itsid,name, andemailarenullfor the AI assistant or an author who no longer exists. - Login sessions:
POST /api/v1/login/sessionsreturns the session with the same fields asGET /api/v1/login/sessions/:id, plusloginUrlandexpiresAt. Inclaims, a claim your client isn’t approved for isnull. A session’smethodisSE_BANKIDinstead ofBANKID_SE, and so ismethodinlogin.*webhook events. Theamrclaim keepsBANKID_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:
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 on2026-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 asDOCUMENT_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 thatGET /api/v1/events/:id returns. In 2026-09, a delivery looks like the following:
2026-10:
- Switch on
typeinstead ofevent. - Read the resource from
data.object. It’s the object the REST API returns, withoutexpand: a document event carries the document asGET /api/v1/documents/:idreturns it, and a party event addsdata.party. - Deduplicate on
id. It’s the event ID, and it stays the same across retries, as doescreatedAt, which is when the event happened. - When you read past events from
GET /api/v1/events, expectdata.objectto show the resource’s current state for an event that no2026-10endpoint 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 }, withtypeUSER,API_KEY,OAUTH_APP, orSYSTEM, ornull. - Read earlier values from
data.previousAttributesontemplate.updatedanddocument.expiration_extended, instead ofchangedFieldsandpreviousExpiresAt.
data, see Payloads.
Signatures
Deliveries are signed according to Standard Webhooks. Thewebhook-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:

