> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sajn.se/llms.txt
> Use this file to discover all available pages before exploring further.

# API changelog

> Breaking changes in each version of the sajn API and webhook payloads

Each entry lists the breaking changes in one version, compared to the version before it. Additive changes, such as new endpoints and new response fields, ship to every supported version and aren't listed. For how versions work, see [API versioning](/api-fundamentals/versioning). To move to the latest version, with before-and-after examples, see [Upgrading to 2026-10](/upgrading/2026-10).

## 2026-10

No sunset date.

### Documents and parties

* Removes the deprecated signer aliases. On `POST /api/v1/documents`, send `parties` instead of `signers`. On `POST /api/v1/documents/:id/reminders`, send `partyIds` instead of `signerIds`. The `ACCEPTOR` role is rejected; send `SIGNER` instead. Responses from `POST /api/v1/documents` and `GET /api/v1/documents/:id`, and document webhook payloads, no longer include `signers`; read `parties` instead. The `/api/v1/documents/:id/signers` endpoints are removed; use the `/api/v1/documents/:id/parties` endpoints instead.
* Returns the full document from `POST /api/v1/documents`, in the same shape as `GET /api/v1/documents/:id`. Read the document ID from `id` instead of `documentId`, and each party ID from `parties[].id` instead of `parties[].signerId`. `GET /api/v1/documents/:id` leaves out `fields` unless you pass `expand=fields`. The `externalId` filter on `GET /api/v1/documents` matches the whole value, case-sensitively; for a partial match, use `query`. `GET /api/v1/documents/:id/download` is removed; use `GET /api/v1/documents/:id/files/:type`. `POST /api/v1/documents/:id/parties/:partyId/remind` is removed; use `POST /api/v1/documents/:id/reminders` with `partyIds`, which reports each party's outcome in `results` instead of failing the request.
* `GET /api/v1/documents/:id/parties/:partyId` no longer returns the party's `token`; `signingUrl` already carries it. An uploaded attachment in a FORM subfield's `fieldMeta.value` is `{ filename, mimeType, size }`, without the storage key and checksum. `POST /api/v1/files` returns the file object, the same as `GET /api/v1/files/:id`, with `uploadUrl` and `key`; use `id` instead of `fileId`.
* Uses ISO 3166-1 alpha-2 country codes everywhere, as organizations and companies already did. A party's `country`, on documents, templates, and forms, is alpha-2 (`SE` instead of `SWE`) in requests and responses, and `GET /api/v1/helpers/countries` returns alpha-2 codes.
* Returns the same template everywhere, in the shape of `GET /api/v1/templates/:id`: the list, create, update, duplicate, delete, and tag endpoints return `templateMeta`, `parties`, `tags`, and `deletedAt`. `GET /api/v1/templates/:id` leaves out the content blocks, with `fields: null`, unless you pass `expand=fields`. `DELETE /api/v1/templates/:id` moves the template to the trash and returns it with `deletedAt` set, and `POST /api/v1/templates/:id/tags` and `DELETE /api/v1/templates/:id/tags/:tagId` return the template, instead of `{ success }`. `DELETE /api/v1/templates/:id/parties/:partyId` returns `{ id, deleted: true }`. In `PATCH /api/v1/templates/:id/parties/:partyId`, `null` clears a property. Updating or deleting a locked template, or changing its parties, fails with `409 INVALID_STATE` instead of `403 PERMISSION_DENIED`.
* Makes internal approval its own resource. To request approval of a draft, call `POST /api/v1/approval-requests` with `documentId` and `approverIds`, or without `approverIds` to submit the approvers already on the document, instead of `POST /api/v1/documents/:id/send` with `approverId`. `GET /api/v1/approval-requests` lists requests, filtered by `documentId`, `approverId`, and `status`, and `GET /api/v1/approval-requests/:id` returns one. To act on a request, call `POST /api/v1/approval-requests/:id/approve`, `/reject`, or `/cancel`; each returns the request. A request lists each approver as `{ user, stage, orGroup, status, comment, resolvedAt }`, where `user` is `{ id, email, name }`. Removes `GET /api/v1/documents/:id/approval`, `DELETE /api/v1/documents/:id/approval`, `POST /api/v1/documents/:id/approval/approve`, and `POST /api/v1/documents/:id/approval/reject`. Removes `GET /api/v1/approvers`; call `GET /api/v1/members?permission=APPROVE_DOCUMENT`, which also lists you.
* Returns the full document, in the shape of `GET /api/v1/documents/:id` without `fields`, from `PATCH /api/v1/documents/:id`, `POST /api/v1/documents/:id/send`, `POST /api/v1/documents/:id/withdraw`, `POST /api/v1/documents/:id/extend-expiration`, and `DELETE /api/v1/documents/:id`. A document has `deletedAt`, `templateId`, `folderId`, and `responsibleUserId`, and a full document also has `approvalRequestId`. A deleted document is returned with `deletedAt` set. `POST /api/v1/documents/:id/send` no longer submits the document for approval: when the document needs one, it returns `409 APPROVAL_REQUIRED` instead of `403`, never `202`, and a request with `approverId` is rejected; request the approval with `POST /api/v1/approval-requests` instead. `DELETE /api/v1/documents/:id/parties/:partyId`, `DELETE /api/v1/files/:id`, and `DELETE /api/v1/documents/:id/links/:linkId` return `{ id, deleted: true }`. `GET /api/v1/templates/:id/documents` is removed; use `GET /api/v1/documents?templateId=`. `GET /api/v1/documents/:id/download/:fileType` is removed; use `GET /api/v1/documents/:id/files/:type`, or `GET /api/v1/documents/:id/files` for every file that is ready.
* Nests a party's company in `company`, with `id`, `name`, `orgNumber`, and `role`, in place of `companyId`, `companyName`, `companyOrgNumber`, and `companyRole`, on document parties and template parties alike. `company` is null for a private individual. Requests to `POST /api/v1/documents`, `PATCH /api/v1/documents/:id/parties/:partyId`, `POST /api/v1/templates/:id/parties`, `PATCH /api/v1/templates/:id/parties/:partyId`, and `PUT /api/v1/forms/:id/template/parties` take `company` too, and a request that still sends one of the flat keys fails with `400 VALIDATION_FAILED`. In a `PATCH` request, `null` clears `phone`, `externalId`, `nationalId`, or a key of `company`, and `company: null` clears the whole company. A document party gains `createdAt`, when it was added to the document; a template party keeps `createdAt` and `updatedAt`.
* `POST /api/v1/documents/:id/links` returns the link in the shape of an item from `GET /api/v1/documents/:id/links`, and every link has `fromDocumentId` and `toDocumentId`. `POST /api/v1/files/:id/confirm` returns the file, in the shape of `GET /api/v1/files/:id`, instead of `{ id, status, size, checksumVerified }`. `GET /api/v1/folders/:id`, `POST /api/v1/folders`, and `PATCH /api/v1/folders/:id` return `childFolderCount` and `itemCount`. `PATCH /api/v1/documents/:id/parties/:partyId` on a sent document changes the `name`, `email`, and `phone` of a party who hasn't signed, instead of failing; any other property fails with `409 INVALID_STATE`. `POST /api/v1/documents/:id/parties` and `PATCH /api/v1/documents/:id/parties/:partyId` return the party in the shape of `GET /api/v1/documents/:id/parties/:partyId`, with its `signingUrl`, and log that you read the signing URL, as the `GET` request does.
* Each document in `GET /api/v1/documents` has `parties`, with every party's `id`, `name`, `email`, `role`, `signingStatus`, and `signedAt`, and `tags`, so you can tell who has signed without reading each document.

