How a product table is structured
A product table is a document field of typePRODUCT_TABLE. The field’s fieldMeta object holds the whole table:
Before you begin
- Store your API key in the
SAJN_API_KEYenvironment variable. To create a key, go to workspace settings in the sajn app, then Utvecklare (Developer) > API-nycklar (API keys). - Have a
DRAFTdocument. For more information, see Create a document.
Create a product table
1
Add the table to the document
Send a Replace
POST request to /api/v1/documents/DOCUMENT_ID/fields with a PRODUCT_TABLE field in the fields array:DOCUMENT_ID with the document ID. The response has the created field in data. Store its id to update the table later.price is a decimal amount in the table’s currency, such as 1499.5, not minor units, and vat is a rate in percent. position is the field’s 0-based place among the document’s content blocks, not a coordinate. To create several fields in one request, add more entries to the fields array.2
Check the totals
Get the document. Its Each row has
productTables array has every table with its rows and the totals that sajn calculated:selected, quantity, vat, lineTotal, lineNet, and lineVat, and each table has totals with subtotal, discountAmount, vatAmount, total, and payable. Amounts are decimal amounts in the table’s currency. After signing, the rows carry the party’s final choices. Document webhook events carry the same productTables array in data.object, for example document.party.signed, document.fully_signed, and document.completed. For more information, see Webhook payloads.DRAFT. A request against a sent document returns 409 INVALID_STATE.
Product rows
Each entry inproducts is one row. Only id and name are required, but a row without a price value contributes nothing to the totals.
Columns
Thecolumns object decides which columns render and what they’re called. Each key maps to an object with name, originalName, key, enabled, and settings.
The key value must be a product property or lineTotal, which is calculated rather than stored. To hide a column without deleting it, set enabled to false. The originalName value keeps the column’s original label so that sajn can retranslate a column you haven’t renamed.
The VAT column is keyed vat. The settings object accepts decimalPlaces, pricePrefix, and priceSuffix. The price and VAT columns also accept pricesIncludeVat, defaultVatRate, and showVatBreakdown, but set those through pricing instead. sajn reads the column settings only for tables authored before pricing existed.
Pricing and VAT
Thepricing object controls how sajn reads your prices:
A request that uses a 2026-09 name, such as
moms or pricesIncludeMoms, fails with 400 VALIDATION_FAILED. Set pricesIncludeVat before you enter prices. It changes what every price value means, so switching it later changes every total in the table.
Discounts
A discount is an object with atype of PERCENT or AMOUNT and a numeric value. A PERCENT discount takes that percentage off; an AMOUNT discount takes that many currency units off. sajn never discounts below zero.
Set discount on a product row to reduce that row, or on the table to reduce the whole table. A table discount also accepts a label string, which replaces the default summary label.
An AMOUNT discount is measured in the basis you entered prices in. On a table with pricesIncludeVat set to true, a 500 kr discount takes 500 kr off the total the party pays. On a table with prices excluding VAT, the same discount takes 500 kr off the subtotal, and VAT is charged on what’s left.
How totals are calculated
sajn calculates each included row, then applies the table discount:net is the row’s amount excluding VAT, and VAT is net × vatRate / 100. When prices include VAT, sajn divides the VAT back out: the amount excluding VAT is net / (1 + vatRate / 100), and VAT is the remainder.
The table’s subtotal value is the sum of every row excluding VAT. sajn then applies the table discount and scales VAT by the same proportion:
Let a recipient choose
To turn a fixed price list into an offer that a party responds to, setallowSelection to true and name the party’s ID in selectionPartyId. Mark the rows you want to offer with optional. The party fills in their choice before they sign, and the totals follow it.
To block signing until the party has chosen, set selectionRequired to true. A table where nothing is selectable counts as answered, so selectionRequired never blocks a party on a fixed price list.
To let the party set a quantity, set quantityEditable on the row and bound it with quantityMin and quantityMax. sajn clamps whatever the party enters to that range.
PARTY_ID with the ID of the party who makes the choice.
In API version
2026-09, this property is selectionSignerId. For every change, see Upgrading to 2026-10.Recipient choices
TherecipientChoice object records what the party chose:
defaultSelected decides which optional rows count.
Group rows into sections
A section groups rows under a heading. Each entry insections needs an id and a name, and each row joins a section through sectionId.
A section with
pickOne set to true presents its rows as alternatives. Give one row defaultSelected so that the section has an answer before the party responds:
Pick-one applies whether or not
allowSelection is set. A pick-one section with no chosen row and no defaultSelected row contributes nothing to the document, so always mark a default.Bill on an interval
Setbilling on a row to MONTHLY, QUARTERLY, or YEARLY to charge it on that interval. Rows without a billing value are one-time charges.
As soon as one row is recurring, the summary adds a total per interval alongside the grand total, so the party sees what they pay once and what they pay each period.
ROT and RUT deductions
Set a row’sproductType to ROT or RUT to mark labor that qualifies for the Swedish tax deduction. sajn deducts 30% of a ROT row and 50% of a RUT row, both measured including VAT, and reports the result as the payable amount.
The deduction is per row. Mark the labor rows and leave materials as GOODS, because materials don’t qualify.
Update a product table
To change a table, send aPATCH request with the complete fieldMeta object. The request replaces the metadata rather than merging into it, so include every property you want to keep:
DOCUMENT_ID: the document ID.FIELD_ID: the product table field’sid.
Handle errors
Each error response has acode. A VALIDATION_FAILED error lists every problem in issues, each with the path of the property that failed. On a POST request, each path starts with fields.N., where N is the field’s index in the request. For every code, see Errors.
Next steps
Create a document
Create the document that holds the table.
Templates and forms
Reuse a product table across documents.
Document fields API
Read the full field reference.
Send for signing
Send the document to its parties.

