{
  "openapi": "3.1.0",
  "info": {
    "title": "Debet & Kredit API",
    "version": "0.1.0",
    "summary": "API v1, kontraktsversion 1.",
    "description": "Din installation har ett eget API. Du skapar nyckeln själv under\nInställningar → API-nycklar, med ett klick — ingen ansökan, inget\npartneravtal, ingen granskning. Din server, dina nycklar.\n\n**Bas-URL.** Varje installation har sin egen adress. Byt ut\n`din-installation.se` mot din.\n\n**Behörigheter.** En nyckel bär en eller flera av:\n- `data:read` (Läsa) — Hämtar fakturor, kunder, verifikat och nyckeltal. Ändrar ingenting.\n- `intake:write` (Ta emot underlag) — Lämnar in ordrar och e-fakturor till inkorgen. Ser inget som redan finns.\n- `ledger:write` (Bokföra) — Skapar och bokför fakturor genom samma spärrar som programmet självt: momslås, låsta perioder och avslutade räkenskapsår gäller lika.\n\n**Spärrarna gäller lika.** Momslås, oföränderliga verifikat och\navslutade räkenskapsår gäller för API:et precis som för gränssnittet.\nEtt anrop som stoppas av ett lås svarar 422 med skälet utskrivet — det\när en egenskap hos ett bokföringsprogram, inte en begränsning i API:et.",
    "license": {
      "name": "Se LICENSE i repot"
    }
  },
  "servers": [
    {
      "url": "https://{installation}",
      "description": "Din egen installation.",
      "variables": {
        "installation": {
          "default": "din-installation.se",
          "description": "Adressen till din installation av Debet & Kredit."
        }
      }
    }
  ],
  "tags": [
    {
      "name": "Upptäckt",
      "description": "Vad installationen är och vad nyckeln får göra."
    },
    {
      "name": "Läsning",
      "description": "Hämta siffror och affärshändelser."
    },
    {
      "name": "Skrivning",
      "description": "Skapa och bokföra."
    },
    {
      "name": "Inkommande",
      "description": "Ordrar och e-fakturor in."
    },
    {
      "name": "Byrå",
      "description": "Byråportalens frysta kontrakt."
    }
  ],
  "security": [
    {
      "ApiKey": []
    }
  ],
  "paths": {
    "/api/v1/meta": {
      "get": {
        "operationId": "v1-meta",
        "summary": "Vad den här installationen är och vad din nyckel får göra",
        "description": "I en självhostad modell kör varje kund sin egen version. Utan ett upptäcktsanrop måste integratören gissa vad installationen kan — det här svaret säger det i stället. Rutten kräver ingen särskild behörighet utöver en giltig nyckel, eftersom en nyckel annars inte skulle kunna ta reda på vad den själv får göra. Den dubbeltjänar som \"testa nyckeln\": ett 200 här betyder att nyckeln lever.\n\n**Takt:** 600 anrop per timme och nyckel (ställbart per nyckel).",
        "tags": [
          "Upptäckt"
        ],
        "responses": {
          "200": {
            "description": "Installationens uppgifter och nyckelns behörigheter.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "api_version": "v1",
                  "schema_version": 1,
                  "app_version": "0.1.0",
                  "installation_schema_version": 112,
                  "key": {
                    "name": "Webshoppen",
                    "scopes": [
                      "data:read",
                      "intake:write"
                    ]
                  },
                  "fiscal_years": [
                    {
                      "year": 2026,
                      "start": "2026-01-01",
                      "end": "2026-12-31",
                      "status": "open"
                    }
                  ],
                  "period_locked_to": "2026-03-31",
                  "vat_due_date": "2026-05-12",
                  "endpoints": [
                    {
                      "method": "GET",
                      "path": "/api/v1/verifikat",
                      "scope": "data:read"
                    }
                  ]
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller okänd nyckel.\n\n`key_revoked` — Nyckeln är återkallad av installationens ägare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`server_misconfigured` — Installationen saknar konfiguration för API:et.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/verifikat": {
      "get": {
        "operationId": "v1-verifikat",
        "summary": "Affärshändelser med sina rader",
        "description": "Verifikat i datumordning, med raderna inbakade i varje post. Det här är den enda vägen till affärshändelser rad för rad — övriga läsrutter svarar med aggregat. Sidindelningen är markörbaserad: skicka tillbaka `next_cursor` för nästa sida. En markör är stabil även om nya verifikat tillkommer under tiden, vilket ett sidnummer inte är.\n\n**Takt:** 600 anrop per timme och nyckel (ställbart per nyckel).\n\n**Behörighet:** `data:read` (Läsa)",
        "tags": [
          "Läsning"
        ],
        "x-required-scope": "data:read",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Tidigaste verifikationsdatum (ÅÅÅÅ-MM-DD).",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-01-01"
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Senaste verifikationsdatum (ÅÅÅÅ-MM-DD).",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-03-31"
          },
          {
            "name": "serie",
            "in": "query",
            "required": false,
            "description": "Verifikationsseriens kod, till exempel A eller F.",
            "schema": {
              "type": "string"
            },
            "example": "A"
          },
          {
            "name": "konto",
            "in": "query",
            "required": false,
            "description": "Ta bara med verifikat som har en rad på det här kontot.",
            "schema": {
              "type": "integer"
            },
            "example": 3001
          },
          {
            "name": "id",
            "in": "query",
            "required": false,
            "description": "Hämta ett enskilt verifikat.",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "example": "8f14e45f-ceea-467a-9a3a-1f2b3c4d5e6f"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Antal verifikat per sida, 1–200. Standard 50.",
            "schema": {
              "type": "integer"
            },
            "example": 50
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Markören ur föregående svars `next_cursor`.",
            "schema": {
              "type": "string"
            },
            "example": "MjAyNi0wMy0xMnw4ZjE0"
          }
        ],
        "responses": {
          "200": {
            "description": "En sida verifikat.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "data": [
                    {
                      "id": "8f14e45f-ceea-467a-9a3a-1f2b3c4d5e6f",
                      "number": 12,
                      "series": "A",
                      "verification_date": "2026-03-12",
                      "description": "Faktura 2026-0042",
                      "counterparty": "Nordisk Design AB",
                      "source": "invoice",
                      "rows": [
                        {
                          "row_no": 1,
                          "account": 1510,
                          "debit": 12500,
                          "credit": 0,
                          "note": null
                        },
                        {
                          "row_no": 2,
                          "account": 3001,
                          "debit": 0,
                          "credit": 10000,
                          "note": null
                        },
                        {
                          "row_no": 3,
                          "account": 2611,
                          "debit": 0,
                          "credit": 2500,
                          "note": null
                        }
                      ]
                    }
                  ],
                  "next_cursor": null,
                  "has_more": false
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request` — En parameter går inte att läsa — svaret säger vilken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller okänd nyckel.\n\n`key_revoked` — Nyckeln är återkallad av installationens ägare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` — Nyckeln är giltig men saknar behörigheten rutten kräver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`server_misconfigured` — Installationen saknar konfiguration för API:et.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "v1-verifikat-skapa",
        "summary": "Bokför en affärshändelse från ditt eget system",
        "description": "Skriver ett verifikat genom motorns egen bokföringsfunktion — samma väg som bokföringsformuläret i programmet, samma spärrar, samma nummerserier. Det här är den generella skrivvägen in i huvudboken: kundfakturarutten tar en faktura, orderintaget tar en butiksorder, och den här tar en affärshändelse av vilket slag som helst.\n\n**Varför den finns.** Vi bygger med flit inga färdiga kopplingar mot enskilda butiks- eller betalplattformar — en adapter mot någon annans API är löpande drift, inte en funktion. Du får i stället ett generiskt format och ett öppet API, och kopplingen byggs en gång och ägs av dig. Den här rutten är det som gör det möjligt.\n\n**Spärrarna gäller lika.** Balanskravet (debet = kredit), kravet på ett belopp skilt från noll, minst två rader, att kontot finns och är aktivt, periodlåset, momslåset och avslutade räkenskapsår — allt ligger inuti motorns funktion och gäller för API:et precis som för dig själv i gränssnittet. Ett anrop som stoppas av ett lås får 422 med skälet utskrivet.\n\n**Rutten skapar aldrig konton.** Ett konto som saknas i kontoplanen eller är avaktiverat ger 422 med kontonumret utskrivet. Lägg upp kontot under Kontoplan först.\n\n**Nyckeln behöver båda behörigheterna: Bokföra och Läsa.** Radernas konton slås upp i kontoplanen för att momsen ska kunna kontrolleras innan verifikatet skrivs, och den uppslagningen är en läsning. En nyckel med enbart Bokföra får 403 med det beskedet i klartext.\n\n**Idempotensen är inte valfri, och skälet är att ett verifikat inte går att ta bort.** Skicka din egen externa referens — ordernumret, körningens id — som `Idempotency-Key`. Samma referens igen ger samma verifikat tillbaka i stället för ett andra.\n\n**Takt:** 600 anrop per timme och nyckel (ställbart per nyckel).\n\n**Behörighet:** `ledger:write` (Bokföra)",
        "tags": [
          "Skrivning"
        ],
        "x-required-scope": "ledger:write",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "8–255 tecken du väljer själv, unikt per affärshändelse. Använd din egen externa referens: samma referens igen ger samma verifikat i stället för ett andra.",
            "schema": {
              "type": "string"
            },
            "example": "kassa-2026-03-12"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "seriesCode",
                  "date",
                  "description",
                  "rows"
                ],
                "properties": {
                  "seriesCode": {
                    "type": "string",
                    "description": "Verifikationsseriens kod, högst två tecken. Serien måste finnas för räkenskapsåret datumet ligger i.",
                    "example": "A"
                  },
                  "date": {
                    "type": "string",
                    "format": "date",
                    "description": "Verifikationsdatum (ÅÅÅÅ-MM-DD). Avgör räkenskapsår, period och vilka lås som prövas.",
                    "example": "2026-03-12"
                  },
                  "description": {
                    "type": "string",
                    "description": "Vad affärshändelsen avser. Står i huvudboken och kan inte ändras i efterhand.",
                    "example": "Dagskassa 12 mars"
                  },
                  "counterparty": {
                    "type": "string",
                    "description": "Motparten, om det finns en.",
                    "example": "Kortinlösen AB"
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Minst två rader med account, debit och credit. Frivilligt per rad: note, cost_center och project (dimensionernas uuid, inte deras koder)."
                  }
                }
              },
              "example": {
                "seriesCode": "A",
                "date": "2026-03-12",
                "description": "Dagskassa 12 mars",
                "counterparty": "Kortinlösen AB",
                "rows": [
                  {
                    "account": 1930,
                    "debit": 12500,
                    "credit": 0,
                    "note": "Insättning"
                  },
                  {
                    "account": 3001,
                    "debit": 0,
                    "credit": 10000
                  },
                  {
                    "account": 2611,
                    "debit": 0,
                    "credit": 2500
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verifikatet bokfördes — eller är sedan tidigare bokfört med samma Idempotency-Key. Svaret bär numret och en länk till posten.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "8f14e45f-ceea-467a-9a3a-1f2b3c4d5e6f",
                  "series": "A",
                  "number": 12,
                  "label": "A12",
                  "verification_date": "2026-03-12",
                  "url": "https://din-installation.se/verifikat/8f14e45f-ceea-467a-9a3a-1f2b3c4d5e6f"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "`idempotency_required` — Huvudet Idempotency-Key saknas.\n\n`invalid_request` — Kroppen går inte att läsa, balanserar inte, saknar belopp, har färre än två rader, eller ber om en källa som bara programmet självt får sätta. Svaret säger vilket.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller okänd nyckel.\n\n`key_revoked` — Nyckeln är återkallad av installationens ägare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` — Nyckeln är giltig men saknar behörigheten rutten kräver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — Samma Idempotency-Key har redan använts med en annan kropp.\n\n`idempotency_in_progress` — Ett anrop med samma Idempotency-Key behandlas just nu. Skicka om exakt samma anrop om en stund — du får då det första svaret. Byt INTE nyckel: det första anropet kan mycket väl ha bokfört.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large` — Kroppen är större än 512 kB. Ett verifikat är några kilobyte; taket stoppar en kropp som inte är ett.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "422": {
            "description": "`period_locked` — Perioden är låst, momslåst eller räkenskapsåret avslutat. Verifikatet är läst men kan inte bokföras — svaret säger vilken spärr som gäller.\n\n`unprocessable` — Motorn tog emot verifikatet men kunde inte bokföra det: ett konto som saknas eller är avaktiverat, en verifikationsserie som inte finns för året, ett datum utan räkenskapsår, eller en dimension som inte är aktiv.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`server_misconfigured` — Installationen saknar konfiguration för API:et.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/stats/overview": {
      "get": {
        "operationId": "stats-overview",
        "summary": "Komplett ekonomisk lägesbild",
        "description": "Månadsresultat, tjänste- och kundfördelning, kostnad per leverantör, marginal och likviditet i ett svar. Alla belopp i SEK exklusive moms, avrundade till hela kronor utom likviditetssaldon som bär ören för exakt avstämning. Det här är API:ets tyngsta läsning — den går igenom hela huvudboken, så anropa den med förnuft och mellanlagra på din sida.\n\n**Takt:** 600 anrop per timme och nyckel (ställbart per nyckel).\n\n**Behörighet:** `data:read` (Läsa)\n\n**Bakåtkompatibelt:** miljövariabeln `STATS_API_KEY` fortsätter fungera parallellt.",
        "tags": [
          "Läsning"
        ],
        "x-required-scope": "data:read",
        "responses": {
          "200": {
            "description": "Lägesbilden.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "generated_at": "2026-09-05T08:00:00.000Z",
                  "months": [
                    {
                      "month": "2026-01",
                      "revenue": 125000,
                      "costs": 61000,
                      "result": 64000
                    }
                  ],
                  "totals": {
                    "revenue": 1250000,
                    "costs": 610000,
                    "result": 640000
                  },
                  "attachments_missing": 3
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller okänd nyckel.\n\n`key_revoked` — Nyckeln är återkallad av installationens ägare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` — Nyckeln är giltig men saknar behörigheten rutten kräver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "`db_error` — Underlaget kunde inte läsas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "`server_misconfigured` — Installationen saknar konfiguration för API:et.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/stats/monthly": {
      "get": {
        "operationId": "stats-monthly",
        "summary": "Resultatserie per månad",
        "description": "Intäkter, kostnader och resultat per månad, i SEK exklusive moms och hela kronor. Konton 3000–7999.\n\n**Takt:** 600 anrop per timme och nyckel (ställbart per nyckel).\n\n**Behörighet:** `data:read` (Läsa)\n\n**Bakåtkompatibelt:** miljövariabeln `STATS_API_KEY` fortsätter fungera parallellt.",
        "tags": [
          "Läsning"
        ],
        "x-required-scope": "data:read",
        "responses": {
          "200": {
            "description": "Månadsserien.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "generated_at": "2026-09-05T08:00:00.000Z",
                  "months": [
                    {
                      "month": "2026-01",
                      "revenue": 125000,
                      "costs": 61000,
                      "result": 64000
                    }
                  ],
                  "totals": {
                    "revenue": 1250000,
                    "costs": 610000,
                    "result": 640000
                  }
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller okänd nyckel.\n\n`key_revoked` — Nyckeln är återkallad av installationens ägare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` — Nyckeln är giltig men saknar behörigheten rutten kräver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "`db_error` — Underlaget kunde inte läsas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "`server_misconfigured` — Installationen saknar konfiguration för API:et.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/stats/daily": {
      "get": {
        "operationId": "stats-daily",
        "summary": "Daglig intäktsstatistik",
        "description": "En post per kalenderdag i intervallet, även dagar utan händelser. Högst 90 dagar per anrop. `unpaid_amount` är ett ÖGONBLICKSVÄRDE — utestående kundfordringar just nu — och upprepas därför identiskt i varje dagpost. Summera aldrig den kolumnen; läs den ur valfri rad.\n\n**Takt:** 600 anrop per timme och nyckel (ställbart per nyckel).\n\n**Behörighet:** `data:read` (Läsa)\n\n**Bakåtkompatibelt:** miljövariabeln `STATS_API_KEY` fortsätter fungera parallellt.",
        "tags": [
          "Läsning"
        ],
        "x-required-scope": "data:read",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "Intervallets första dag (ÅÅÅÅ-MM-DD).",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-03-01"
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "Intervallets sista dag (ÅÅÅÅ-MM-DD). Högst 90 dagar från `from`.",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "example": "2026-03-31"
          }
        ],
        "responses": {
          "200": {
            "description": "En post per dag i intervallet. Svaret är en array, inte ett objekt.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": [
                  {
                    "date": "2026-03-01",
                    "revenue_total": 12000,
                    "invoices_count": 3,
                    "unpaid_amount": 48000
                  },
                  {
                    "date": "2026-03-02",
                    "revenue_total": 0,
                    "invoices_count": 0,
                    "unpaid_amount": 48000
                  }
                ]
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Datumen går inte att läsa, eller intervallet är längre än 90 dagar.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller okänd nyckel.\n\n`key_revoked` — Nyckeln är återkallad av installationens ägare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` — Nyckeln är giltig men saknar behörigheten rutten kräver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`server_misconfigured` — Installationen saknar konfiguration för API:et.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/kundfakturor": {
      "post": {
        "operationId": "v1-kundfakturor",
        "summary": "Skapa ett fakturautkast, och bokför det om du vill",
        "description": "Tar en färdig faktura mot en KÄND kund — det är skillnaden mot orderintaget, som tar en butiksorder som ska tolkas. Utkastet skrivs genom samma väg som fakturaformuläret, med samma kontroll mellan radens momssats och intäktskontots momskod. Med `\"book\": true` bokförs fakturan direkt: nummer, OCR och verifikat sätts i en transaktion.\n\nBokföringen går genom motorns egna funktioner, vilket betyder att momslås, oföränderliga verifikat och avslutade räkenskapsår gäller lika för API:et som för dig själv i gränssnittet. Ett anrop som stoppas av ett lås får 422 med skälet utskrivet — det är en egenskap hos ett bokföringsprogram, inte en brist i API:et.\n\n**Nyckeln behöver båda behörigheterna: Bokföra och Läsa.** Utkastet slås upp mot kunden för att veta momstyp, och mot intäktskontots momskod för att kunna jämföra den med radens momssats — båda uppslagen är läsningar. En nyckel med enbart Bokföra får 403 med det beskedet i klartext.\n\n**Takt:** 600 anrop per timme och nyckel (ställbart per nyckel).\n\n**Behörighet:** `ledger:write` (Bokföra)",
        "tags": [
          "Skrivning"
        ],
        "x-required-scope": "ledger:write",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "8–255 tecken som du väljer själv, unikt per faktura. Samma nyckel igen ger det sparade svaret i stället för en andra faktura.",
            "schema": {
              "type": "string"
            },
            "example": "order-2026-0042"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "customerId",
                  "invoiceDate",
                  "paymentTerms",
                  "rows"
                ],
                "properties": {
                  "customerId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Kundens id. Kunden måste finnas — den här rutten skapar aldrig kunder.",
                    "example": "3c9a1b2d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
                  },
                  "invoiceDate": {
                    "type": "string",
                    "format": "date",
                    "description": "Fakturadatum (ÅÅÅÅ-MM-DD).",
                    "example": "2026-03-12"
                  },
                  "paymentTerms": {
                    "type": "integer",
                    "description": "Betalningsvillkor i dagar, 0–90. Förfallodagen räknas i kalendern.",
                    "example": 30
                  },
                  "yourReference": {
                    "type": "string",
                    "description": "Kundens referens."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Fritext på fakturan."
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Minst en rad med description, quantity, unitPrice, vatRate och account."
                  },
                  "book": {
                    "type": "boolean",
                    "description": "true bokför fakturan direkt. Utelämnad eller false lämnar den som utkast.",
                    "example": false
                  }
                }
              },
              "example": {
                "customerId": "3c9a1b2d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                "invoiceDate": "2026-03-12",
                "paymentTerms": 30,
                "rows": [
                  {
                    "description": "Konsultation mars",
                    "quantity": 10,
                    "unitPrice": 1000,
                    "vatRate": 25,
                    "account": 3041
                  }
                ],
                "book": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Fakturan skapades — eller är sedan tidigare skapad med samma Idempotency-Key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "id": "7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a",
                  "status": "draft",
                  "invoice_number": null,
                  "net_amount": 10000,
                  "vat_amount": 2500,
                  "total_amount": 12500
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "`idempotency_required` — Huvudet Idempotency-Key saknas.\n\n`invalid_request` — Kroppen går inte att läsa — svaret säger vilket fält.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller okänd nyckel.\n\n`key_revoked` — Nyckeln är återkallad av installationens ägare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` — Nyckeln är giltig men saknar behörigheten rutten kräver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "409": {
            "description": "`idempotency_conflict` — Samma Idempotency-Key har redan använts med en annan kropp.\n\n`idempotency_in_progress` — Ett anrop med samma Idempotency-Key behandlas just nu. Skicka om exakt samma anrop om en stund — du får då det första svaret. Byt INTE nyckel: det första anropet kan mycket väl ha bokfört.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "description": "`payload_too_large` — Kroppen är större än 512 kB. En faktura är några kilobyte; taket stoppar en kropp som inte är en.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "422": {
            "description": "`unprocessable` — Fakturan är mottagen men kunde inte skapas eller bokföras. Skälet står i svaret — låst period, avslutat räkenskapsår eller en momskonflikt mellan rad och konto.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`server_misconfigured` — Installationen saknar konfiguration för API:et.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/inbound/order": {
      "post": {
        "operationId": "inbound-order",
        "summary": "Order från e-handel eller kassasystem",
        "description": "Tar emot en order i ETT generiskt format och gör den till ett fakturautkast — inga plattformsadaptrar, och det är ett val. En adapter mot någon annans API går inte att bygga rätt utan ett konto att pröva mot, och en gissning om moms blir kundens myndighetsfel.\n\n`pricesIncludeVat` är obligatoriskt: det går inte att se på ett tal om momsen ingår, och att anta fel gör varje rad 25 procent fel. En order som inte går att kontera rätt får 422 och SPARAS med sitt skäl — den går att rätta och skicka om. En spärrad order som försvinner är omöjlig att skilja från en order som aldrig kom.\n\n**Takt:** 600 anrop per timme och nyckel (ställbart per nyckel).\n\n**Behörighet:** `intake:write` (Ta emot underlag)\n\n**Bakåtkompatibelt:** miljövariabeln `ORDER_INBOUND_SECRET` fortsätter fungera parallellt.",
        "tags": [
          "Inkommande"
        ],
        "x-required-scope": "intake:write",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "externalId",
                  "orderDate",
                  "currency",
                  "pricesIncludeVat",
                  "customer",
                  "rows"
                ],
                "properties": {
                  "externalId": {
                    "type": "string",
                    "description": "Butikens ordernummer. Unikt — samma nummer kan aldrig bli två fakturor.",
                    "example": "SHOP-10042"
                  },
                  "orderDate": {
                    "type": "string",
                    "format": "date",
                    "description": "Orderdatum (ÅÅÅÅ-MM-DD).",
                    "example": "2026-03-12"
                  },
                  "currency": {
                    "type": "string",
                    "description": "Orderns valuta. SEK — bokföringen sker i redovisningsvalutan, och en order i annan valuta spärras med sitt skäl i stället för att räknas om med en gissad kurs.",
                    "example": "SEK"
                  },
                  "pricesIncludeVat": {
                    "type": "boolean",
                    "description": "Om radernas priser är inklusive moms. Obligatoriskt.",
                    "example": true
                  },
                  "total": {
                    "type": "number",
                    "description": "Vad kunden betalade inklusive moms. Krävs när pricesIncludeVat är true, så baklängesräkningen kan stämmas av.",
                    "example": 1249
                  },
                  "customer": {
                    "type": "object",
                    "description": "name, och gärna email, orgNumber, vatNumber och countryCode."
                  },
                  "rows": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "description, quantity, unitPrice, vatRate och gärna articleNumber."
                  }
                }
              },
              "example": {
                "externalId": "SHOP-10042",
                "orderDate": "2026-03-12",
                "currency": "SEK",
                "pricesIncludeVat": true,
                "total": 1249,
                "customer": {
                  "name": "Anna Andersson",
                  "email": "anna@example.com",
                  "countryCode": "SE"
                },
                "rows": [
                  {
                    "description": "Träningsbälte",
                    "quantity": 1,
                    "unitPrice": 1249,
                    "vatRate": 25
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Utkastet skapades — eller ordern var redan mottagen (`duplicate: true`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "invoiceId": "7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a",
                  "customerId": "3c9a1b2d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Kroppen är inte giltig JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller okänd nyckel.\n\n`key_revoked` — Nyckeln är återkallad av installationens ägare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` — Nyckeln är giltig men saknar behörigheten rutten kräver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "description": "Ordern är större än 1 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "422": {
            "description": "Ordern är mottagen och sparad med sitt skäl, men kunde inte konteras. Någon ska titta på den — skicka den inte om oförändrad.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`server_misconfigured` — Installationen saknar konfiguration för API:et.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/inbound/peppol": {
      "post": {
        "operationId": "inbound-peppol",
        "summary": "E-faktura in (UBL, Peppol BIS Billing 3.0)",
        "description": "Tar emot ett FÄRDIGT UBL-dokument och gör det till ett utkast i leverantörsinkorgen. Det här är inte en Peppol-accesspunkt: att ta emot i nätverket kräver AS4, SMP-registrering och ett operatörsavtal som någon måste drifta. Du har din egen accesspunkt och pekar den hit.\n\nIdempotensen bygger på SHA-256 av dokumentet självt: en omleverans är identiska byte och blir inget nytt utkast, medan en rättad faktura är ett annat dokument och ska bli det. Allt oklart — okänd leverantör, kreditnota, en varning — sätts till granska i stället för att gissas.\n\n**Takt:** 600 anrop per timme och nyckel (ställbart per nyckel).\n\n**Behörighet:** `intake:write` (Ta emot underlag)\n\n**Bakåtkompatibelt:** miljövariabeln `PEPPOL_INBOUND_SECRET` fortsätter fungera parallellt.",
        "tags": [
          "Inkommande"
        ],
        "x-required-scope": "intake:write",
        "parameters": [
          {
            "name": "Content-Type",
            "in": "header",
            "required": true,
            "description": "application/xml för UBL:en direkt, eller application/json för kuverteringen.",
            "schema": {
              "type": "string"
            },
            "example": "application/xml"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/xml": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/json": {
              "schema": {
                "type": "object",
                "required": [],
                "properties": {
                  "document": {
                    "type": "string",
                    "description": "UBL:en som text, när kroppen är JSON."
                  },
                  "documentBase64": {
                    "type": "string",
                    "description": "UBL:en base64-kodad, när kroppen är JSON."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Utkastet skapades — eller dokumentet var redan mottaget (`duplicate: true`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": true,
                  "draftId": "7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a",
                  "invoiceNo": "F-2026-118",
                  "supplier": "Leverantören AB",
                  "total": 12500,
                  "warnings": []
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Dokumentet går inte att läsa, eller är ställt till någon annan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller okänd nyckel.\n\n`key_revoked` — Nyckeln är återkallad av installationens ägare.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`insufficient_scope` — Nyckeln är giltig men saknar behörigheten rutten kräver.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "413": {
            "description": "Dokumentet är större än 4 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "502": {
            "description": "Dokumentet kunde inte lagras. Leverera om.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "503": {
            "description": "`server_misconfigured` — Installationen saknar konfiguration för API:et.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/byra/token": {
      "post": {
        "operationId": "byra-token",
        "summary": "Växla en byrånyckel mot en kortlivad inloggning",
        "description": "Byråspåret är sitt eget och har ett annat nyckelformat (`dkb_`) i en annan tabell. Byrån växlar sin nyckel mot en token som lever en timme och använder den mot /api/stats/byra.\n\nKontraktet är FRYST. Det läses av byråportalen, som är en separat produkt, och får bara utökas bakåtkompatibelt — aldrig ett borttaget fält, aldrig en ändrad betydelse, aldrig en sänkt schema_version.\n\n**Takt:** 60 anrop per timme och avsändaradress.\n\n**Fryst kontrakt.** Läses av byråportalen och får bara utökas bakåtkompatibelt.",
        "tags": [
          "Byrå"
        ],
        "security": [
          {
            "ByraNyckel": []
          }
        ],
        "responses": {
          "200": {
            "description": "En token som lever en timme.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "access_token": "eyJhbGciOi…",
                  "token_type": "Bearer",
                  "expires_in": 3600,
                  "expires_at": 1789000000,
                  "scopes": [
                    "stats:read"
                  ],
                  "agency": "Redovisningsbyrån AB"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Okänd eller felformad byrånyckel.\n\n`key_revoked` — Klienten har dragit in åtkomsten.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "405": {
            "description": "`method_not_allowed` — Endast POST. En token är inte en resurs som kan hämtas om.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — För många växlingar från samma adress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "503": {
            "description": "`token_unavailable` — Nyckeln är giltig men sessionen kunde inte skapas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/stats/byra": {
      "get": {
        "operationId": "stats-byra",
        "summary": "Aggregatet en byrå får läsa",
        "description": "En rad, inga affärshändelser: antal obokförda poster, omatchade banktransaktioner, verifikat utan underlag, senaste verifikatdatum, låsdatum, nästa momsdeadline och räkenskapsårets status. Inga belopp, inga motparter.\n\nMånadsavslutets fält kom till i en senare version och är därför nullbara: `last_closed_month`, `open_month` och `open_month_status` säger vilken månad som ligger närmast i tur, `months_behind` hur många som återstår, och `open_month_failing` vilka villkor som är röda i den — villkorens NAMN, t.ex. `[\"bank\", \"underlag\"]`, aldrig deras innehåll. Fältet är alltid en array; frågan om det finns någon månad att tala om besvaras av `open_month`. Lönevillkoret kan inte förekomma där: lön och personal ligger utanför byråns läskrets.\n\nSvaret bär två versionstal som svarar på olika frågor: `schema_version` säger om portalen kan läsa svaret, `installation_schema_version` hur gammal installationen är. Kontraktet är FRYST — det får utökas bakåtkompatibelt och ingenting annat, så en portal byggd mot version 1 fortsätter läsa svaret.\n\n**Takt:** 60 anrop per timme och avsändaradress.\n\n**Fryst kontrakt.** Läses av byråportalen och får bara utökas bakåtkompatibelt.",
        "tags": [
          "Byrå"
        ],
        "security": [
          {
            "ByraToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Aggregatet.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "schema_version": 1,
                  "installation_schema_version": 112,
                  "period": "202609",
                  "unbooked_count": 4,
                  "attachments_missing": 3,
                  "unmatched_bank": 4,
                  "last_verification": "2026-09-01",
                  "period_locked_to": "2026-03-31",
                  "vat_due_date": "2026-05-12",
                  "fiscal_year": {
                    "start": "2026-01-01",
                    "end": "2026-12-31",
                    "status": "open"
                  },
                  "last_closed_month": "2026-07-01",
                  "open_month": "2026-08-01",
                  "open_month_status": "in_progress",
                  "open_month_failing": [
                    "bank",
                    "underlag"
                  ],
                  "months_behind": 2
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "`unauthorized` — Saknad, felformad eller avvisad token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "description": "`no_access` — Token duger men öppnar ingenting — åtkomsten kan vara återkallad.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited` — För många anrop från samma adress.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              },
              "Retry-After": {
                "description": "Sekunder tills kvoten återställs.",
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "`db_error` — Aggregatet kunde inte läsas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fel"
                }
              }
            },
            "headers": {
              "X-Request-Id": {
                "description": "Anropets id. Ta med det i ett supportärende.",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "Nyckeln skapas under Inställningar → API-nycklar och visas exakt en gång. Skicka den som `Authorization: Bearer dk_live_…`. Aldrig i en frågesträng: en nyckel i en URL hamnar i varje proxylogg på vägen."
      },
      "ByraNyckel": {
        "type": "http",
        "scheme": "bearer",
        "description": "Byrånyckeln (`dkb_…`), utfärdad av klienten under Inställningar → Byråns åtkomst. Den är INTE en API-nyckel: en `dk_live_`-nyckel avvisas här. Nyckeln växlas mot en token och används aldrig direkt mot en läsrutt."
      },
      "ByraToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "Den kortlivade token `POST /api/byra/token` svarar med. Lever en timme; växla om när den gått ut. Varken byrånyckeln eller en `dk_live_`-nyckel duger här."
      }
    },
    "schemas": {
      "Fel": {
        "type": "object",
        "required": [
          "error",
          "message",
          "request_id"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Maskinkod i snake_case. Stabil för alltid — det här är det enda fältet en integration ska grena på. Nya koder kan tillkomma; en befintlig kod byter aldrig betydelse."
          },
          "message": {
            "type": "string",
            "description": "Förklaring på svenska, riktad till människan som felsöker. Får formuleras om när som helst och är aldrig en del av kontraktet."
          },
          "detail": {
            "type": "object",
            "description": "Strukturerade uppgifter om just det här felet — aldrig råtext ur databasen."
          },
          "request_id": {
            "type": "string",
            "description": "Anropets id, också i svarshuvudet X-Request-Id. Ta med det i ett supportärende så går anropet att peka ut i loggen."
          }
        }
      }
    }
  }
}