### Fields

* Returns the field itself from the field endpoints. `POST /api/v1/documents/:id/fields` and `POST /api/v1/templates/:id/fields` return the created fields in `data`, each in the shape of `GET /api/v1/documents/:id/fields/:fieldId`; read the document or template ID from each field instead of the top-level `documentId` or `templateId`. A `PATCH` request to a field returns the updated field in that shape. A `DELETE` request to a field returns `{ id, deleted: true }`; to keep the deleted content, read the field before you delete it.
* Gives fields one write path. `PATCH /api/v1/documents/:id/fields/:fieldId` and `PATCH /api/v1/templates/:id/fields/:fieldId` take a field ID only; the `key:` path prefix is removed. To fill in a FORM subfield or another value, use `PATCH /api/v1/documents/:id/field-values`. To find the field that holds a key, pass `key` to `GET /api/v1/documents/:id/fields` or `GET /api/v1/templates/:id/fields`. `PATCH /api/v1/documents/:id/fields`, which updated FORM subfields by key, is removed for the same reason. The `key` property of a field in the create and update requests, which sajn ignored, is removed.
* Uses `partyId` and `customFieldId` inside `fieldMeta`, like everywhere else. A box in `placedFields` and the `fieldMeta` of a FORM subfield take `partyId` instead of `signerId`, a PRODUCT\_TABLE takes `selectionPartyId` instead of `selectionSignerId`, and `visibilityRule` takes `customFieldId` instead of `customInputId`. This applies to every request and response that carries `fieldMeta`, including `upsert` on the placed-fields endpoints, `initialFields` on `POST /api/v1/templates`, and blocks. `GET /api/v1/documents/:id/field-values` returns `partyId` instead of `signerId`. A request that still sends an old name inside `fieldMeta` or `upsert` fails with `400 VALIDATION_FAILED`.
* Spells every enum inside `fieldMeta` in uppercase, like the rest of the API. A placed box has `kind` `INPUT` or `STATIC`, and its `inputType` (such as `SIGNATURE`), `fillSource` (such as `SIGNER`), and `style.align` are uppercase. So are the `type` of a FORM subfield and of its `fieldMeta` (such as `DATEPICKER`), an attachment's `allowedTypes`, the `type` of an AcroForm field in `formFields` and of a TABLE column, a DURATION field's `durationType` and period `unit`, and `sectionStyle.background` and `sectionStyle.padding`. On `GET` and `PATCH /api/v1/documents/:id/field-values`, `kind` is `FORM`, `PDF_ACROFORM`, or `PDF_PLACED`, `filledBy` is `SENDER` or `SIGNER`, and `type` is uppercase. A request that sends a lowercase value inside `fieldMeta` fails with `400 VALIDATION_FAILED`.
* Names VAT `vat` instead of the Swedish `moms` in product tables. In a PRODUCT\_TABLE field's `fieldMeta`, a product's `moms` is `vat`, `pricing.pricesIncludeMoms`, `defaultMomsRate`, and `showMomsBreakdown` are `pricesIncludeVat`, `defaultVatRate`, and `showVatBreakdown`, the same settings on a column are renamed the same way, the VAT column is keyed `vat`, and `summaryLabels.moms` is `summaryLabels.vat`. In `productTables` on `GET /api/v1/documents/:id` and on webhooks, a row's `moms` and `lineMoms` are `vat` and `lineVat`, and `momsAmount` in the totals is `vatAmount`. Prices and amounts are decimal amounts in the table's currency, as before. A request that still sends a `moms` name fails with `400 VALIDATION_FAILED`.
* `POST /api/v1/documents/:id/fields` and `POST /api/v1/templates/:id/fields` take `{ fields: [...] }`, also for one field, instead of a field or an array of fields, and an issue path starts with `fields.`. The placed-fields endpoints return the whole field, in the shape of `GET /api/v1/documents/:id/fields/:fieldId`. `GET /api/v1/documents/:id/field-values` and `PATCH /api/v1/documents/:id/field-values` return the values in `data` instead of `values`. On `PATCH /api/v1/documents/:id/field-values`, a result's `error` is `{ code, message, userMessage }` instead of a Swedish string, with `code` `NOT_FOUND`, `INVALID_STATE`, or `VALIDATION_FAILED`, and the top-level `success` is removed; check each result's `success`.

