Valverna

Getting started · Schema v2.1

The unified schema

Every transaction — whether it originated on Stripe, Klarna, Swish, or Shopify — is normalized to the same shape before it ever reaches your code. One immutable document per transaction, all field names in camelCase, all monetary amounts as integer minor units (öre, cents). Refunds and credit notes never mutate the original — they create new linked documents.

Three layers, one shape

Country differences never change the document shape. The schema is built on three layers — the same pattern Peppol uses for international invoicing.

01

Core

Fields that exist everywhere — vendor, items, tax, totals, payment, source. Structurally optional fields may be conditionally required by a rule pack.

02

Country rule packs

Per-country validation keyed by vendor.country + documentTier. A pack may only restrict the core — require fields, constrain values. It never adds fields.

03

Extensions

Country-specific additions live in a namespace — e.g. extensions.se.controlCode. Only extensions add fields, so the core never grows per country.

Every document declares which rule pack it satisfies via profileId. If Sweden requires extensions.se.controlCode, it's required when the profile is an se-* profile and absent everywhere else.

Profiles

A profile = (country, document tier, version). Together with documentTier (receipt today; invoice tiers reserved), it tells you exactly which validation rules a document passed.

se-receipt-1 Active

Sweden, receipt tier. Validated by the write API — kassaregister fields, SEK, SE_MOMS rates.

core-receipt-1 Active

The minimum core. Applied as fallback when no country pack exists for the business country.

no / dk / fi / uk / us-receipt-1 Active

Live — validated by the write API against each country's VAT/format/rounding rules.

*-simplified_invoice-* / *-full_invoice-* Reserved

Invoice tiers land later as rule packs — no schema changes required.

Canonical shape

A Swedish café sale in May 2026 — food at the temporary 6% rate, merchandise at 25%, paid cash (so totals.rounding is present). This is the exact shape returned by every read endpoint.

Transaction object

{
  "id": "txn_a1b2c3d4",
  "schemaVersion": "2.1",
  "profileId": "se-receipt-1",
  "documentTier": "receipt",
  "type": "sale",
  "settled": true,
  "receiptNumber": "R-2026-004521",
  "timestamp": "2026-05-12T14:32:15+02:00",
  "createdAt": "2026-05-12T12:32:18Z",
  "currency": "SEK",

  "vendor": {
    "id": "vendor_abc123",
    "name": "Kaffekoppen Södermalm",
    "legalName": "Kaffekoppen AB",
    "orgNumber": "556789-1234",
    "vatNumber": "SE556789123401",
    "country": "SE",
    "address": {
      "line1": "Götgatan 45",
      "city": "Stockholm",
      "postalCode": "118 26",
      "country": "SE"
    },
    "locationName": "Kaffekoppen Södermalm"
  },

  "customer": null,

  "items": [
    {
      "name": "Cappuccino",
      "quantity": 2,
      "unitPrice": 4900,
      "amount": 9800,
      "taxRate": 6,
      "taxCategory": "S",
      "taxAmount": 555,
      "category": "food_beverage"
    },
    {
      "name": "Kanelbulle",
      "quantity": 1,
      "unitPrice": 3500,
      "amount": 3500,
      "taxRate": 6,
      "taxCategory": "S",
      "taxAmount": 198,
      "category": "food_beverage"
    },
    {
      "name": "Tygkasse med logga",
      "quantity": 1,
      "unitPrice": 14900,
      "amount": 14900,
      "taxRate": 25,
      "taxCategory": "S",
      "taxAmount": 2980,
      "category": "merchandise"
    }
  ],

  "tax": {
    "scheme": "SE_MOMS",
    "pricesIncludeTax": true,
    "summary": [
      { "rate": 6, "category": "S", "label": "Moms 6%", "taxableAmount": 12547, "taxAmount": 753 },
      { "rate": 25, "category": "S", "label": "Moms 25%", "taxableAmount": 11920, "taxAmount": 2980 }
    ]
  },

  "totals": {
    "subtotal": 28200,
    "discount": 0,
    "tax": 3733,
    "rounding": 0,
    "total": 28200
  },

  "payment": { "method": "cash" },

  "extensions": {
    "se": {
      "controlUnitId": "CU-SE-20241234",
      "controlCode": "ABCD 1234 EFGH 5678",
      "sequenceNumber": 4521
    }
  },

  "source": {
    "platform": "izettle",
    "externalId": "iz_pay_9f8e7d6c",
    "syncedAt": "2026-05-12T12:32:18Z"
  },

  "locale": "sv-SE"
}

