Skip to main content
A product table presents products or services with pricing inside a document. Use one for quotes, order forms, service agreements, and any document where the party needs to see what they’re paying for. A product table can also ask a party to choose. Mark rows as optional, group alternatives so that the party picks one, or let the party set a quantity. sajn recalculates the totals from what the party chose and seals those amounts into the signed PDF file.

How a product table is structured

A product table is a document field of type PRODUCT_TABLE. The field’s fieldMeta object holds the whole table:

Before you begin

  • Store your API key in the SAJN_API_KEY environment variable. To create a key, go to workspace settings in the sajn app, then Utvecklare (Developer) > API-nycklar (API keys).
  • Have a DRAFT document. For more information, see Create a document.

Create a product table

1

Add the table to the document

Send a POST request to /api/v1/documents/DOCUMENT_ID/fields with a PRODUCT_TABLE field in the fields array:
Replace 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 productTables array has every table with its rows and the totals that sajn calculated:
Each row has 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.
You can add and change fields only while the document is a DRAFT. A request against a sent document returns 409 INVALID_STATE.

Product rows

Each entry in products is one row. Only id and name are required, but a row without a price value contributes nothing to the totals.

Columns

The columns 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

The pricing 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 a type 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:
When prices exclude VAT, 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:
For the three rows in Create a product table, with prices excluding VAT and a 10% table discount:

Let a recipient choose

To turn a fixed price list into an offer that a party responds to, set allowSelection 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.
Replace 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

The recipientChoice object records what the party chose:
sajn writes this object when the party fills in the document, and reads it back when it calculates the totals and renders the signed PDF file. You can read it through the API, but you can’t submit a choice on the party’s behalf. Before the party answers, defaultSelected decides which optional rows count.

Group rows into sections

A section groups rows under a heading. Each entry in sections 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

Set billing 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’s productType 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 a PATCH request with the complete fieldMeta object. The request replaces the metadata rather than merging into it, so include every property you want to keep:
Replace the following:
  • DOCUMENT_ID: the document ID.
  • FIELD_ID: the product table field’s id.
The response is the updated field.

Handle errors

Each error response has a code. 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.