### Naming

* Renames the Swedish BankID codes to their eID scheme names, like every other scheme: `SE_BANKID` replaces `BANKID` in `requiredSignature`, and `SE_BANKID_BEFORE_SIGNING` replaces `BANKID_BEFORE_SIGNING` in `twoStepVerification` and `accessVerification`. Requests that send the old codes are rejected, and `GET /api/v1/helpers/signature-methods` and `GET /api/v1/helpers/two-step-verifications` return the new ones.
* Renames the remaining signer fields to party fields. `GET /api/v1/documents/:id/signatures` returns `partyId` and `partyName` instead of `signerId` and `signerName`. The reminder endpoints return `partyId`, `partyEmail`, and `partyName` instead of `signerId`, `signerEmail`, and `signerName`. `GET /api/v1/documents/:id/delegations` returns `originalPartyId`, `delegatePartyId`, `originalParty`, and `delegateParty` instead of `originalSignerId`, `delegateSignerId`, `originalSigner`, and `delegateSigner`. The form endpoints return `respondentPartyId` instead of `respondentSignerId`, and `PUT /api/v1/forms/:id/respondent` takes `partyId` instead of `signerId`.
* Renames the national identity number from `ssn` to `nationalId` on parties, template parties, and contacts, and in the `POST /api/v1/sajn-id` request. `documentMeta.ssnDisplayMode` and `templateMeta.ssnDisplayMode` become `nationalIdDisplayMode`. In `GET /api/v1/documents/:id/signatures`, `identity.match.method` returns `NATIONAL_ID` instead of `SSN`. The `SECURITY_SIGNATURE_IDENTITY_ACCESSED` webhook payload sends `nationalIdDisplayMode` instead of `ssnDisplayMode`. Requests that still send `ssn` or `ssnDisplayMode` are rejected.
* Renames custom field IDs to `customFieldId`. On `POST /api/v1/documents` and `PATCH /api/v1/documents/:id`, send `customFields[].customFieldId` instead of `customFields[].customInputId`. `GET /api/v1/document-categories` returns `customFieldIds` instead of `customInputIds`.
* Uses uppercase enum values everywhere. Workspace members have `status` `ACTIVE` or `INACTIVE`, and the `status` filter on `GET /api/v1/members` takes `ACTIVE` or `INACTIVE`; leave it out for every member. Login sessions on `GET /api/v1/login/sessions/:id` have `status` `PENDING`, `COMPLETED`, `FAILED`, or `EXPIRED`. Removes the event types `DOCUMENT_OPENED`, `DOCUMENT_RECREATED`, `WORKFLOW_STARTED`, `WORKFLOW_COMPLETED`, and `WORKFLOW_FAILED`, which sajn never emits, from the events and webhook deliveries endpoints. To track a party opening a document, use `document.party.opened`; to track changes to a sent document, use `document.updated`.
* Spells the remaining closed enums in uppercase. Form `questions` have `kind` `NAME`, `EMAIL`, `PHONE`, `FIELD`, `TEXT`, `HEADING`, or `PARAGRAPH` and `width` `FULL` or `HALF`; form `slots` have an uppercase `type`, and `publishIssues` have `severity` `BLOCKER` or `WARNING`. A document link's `origin` is `MANUAL`, `DUPLICATE`, or `RENEWAL`. In `documentStyle.theme`, `bodyFont` and `headingFont` (such as `OPEN_SANS`), `density`, and `textSize` are uppercase. A sajn ID verification that was cancelled has `status` `CANCELLED`, the spelling documents use, in responses and in the `status` filter. In `POST /api/v1/documents`, `integrationLink.integration` is `HUBSPOT` and `integrationLink.type` is `DEAL`.
* Renames a form's `title` to `name`, in responses and in `PATCH /api/v1/forms/:id`. `POST /api/v1/forms` returns `200` instead of `201`. `POST /api/v1/forms/:id/unpublish` and `POST /api/v1/forms/:id/rotate-token` return the form instead of `{ success }` and `{ url }`, and `DELETE /api/v1/forms/:id` returns `{ id, deleted: true }`. The form's template moves from `/api/v1/forms/:id/document` to `/api/v1/forms/:id/template`, with `/template/fields` and `/template/parties`, and `PATCH /api/v1/forms/:id/template` returns the template instead of `{ success }`. Its `templateMeta.signingOrder` is `signingMode`, as on templates.
* Moves sajn ID verifications from `/api/v1/sajn-id` to `/api/v1/identity-checks`, the name the `identity_check.*` webhook events and the `identityChecks` quota in `GET /api/v1/limits` use. An identity check has `language`, an ISO 639-1 code such as `sv`, instead of the free-form `locale`; send `language` instead of `locale` when you create one. `createdBy` is `{ id, email, name }`. A check in a list has the same fields as one you get by ID, with `audits`, `data`, and `verificationUrl` set to `null`. The response to the create request no longer returns `token`, which `verificationUrl` already carries. Requests that set `email` or `phone` to an empty string fail with `400 VALIDATION_FAILED`; leave the field out instead.
* Moves a document's chat from `/api/v1/documents/:id/chat` to `/api/v1/documents/:id/messages`. The list returns its messages in `data`, like every other list. A message and a comment have one shape: the text is `body`, in responses and in `POST /api/v1/documents/:id/messages`, instead of a message's `content`, and the author is `author: { type, id, name, email }`, instead of a comment's separate `authorType`. A comment's `author` is always present: `id`, `name`, and `email` are null for the AI assistant or an author who no longer exists.
* Renames the PARALLEL or SEQUENTIAL setting in `templateMeta` from `signingOrder` to `signingMode`, in responses and in `PATCH /api/v1/templates/:id`, so that `signingOrder` names only a party's position. A request that still sends `templateMeta.signingOrder` fails with `400 VALIDATION_FAILED`.
* Folds the contact and company searches into the lists. `GET /api/v1/contacts/search` is removed: pass `email`, `phone`, or `externalId` to `GET /api/v1/contacts`, where every filter you pass must match, instead of any one. `GET /api/v1/companies/search` is removed: pass `orgNumber` or `name` to `GET /api/v1/companies`, again matching every filter. `GET /api/v1/companies/lookup?orgNr=` moves to `GET /api/v1/company-registry/:orgNumber`, which returns `basic.orgNumber` instead of `basic.orgNr`. A contact returns its address (`addressLine1`, `addressLine2`, `postalCode`, `city`, `state`, and `country`), which you can also set when you create or update one, and its `company` is the full company. A company returns `createdAt` and `updatedAt`. `GET /api/v1/companies/:id` no longer returns `contacts`; list them with `GET /api/v1/contacts?companyId=`.
* Gives a member one shape everywhere: `POST /api/v1/members`, `GET /api/v1/members`, `GET /api/v1/members/:userId`, and `PATCH /api/v1/members/:userId` return `{ id, email, name, role, status, lastActiveAt, joinedAt }`, where `id` is the user ID that was `userId`. `POST /api/v1/members` only adds a member of the organization to the workspace and returns `404` for any other email address; it no longer takes `sendInvite` or returns `invited`, `roleId`, `roleName`, `workspaceId`, `organizationId`, or `addedAt`. To invite someone new, call `POST /api/v1/member-invites` with `email` and `roleId`, which returns the invitation. `DELETE /api/v1/members/:userId` and `DELETE /api/v1/member-invites/:id` return `{ id, deleted: true }`, and `POST /api/v1/member-invites/:id/resend` returns the invitation, instead of `{ success: true }`. An invitation's `invitedBy` is `{ id, email, name }`, and so is every other user reference. `GET /api/v1/me` returns the user ID as `id` instead of `userId`. `/api/v1/workspace-roles` is renamed `/api/v1/roles`, and `/api/v1/workspace-permissions` is renamed `/api/v1/permissions`; `DELETE /api/v1/roles/:id` returns `{ id, deleted: true }`. To list the members who can approve documents, call `GET /api/v1/members?permission=APPROVE_DOCUMENT` instead of `GET /api/v1/approvers`.
* Renames the PARALLEL or SEQUENTIAL setting in `documentMeta` from `signingOrder` to `signingMode`, in responses and in `POST /api/v1/documents` and `PATCH /api/v1/documents/:id`, so that `signingOrder` names only a party's position. A request that still sends `documentMeta.signingOrder` fails with `400 VALIDATION_FAILED`.
* Gives every user reference one shape, `{ id, email, name }`. Templates, folders, and blocks return `createdBy` instead of `createdById`, and a form returns `sender` instead of `senderUserId`. A file's `uploadedBy` has `name` instead of `firstName` and `lastName`. In `GET /api/v1/documents/:id/reminders`, `triggeredBy` adds the user's `id`, and it's `null` for a reminder sajn sent automatically, instead of an object with a null `name` and `email`.
* Renames `preferredLanguage` in `documentMeta` and `templateMeta` to `language`, and a block's `locale` to `language`, in responses and requests. `reminderIntervalDays` in `documentMeta` and `templateMeta` is an integer instead of a string: `0` turns reminders off, and `null` uses the default of 3 days. A request that still sends `preferredLanguage` fails with `400 VALIDATION_FAILED`.