Field groups

Fields are organized into four semantic groups.

Identity

id, schemaVersion, profileId, documentTier, type, settled, receiptNumber, timestamp, createdAt

Parties

vendor (snapshot of the business), customer (when known)

Money

items[], tax, totals, payment, currency

Provenance

source, extensions, linkedTransaction, locale, metadata

Transaction types

There is no mutable status field. A document has a type, a settled boolean (has the money changed hands?), and — for everything except plain sales — a linkedTransaction pointing at the original.

sale

Completed purchase or conversion. totals.total is non-negative; 0 represents a free or fully discounted sale.

refund

Full or partial return. totals.total = amount going back to the customer (positive). Links to the original sale.

void

Cancellation before settlement (same day). totals.total = 0, settled = false. Links to the voided sale.

credit_note

Formal price reduction without returning goods. Common in B2B. Links to the original document.

exchange

Return + new purchase in one transaction. items mixes negative and positive amounts; totals.total is the net.

Tax

tax.scheme required
Selects the rate table that validates this document. SE_MOMS, NO_MVA, DK_MOMS, FI_ALV, EU_VAT, UK_VAT, US_SALES_TAX. When a Nordic vendor (SE/NO/DK/FI) uses EU_VAT (the EU OSS scheme), the sale must be a genuine cross-border distance sale: customer.country is required, must be a valid ISO 3166-1 alpha-2 code, and must differ from the vendor's country.
tax.pricesIncludeTax required
true in EU/Nordic (VAT-inclusive). false in US (tax added at checkout).
tax.reverseCharge optional
true for B2B reverse charge (EU cross-border). Required when any line uses category AE.
tax.summary required
Breakdown by (rate, category) pair. Each entry: rate, category, optional label (e.g. "Moms 25%"), taxableAmount, taxAmount (minor units).
tax.jurisdictions optional
US only. Multi-level breakdown. Each entry: level (state, county, city, district), name, rate, taxAmount.

Tax categories

Every line item carries a taxCategory, and every summary entry a category. A bare taxRate: 0 is ambiguous — the code carries the legal distinction. Codes follow UN/CEFACT UNCL5305.

S

Standard/reduced rate

Any positive rate from the scheme’s table.

Z

Zero-rated

0% with right of deduction — exports, intra-EU supplies, UK zero-rated food.

E

Exempt

No VAT, no deduction right — healthcare, education, financial services.

AE

Reverse charge

Buyer accounts for VAT. Requires tax.reverseCharge: true.

O

Out of scope

Not subject to the tax — US non-taxable lines, non-business.

Payment & cash rounding

The payment object is the legal minimum — receipt law in SE/NO/DK requires the tender type — never processor event data. Authorization codes, settlement batches, and acquirer references don't belong here.

payment.method required
cash, card, mobile, transfer, other. mobile covers Swish, Vipps, MobilePay.
payment.last4 optional
Last 4 digits of the card PAN, for receipt matching. Never more digits, never an expiry date.
totals.rounding optional
Cash rounding adjustment, signed, minor units. Required when payment.method == "cash" — SEK rounds the total to the nearest whole krona. Applies to the grand total only, never to the VAT base. Sweden’s öresavrundning is statutory and may also appear on card/other tenders (still landing on the krona grid); for NO/DK/FI electronic tenders settle exact, so it is absent or 0.

Extensions

Country-specific additions are namespaced under extensions so they can never collide with the core or each other. Namespaces are lowercase country codes. This replaces the old top-level fiscal object from schema v2.0.

extensions.se — Sweden's kassaregister fields, required by se-receipt-1:

extensions.se.controlUnitId required
Certified kontrollenhet serial number — or the control-system code when a cloud kontrollsystem is used.
extensions.se.controlCode required
Kontrollkod generated by the control unit/system for this receipt.
extensions.se.sequenceNumber required
Sequential receipt counter from the control unit / POS register.

Reserved namespaces: no (chained fiscal signatures), dk, fi, uk, us.