### Query strings and pagination

* Reads query parameters as plain strings: send `externalId=12345` or `archived=false` as is, not JSON-encoded. Booleans take `true` or `false`, dates take an ISO 8601 date or date-time (UTC without an offset), and multi-value filters such as `status`, `tagId`, and `type` take a comma-separated list or a repeated parameter. An unknown query parameter returns `400` instead of being ignored. Renames `search` to `query` on `GET /api/v1/templates`, `since` and `until` to `createdAfter` and `createdBefore` on `GET /api/v1/events`, and `dateFrom` and `dateTo` to `createdAfter` and `createdBefore` on `GET /api/v1/identity-checks`, which replaces `GET /api/v1/sajn-id`, and `locale` to `language` on `GET /api/v1/blocks`. `createdBefore` includes its boundary, whereas `until` excluded it, and the identity checks list no longer defaults to the last seven days. `GET /api/v1/companies/:id` returns the company without the `company` wrapper.
* Gives every list one contract. Page with `limit`, from 1 to 100 with a default of 25, and `cursor`, the `nextCursor` of the previous response; `page` and `perPage` are removed, and sending them returns `400`. A list returns `{ data, hasMore, nextCursor }`, and `total` only when you pass `include=total`; `totalPages` is removed. Lists that returned everything in one response are paginated too: `GET /api/v1/folders`, `/document-categories`, `/workspaces`, `/companies`, `/documents/:id/activity`, and `/documents/:id/comments`. Lists bounded by their parent, such as a document's parties, fields, and signatures, and the `/helpers` lists return every item in one response, as `{ data, hasMore: false, nextCursor: null }`. Lists sort newest first by default, including `GET /api/v1/documents`, which sorted by `updatedAt`, and `GET /api/v1/forms` and `/forms/:id/submissions`, which sorted by `updatedAt` ascending; pass `orderBy` and `orderDirection` to change it. Documents page by keyset on every `orderBy`, with empty `completedAt` and `expiresAt` values last. Filters that reference another resource end in `Id` and take several values (`createdBy` is `createdById` on `GET /api/v1/templates`, and `tagId`, `companyId`, `roleId`, and the delivery and submission `status` take a list). `ALL` is no longer a filter value: leave the filter out instead. Block `scope` values are uppercase. `folderId=root` replaces the empty `folderId` for top-level documents and templates. Mutable resources take `createdAfter`, `createdBefore`, `updatedAfter`, and `updatedBefore`, all inclusive.
* Renames the `scope` filter of `GET /api/v1/blocks` to `visibility`, with the values a block's `visibility` takes. A block sajn curates has `visibility: SHARED` instead of `WORKSPACE`, so `visibility=WORKSPACE` returns exactly the blocks that list `WORKSPACE`.

### Errors

* Gives every error response the same shape: `code`, `message`, `userMessage`, `requestId`, and, where they apply, `resource`, `issues`, `requiredScopes`, and `grantedScopes`. `code` is always present and comes from a closed, documented list; branch on it, not on the status or the message. `message` is always English text for developers, and `userMessage`, always present, is the text that's safe to show your users, usually Swedish. `requestId` replaces `errorId` and matches the `Sajn-Request-Id` response header. Each validation issue gains a `code`, its `message` is English, and a `VALIDATION_FAILED` error always lists at least one issue. A `NOT_FOUND` error names the missing resource's type in `resource`, and a path that matches no endpoint returns `ROUTE_NOT_FOUND`. Codes are split or renamed: `INVALID_REQUEST`, `INVALID_BODY`, and `VALIDATION_ERROR` become `VALIDATION_FAILED`; `FORBIDDEN` becomes `PERMISSION_DENIED`, `INSUFFICIENT_SCOPE`, `PLAN_REQUIRED`, or `ACCOUNT_INACTIVE`; `TOO_MANY_REQUESTS` becomes `RATE_LIMITED` or `DAILY_QUOTA_EXCEEDED`; a reused `Idempotency-Key` returns `IDEMPOTENCY_KEY_REUSED` or `IDEMPOTENCY_KEY_IN_USE`; and a failing external service or integration returns `UPSTREAM_UNAVAILABLE`. `401 UNAUTHORIZED` means only that the token is missing, invalid, expired, or revoked. A valid token that isn't allowed to do something returns `403 PERMISSION_DENIED` instead of `401`. A state conflict returns `409 INVALID_STATE` instead of `400`, including editing a document that isn't a draft, editing the fields of a locked template (previously `401`), and downloading a file that isn't produced yet (previously `404`). Other statuses change: sending a document past the monthly limit returns `403 LIMIT_EXCEEDED` instead of `400`, a deactivated user returns `403 ACCOUNT_INACTIVE` instead of `401`, a duplicate unique value returns `409 ALREADY_EXISTS` and an invalid value caught deeper in the API returns `400 VALIDATION_FAILED` instead of `500`, and a failing external service returns a retryable `503 UPSTREAM_UNAVAILABLE` instead of `500`.
* A form's `publishIssues` have an English `message`, and the Swedish text that `message` had is in `userMessage`.

### Webhooks

* Removes the `X-Sajn-Secret` header, which carried the endpoint secret in plain text, from webhook deliveries. Verify deliveries with the Standard Webhooks `webhook-signature` header instead.
* Uses the REST API's names in webhook payloads. On document events, the document has `name` instead of `title`, and `SECURITY_DOCUMENT_DOWNLOADED` and `SECURITY_SIGNATURE_IDENTITY_ACCESSED` have `documentName` instead of `documentTitle`. A party without an email address has `email: null` instead of `""`. In `productTables`, on webhooks and on `GET /api/v1/documents/:id`, `selectionPartyId` replaces `selectionSignerId`. Documents add `expiresAt` and `completedAt`, and parties add `role` and `signedAt`. `DOCUMENT_ARCHIVE_UPLOADED` no longer includes `s3Key`, an internal storage key.
* Gives `DOCUMENT_PARTY_AUTH_FAILED` and `DOCUMENT_PARTY_DELEGATED` the `{ document, party }` payload of every other party event, in place of the flat `documentId`, `documentName`, and `signerId`. `DOCUMENT_PARTY_AUTH_FAILED` keeps `method`, `hintCode`, and `failedAt` next to them, and `method` uses the eID scheme name, such as `SE_BANKID` instead of `BANKID`. `DOCUMENT_PARTY_DELEGATED` moves the delegation into `delegation`, with `id`, `delegate`, `reason`, and `delegatedAt`; read the delegating party from `party` instead of `originalSigner`.
* Changes the webhook delivery body to the event itself, `{ id, type, createdAt, apiVersion, workspaceId, environment, actor, data }`, which `GET /api/v1/events` and `GET /api/v1/events/:id` also return. `type` is a dotted lowercase event name such as `document.party.signed` instead of `DOCUMENT_PARTY_SIGNED`, and the `event` filter of `GET /api/v1/events` becomes `type`. `data.object` is the resource the event is about; a party event adds `data.party`, and the document, previously in `payload.document`, moves to `data.object`. `environment` is `PRODUCTION` or `SANDBOX`, and `actor` is the user, API key or system behind the event, or null. `id` is the event ID and `createdAt` the time of the event, so both stay the same across retries and replays; `webhookEndpoint` is removed. Some events are renamed or merged: `DOCUMENT_SIGNED` is `document.fully_signed`; `DOCUMENT_ARCHIVE_UPLOADED` is no longer sent, because `document.created` reports the same document with `data.source`; the two reminder events become `document.party.reminded` with `data.trigger` set to `AUTOMATIC` or `MANUAL`; `ID_*` events are `identity_check.*`, with `ID_CANCELED` as `identity_check.cancelled`; and `SECURITY_*` events are `security.*`, with the actor in `actor` and without `occurredAt`. `DOCUMENT_MODIFIED` is `document.updated`, and the workspace membership events drop the `workspace_` prefix: `SECURITY_WORKSPACE_MEMBER_REMOVED`, `SECURITY_WORKSPACE_MEMBER_ROLE_CHANGED`, and `SECURITY_WORKSPACE_ROLE_UPDATED` are `security.member_removed`, `security.member_role_changed`, and `security.role_updated`. `previousAttributes` replaces `changedFields` on `template.updated` and `previousExpiresAt` on `document.expiration_extended`. Enum values in `data` are uppercase: `via` on `member.added` and `scope` on `security.documents_exported`. Deliveries are signed according to Standard Webhooks, with the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers in place of `X-Sajn-Signature`, `X-Sajn-Delivery` and `X-Sajn-Environment`, and a new 2026-10 webhook gets a `whsec_` secret.
* Renames a webhook's `webhookUrl` to `url` and `eventTriggers` to `events`, which takes the dotted event types such as `document.completed`. A webhook adds `status` (`ENABLED`, `DISABLED` or `PAUSED`), `pausedAt` and `pauseReason`, and a paused webhook resumes with `POST /api/v1/webhooks/:id/reactivate`. `PATCH /api/v1/webhooks/:id` no longer takes `secret`: rotate the secret with `POST /api/v1/webhooks/:id/rotate-secret`, which signs deliveries with both secrets for 24 hours. A `secret` you pass on create must be a Standard Webhooks secret, `whsec_` followed by base64. `DELETE /api/v1/webhooks/:id` returns `{ id, deleted: true }` instead of `{ success: true }`.
* Lists a webhook's deliveries instead of its attempts in `GET /api/v1/webhooks/:id/deliveries`. A delivery is one event sent to one endpoint, so its `id` is the delivery and every attempt to send it is in `attempts`, with `durationMs` and the request and response headers. A delivery adds `eventId` and `type`, and its `status` is `SUCCESS`, `FAILED` or `PENDING`. The `event` filter becomes `type`, `eventId` filters on one event, and `status` filters on the delivery's status instead of an attempt's. `GET /api/v1/webhooks/:id/deliveries/:deliveryId` and `POST /api/v1/webhooks/:id/deliveries/:deliveryId/retry` take the delivery ID, and the retry returns the new delivery instead of `{ ok, queuedReplayOf }`. `POST /api/v1/events/:id/replay` is removed: to send an event again, retry its delivery to that endpoint. A retry, in every API version, sends the delivery under a new delivery ID.
* Returns a webhook's `secret` only from `POST /api/v1/webhooks` and `POST /api/v1/webhooks/:id/rotate-secret`. No other webhook response returns it. Store the secret when you create the webhook; to replace a lost one, rotate it.
* Makes `data.object` on webhook events the object the REST API returns for the resource, without `expand`, so a field added to the REST object also appears in the event. A document event carries the document as `GET /api/v1/documents/:id` returns it, with `documentMeta`, `tags`, `customFields`, `templateId`, `folderId`, `responsibleUserId` and `approvalRequestId`, and without `organizationId`; `data.party` is the party as `GET /api/v1/documents/:id/parties` lists it, with alpha-2 `country` and a nested `company`. A contact event carries the contact as `GET /api/v1/contacts/:id` returns it, with `postalCode` instead of `zipCode`, and without `companyName` and `workspaceId`. A template event carries the template as `GET /api/v1/templates/:id` returns it. `member.added` carries the member as `GET /api/v1/members/:userId` returns it, with the user ID in `id`, and `via` moves to `data.via`. An identity check event carries the check as `GET /api/v1/identity-checks` lists it, with `status` `CANCELLED` instead of `CANCELED`, and `identity_check.failed` adds `data.failureReason`. Payloads leave out national identity numbers. `template.updated` sets `previousAttributes` to the earlier values of the fields that changed, and `document.created` sets `data.source` for a document created from the email inbox. Security events name users as `{ id, email, name }`: the user who acted is `user`, and the member a membership event is about is `member`, instead of `workspaceMemberId` and `email`. A call by an OAuth application has the actor `{ type: "OAUTH_APP", id }`, with the application's ID.

### Limits

* Removes the deprecated `bankIdSignatures` and `aiTokens` aliases from `GET /api/v1/limits`. Read `signatures.SE_BANKID` instead of `bankIdSignatures`, and `aiBudgetOre`, `aiSpentOre`, and `aiSpentOreSigner` instead of `aiTokens` and `aiTokensSigner`.
* Removes the cost fields from `GET /api/v1/limits`: `quota.additionalBankidCost`, `quota.smsCost`, and `additionalCosts`. They always returned `0`. To track use beyond the plan, compare `usage` with `quota`; the prepaid credit that pays for it is in `balance`.
* Gives the amounts of money in `GET /api/v1/limits` as `{ amount, currency }`, with `amount` in the currency's minor unit, such as öre for `SEK`. Product table prices and totals stay decimal amounts in the table's currency, and `documentMeta.value` and `templateMeta.value` stay free text. In `GET /api/v1/limits`, `balance` is in the organization's currency, and the AI budget fields lose their `Ore` suffix: `quota.aiBudget`, `remaining.aiBudget`, `usage.aiSpent`, and `usage.aiSpentSigner`, always in `SEK`. An unlimited AI budget stays `null`. `GET /api/v1/organization` returns `postalCode` instead of `zipCode`.

### Other changes

* Returns every response property, with `null` when it has no value, instead of leaving it out. This applies to, among others, `fields` on `GET /api/v1/documents/:id` without `expand=fields`, `options` on field values and form slots, `error` on a successful field-value or batch result, `verificationUrl` and `audits` on identity checks, folder counts, and the template settings on `GET /api/v1/forms/:id/template`.
* A custom field's `options` is an array of strings, such as `["Small","Large"]`, instead of a JSON-encoded string, in requests and responses.
* `POST /api/v1/login/sessions` returns the session, with the same fields as `GET /api/v1/login/sessions/:id`, plus `loginUrl` and `expiresAt`, which are null on every other read. In `claims`, a claim your client isn't approved for is `null` instead of missing. `method` names the eID as the rest of the API does, `SE_BANKID` instead of `BANKID_SE`.
* `DELETE /api/v1/contacts/:id` and `DELETE /api/v1/custom-fields/:id` return `{ id, deleted: true }` instead of the deleted object; to keep its content, read it before you delete it. `POST /api/v1/documents/:id/tags` and `DELETE /api/v1/documents/:id/tags/:tagId` return the document, in the shape of `GET /api/v1/documents/:id`, instead of `{ success: true }`. In `PATCH /api/v1/contacts/:id`, `null` clears `email`, `phone`, `nationalId`, `externalId`, and `companyRole`.

## 2026-09

Deprecated: from 2027-10-01, requests on this version return `400 API_VERSION_SUNSET`.

The first dated version: the v1 API as it was on its release date.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.