# Debet & Kredit — API API-version: v1 Kontraktsversion (svarens form): 1 Programversion som genererade dokumentet: 0.1.0 > Debet & Kredit är ett svenskt bokföringsprogram som körs SJÄLVHOSTAT: varje > kund driver sin egen installation på sin egen server. Det finns därför ingen > gemensam bas-URL. Byt ut https://din-installation.se mot kundens egen adress > i varje exempel nedan. Ingen ansökan, inget partneravtal, ingen granskning och ingen integrationslicens. Installationens ägare skapar nyckeln själv i appen under Inställningar → API-nycklar. ## Autentisering Skicka nyckeln som bearer-token på varje anrop: ``` Authorization: Bearer dk_live_DIN_NYCKEL ``` Nyckeln börjar med dk_live_ följt av 43 tecken. Den accepteras ENBART i Authorization-huvudet, aldrig i en frågesträng. HTTPS krävs. Nyckeln skapas i appen, visas exakt en gång och återkallas med ett klick. Återkallelsen biter i samma sekund eftersom behörigheten slås upp på nytt i varje databasfråga — inte när en token löper ut. ### Behörigheter - data:read (Läsa): Hämtar fakturor, kunder, verifikat och nyckeltal. Ändrar ingenting. - intake:write (Ta emot underlag): Lämnar in ordrar och e-fakturor till inkorgen. Ser inget som redan finns. - 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. Saknas behörigheten svarar API:et 403 insufficient_scope och skriver ut vilken som krävdes. Ingen nyckel når lön, personal, företagsuppgifter, autogiromedgivanden eller sparade nycklar — oavsett behörighet. Gränsen sitter i databasen. ## Felformat Alla fel har samma form: ```json { "error": "period_locked", "message": "Perioden 2026-03 är momsspärrad och kan inte bokföras i.", "detail": { "period": "2026-03" }, "request_id": "0f9c2f1e-7b3a-4c11-9d55-1a2b3c4d5e6f" } ``` - error (string): 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 (string): 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 (object, valfritt): Strukturerade uppgifter om just det här felet — aldrig råtext ur databasen. - request_id (string): Anropets id, också i svarshuvudet X-Request-Id. Ta med det i ett supportärende så går anropet att peka ut i loggen. VIKTIGT FÖR EN INTEGRATION: gren på `error`, aldrig på `message`. Koden i `error` är stabil för alltid och byter aldrig betydelse; `message` är skriven för människan som felsöker och får formuleras om när som helst. Nya koder kan tillkomma — behandla en okänd kod som ett allmänt fel. request_id finns också i svarshuvudet X-Request-Id, även på lyckade svar. ### Statuskodernas betydelse - 400 formen: kroppen eller en parameter går inte att läsa. Rätta och skicka om. - 401 nyckeln: saknad, felformad, okänd eller återkallad. Omförsök hjälper inte. - 403 behörigheten: nyckeln saknar det scope rutten kräver. Omförsök hjälper inte. - 404 resursen finns inte i den här installationen. - 409 idempotenskrock: samma Idempotency-Key med annan kropp. - 413 kroppen är större än ruttens tak. - 422 mottagen men inte behandlingsbar. SE NEDAN. - 429 takten: vänta det antal sekunder Retry-After anger. - 5xx vår sida: gör om anropet med samma Idempotency-Key. ### Om 422 — läs det här innan du bygger felhantering Debet & Kredit är ett bokföringsprogram, och bokföringens spärrar gäller lika för API:et som för en människa i gränssnittet: momslås, oföränderliga verifikat och avslutade räkenskapsår sitter i databasen, inte i knapparna. Följden är att vissa anrop svarar 422 i stället för att lyda. Det betyder MOTTAGET MEN INTE BOKFÖRT — inte att anropet misslyckades: - En order som inte går att kontera rätt SPARAS med sitt skäl och syns i listan över mottagna ordrar. Skicka den inte om oförändrad; en människa ska titta på den. - En faktura som inte kan bokföras i en låst period LIGGER KVAR som utkast. Svaret bär invoice_id så att den går att hitta. Ingenting kastas. En spärrad post som försvinner är omöjlig att skilja från en post som aldrig kom. ## Idempotens Skrivanrop KRÄVER huvudet Idempotency-Key (8-255 tecken, valfritt av dig, gärna ditt eget ordernummer). Det är obligatoriskt och inte valfritt därför att ett nätverk som tappar svaret men inte anropet är det normala felet. - Samma nyckel + samma kropp: du får det första svaret tillbaka, plus huvudet Idempotent-Replay: true. Ingen andra post skapas. - Samma nyckel + annan kropp: 409 idempotency_conflict. - Ny nyckel: ett nytt anrop. Sparade svar städas efter 72 timmar. ## Endpoints Upptäckt: - GET /api/v1/meta — Vad den här installationen är och vad din nyckel får göra Läsning: - GET /api/v1/verifikat — Affärshändelser med sina rader - GET /api/stats/overview — Komplett ekonomisk lägesbild - GET /api/stats/monthly — Resultatserie per månad - GET /api/stats/daily — Daglig intäktsstatistik Skrivning: - POST /api/v1/verifikat — Bokför en affärshändelse från ditt eget system - POST /api/v1/kundfakturor — Skapa ett fakturautkast, och bokför det om du vill Inkommande: - POST /api/inbound/order — Order från e-handel eller kassasystem - POST /api/inbound/peppol — E-faktura in (UBL, Peppol BIS Billing 3.0) Byrå: - POST /api/byra/token — Växla en byrånyckel mot en kortlivad inloggning - GET /api/stats/byra — Aggregatet en byrå får läsa ## Upptäckt ### GET /api/v1/meta Vad den här installationen är och vad din nyckel får göra 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. Behörighet: varje giltig nyckel Takt: 600 anrop per timme och nyckel (ställbart per nyckel). Exempel på svar: ```json { "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" } ] } ``` Statuskoder: - 200: Installationens uppgifter och nyckelns behörigheter. - 401 unauthorized: Saknad, felformad eller okänd nyckel. - 401 key_revoked: Nyckeln är återkallad av installationens ägare. - 429 rate_limited: Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs. - 503 server_misconfigured: Installationen saknar konfiguration för API:et. curl: ```bash curl -X GET "https://din-installation.se/api/v1/meta" \ -H "Authorization: Bearer dk_live_DIN_NYCKEL" ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_API_KEY; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/v1/meta`, { method: "GET", headers: { Authorization: `Bearer ${nyckel}`, }, }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_API_KEY"] bas = "https://din-installation.se" svar = requests.get( f"{bas}/api/v1/meta", headers={ "Authorization": f"Bearer {nyckel}", }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ## Läsning ### GET /api/v1/verifikat Affärshändelser med sina rader 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. Behörighet: data:read (Läsa) Takt: 600 anrop per timme och nyckel (ställbart per nyckel). Frågesträng: - from (date, valfri): Tidigaste verifikationsdatum (ÅÅÅÅ-MM-DD). Exempel: 2026-01-01 - to (date, valfri): Senaste verifikationsdatum (ÅÅÅÅ-MM-DD). Exempel: 2026-03-31 - serie (string, valfri): Verifikationsseriens kod, till exempel A eller F. Exempel: A - konto (integer, valfri): Ta bara med verifikat som har en rad på det här kontot. Exempel: 3001 - id (uuid, valfri): Hämta ett enskilt verifikat. Exempel: 8f14e45f-ceea-467a-9a3a-1f2b3c4d5e6f - limit (integer, valfri): Antal verifikat per sida, 1–200. Standard 50. Exempel: 50 - cursor (string, valfri): Markören ur föregående svars `next_cursor`. Exempel: MjAyNi0wMy0xMnw4ZjE0 Exempel på svar: ```json { "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 } ``` Statuskoder: - 200: En sida verifikat. - 400 invalid_request: En parameter går inte att läsa — svaret säger vilken. - 403 insufficient_scope: Nyckeln är giltig men saknar behörigheten rutten kräver. - 401 unauthorized: Saknad, felformad eller okänd nyckel. - 401 key_revoked: Nyckeln är återkallad av installationens ägare. - 429 rate_limited: Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs. - 503 server_misconfigured: Installationen saknar konfiguration för API:et. curl: ```bash curl -X GET "https://din-installation.se/api/v1/verifikat?from=2026-01-01&to=2026-03-31&serie=A&konto=3001&id=8f14e45f-ceea-467a-9a3a-1f2b3c4d5e6f&limit=50" \ -H "Authorization: Bearer dk_live_DIN_NYCKEL" ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_API_KEY; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/v1/verifikat?from=2026-01-01&to=2026-03-31&serie=A&konto=3001&id=8f14e45f-ceea-467a-9a3a-1f2b3c4d5e6f&limit=50`, { method: "GET", headers: { Authorization: `Bearer ${nyckel}`, }, }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_API_KEY"] bas = "https://din-installation.se" svar = requests.get( f"{bas}/api/v1/verifikat?from=2026-01-01&to=2026-03-31&serie=A&konto=3001&id=8f14e45f-ceea-467a-9a3a-1f2b3c4d5e6f&limit=50", headers={ "Authorization": f"Bearer {nyckel}", }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ### GET /api/stats/overview Komplett ekonomisk lägesbild 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. Behörighet: data:read (Läsa) Takt: 600 anrop per timme och nyckel (ställbart per nyckel). Bakåtkompatibelt: miljövariabeln STATS_API_KEY fungerar parallellt. Exempel på svar: ```json { "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 } ``` Statuskoder: - 200: Lägesbilden. - 403 insufficient_scope: Nyckeln är giltig men saknar behörigheten rutten kräver. - 401 unauthorized: Saknad, felformad eller okänd nyckel. - 401 key_revoked: Nyckeln är återkallad av installationens ägare. - 429 rate_limited: Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs. - 503 server_misconfigured: Installationen saknar konfiguration för API:et. - 500 db_error: Underlaget kunde inte läsas. curl: ```bash curl -X GET "https://din-installation.se/api/stats/overview" \ -H "Authorization: Bearer dk_live_DIN_NYCKEL" ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_API_KEY; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/stats/overview`, { method: "GET", headers: { Authorization: `Bearer ${nyckel}`, }, }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_API_KEY"] bas = "https://din-installation.se" svar = requests.get( f"{bas}/api/stats/overview", headers={ "Authorization": f"Bearer {nyckel}", }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ### GET /api/stats/monthly Resultatserie per månad Intäkter, kostnader och resultat per månad, i SEK exklusive moms och hela kronor. Konton 3000–7999. Behörighet: data:read (Läsa) Takt: 600 anrop per timme och nyckel (ställbart per nyckel). Bakåtkompatibelt: miljövariabeln STATS_API_KEY fungerar parallellt. Exempel på svar: ```json { "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 } } ``` Statuskoder: - 200: Månadsserien. - 403 insufficient_scope: Nyckeln är giltig men saknar behörigheten rutten kräver. - 401 unauthorized: Saknad, felformad eller okänd nyckel. - 401 key_revoked: Nyckeln är återkallad av installationens ägare. - 429 rate_limited: Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs. - 503 server_misconfigured: Installationen saknar konfiguration för API:et. - 500 db_error: Underlaget kunde inte läsas. curl: ```bash curl -X GET "https://din-installation.se/api/stats/monthly" \ -H "Authorization: Bearer dk_live_DIN_NYCKEL" ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_API_KEY; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/stats/monthly`, { method: "GET", headers: { Authorization: `Bearer ${nyckel}`, }, }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_API_KEY"] bas = "https://din-installation.se" svar = requests.get( f"{bas}/api/stats/monthly", headers={ "Authorization": f"Bearer {nyckel}", }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ### GET /api/stats/daily Daglig intäktsstatistik 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. Behörighet: data:read (Läsa) Takt: 600 anrop per timme och nyckel (ställbart per nyckel). Bakåtkompatibelt: miljövariabeln STATS_API_KEY fungerar parallellt. Frågesträng: - from (date, krävs): Intervallets första dag (ÅÅÅÅ-MM-DD). Exempel: 2026-03-01 - to (date, krävs): Intervallets sista dag (ÅÅÅÅ-MM-DD). Högst 90 dagar från `from`. Exempel: 2026-03-31 Exempel på svar: ```json [ { "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 } ] ``` Statuskoder: - 200: En post per dag i intervallet. Svaret är en array, inte ett objekt. - 400: Datumen går inte att läsa, eller intervallet är längre än 90 dagar. - 403 insufficient_scope: Nyckeln är giltig men saknar behörigheten rutten kräver. - 401 unauthorized: Saknad, felformad eller okänd nyckel. - 401 key_revoked: Nyckeln är återkallad av installationens ägare. - 429 rate_limited: Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs. - 503 server_misconfigured: Installationen saknar konfiguration för API:et. curl: ```bash curl -X GET "https://din-installation.se/api/stats/daily?from=2026-03-01&to=2026-03-31" \ -H "Authorization: Bearer dk_live_DIN_NYCKEL" ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_API_KEY; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/stats/daily?from=2026-03-01&to=2026-03-31`, { method: "GET", headers: { Authorization: `Bearer ${nyckel}`, }, }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_API_KEY"] bas = "https://din-installation.se" svar = requests.get( f"{bas}/api/stats/daily?from=2026-03-01&to=2026-03-31", headers={ "Authorization": f"Bearer {nyckel}", }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ## Skrivning ### POST /api/v1/verifikat Bokför en affärshändelse från ditt eget system 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. **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. **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. **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. **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. **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. Behörighet: ledger:write (Bokföra) Takt: 600 anrop per timme och nyckel (ställbart per nyckel). Idempotens: huvudet Idempotency-Key krävs (8-255 tecken). Huvuden: - Idempotency-Key (string, krävs): 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. Exempel: kassa-2026-03-12 Kropp (JSON): - seriesCode (string, krävs): Verifikationsseriens kod, högst två tecken. Serien måste finnas för räkenskapsåret datumet ligger i. Exempel: A - date (date, krävs): Verifikationsdatum (ÅÅÅÅ-MM-DD). Avgör räkenskapsår, period och vilka lås som prövas. Exempel: 2026-03-12 - description (string, krävs): Vad affärshändelsen avser. Står i huvudboken och kan inte ändras i efterhand. Exempel: Dagskassa 12 mars - counterparty (string, valfri): Motparten, om det finns en. Exempel: Kortinlösen AB - rows (array, krävs): Minst två rader med account, debit och credit. Frivilligt per rad: note, cost_center och project (dimensionernas uuid, inte deras koder). Exempel på anrop: ```json { "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 } ] } ``` Exempel på svar: ```json { "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" } ``` Statuskoder: - 200: Verifikatet bokfördes — eller är sedan tidigare bokfört med samma Idempotency-Key. Svaret bär numret och en länk till posten. - 400 idempotency_required: Huvudet Idempotency-Key saknas. - 400 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. - 409 idempotency_conflict: Samma Idempotency-Key har redan använts med en annan kropp. - 409 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. - 413 payload_too_large: Kroppen är större än 512 kB. Ett verifikat är några kilobyte; taket stoppar en kropp som inte är ett. - 422 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. - 422 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. - 403 insufficient_scope: Nyckeln är giltig men saknar behörigheten rutten kräver. - 401 unauthorized: Saknad, felformad eller okänd nyckel. - 401 key_revoked: Nyckeln är återkallad av installationens ägare. - 429 rate_limited: Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs. - 503 server_misconfigured: Installationen saknar konfiguration för API:et. curl: ```bash curl -X POST "https://din-installation.se/api/v1/verifikat" \ -H "Authorization: Bearer dk_live_DIN_NYCKEL" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-2026-0042" \ -d '{ "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 } ] }' ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_API_KEY; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/v1/verifikat`, { method: "POST", headers: { Authorization: `Bearer ${nyckel}`, "Content-Type": "application/json", "Idempotency-Key": "order-2026-0042", }, body: JSON.stringify({ "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 } ] }), }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_API_KEY"] bas = "https://din-installation.se" svar = requests.post( f"{bas}/api/v1/verifikat", headers={ "Authorization": f"Bearer {nyckel}", "Idempotency-Key": "order-2026-0042", }, json={ "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 } ] }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ### POST /api/v1/kundfakturor Skapa ett fakturautkast, och bokför det om du vill 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. Bokfö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. **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. Behörighet: ledger:write (Bokföra) Takt: 600 anrop per timme och nyckel (ställbart per nyckel). Idempotens: huvudet Idempotency-Key krävs (8-255 tecken). Huvuden: - Idempotency-Key (string, krävs): 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. Exempel: order-2026-0042 Kropp (JSON): - customerId (uuid, krävs): Kundens id. Kunden måste finnas — den här rutten skapar aldrig kunder. Exempel: 3c9a1b2d-4e5f-4a6b-8c7d-9e0f1a2b3c4d - invoiceDate (date, krävs): Fakturadatum (ÅÅÅÅ-MM-DD). Exempel: 2026-03-12 - paymentTerms (integer, krävs): Betalningsvillkor i dagar, 0–90. Förfallodagen räknas i kalendern. Exempel: 30 - yourReference (string, valfri): Kundens referens. - notes (string, valfri): Fritext på fakturan. - rows (array, krävs): Minst en rad med description, quantity, unitPrice, vatRate och account. - book (boolean, valfri): true bokför fakturan direkt. Utelämnad eller false lämnar den som utkast. Exempel: false Exempel på anrop: ```json { "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 } ``` Exempel på svar: ```json { "id": "7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a", "status": "draft", "invoice_number": null, "net_amount": 10000, "vat_amount": 2500, "total_amount": 12500 } ``` Statuskoder: - 200: Fakturan skapades — eller är sedan tidigare skapad med samma Idempotency-Key. - 400 idempotency_required: Huvudet Idempotency-Key saknas. - 400 invalid_request: Kroppen går inte att läsa — svaret säger vilket fält. - 413 payload_too_large: Kroppen är större än 512 kB. En faktura är några kilobyte; taket stoppar en kropp som inte är en. - 409 idempotency_conflict: Samma Idempotency-Key har redan använts med en annan kropp. - 409 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. - 422 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. - 403 insufficient_scope: Nyckeln är giltig men saknar behörigheten rutten kräver. - 401 unauthorized: Saknad, felformad eller okänd nyckel. - 401 key_revoked: Nyckeln är återkallad av installationens ägare. - 429 rate_limited: Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs. - 503 server_misconfigured: Installationen saknar konfiguration för API:et. curl: ```bash curl -X POST "https://din-installation.se/api/v1/kundfakturor" \ -H "Authorization: Bearer dk_live_DIN_NYCKEL" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-2026-0042" \ -d '{ "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 }' ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_API_KEY; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/v1/kundfakturor`, { method: "POST", headers: { Authorization: `Bearer ${nyckel}`, "Content-Type": "application/json", "Idempotency-Key": "order-2026-0042", }, body: JSON.stringify({ "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 }), }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_API_KEY"] bas = "https://din-installation.se" svar = requests.post( f"{bas}/api/v1/kundfakturor", headers={ "Authorization": f"Bearer {nyckel}", "Idempotency-Key": "order-2026-0042", }, json={ "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 }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ## Inkommande ### POST /api/inbound/order Order från e-handel eller kassasystem 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. `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. Behörighet: intake:write (Ta emot underlag) Takt: 600 anrop per timme och nyckel (ställbart per nyckel). Bakåtkompatibelt: miljövariabeln ORDER_INBOUND_SECRET fungerar parallellt. Kropp (JSON): - externalId (string, krävs): Butikens ordernummer. Unikt — samma nummer kan aldrig bli två fakturor. Exempel: SHOP-10042 - orderDate (date, krävs): Orderdatum (ÅÅÅÅ-MM-DD). Exempel: 2026-03-12 - currency (string, krävs): 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. Exempel: SEK - pricesIncludeVat (boolean, krävs): Om radernas priser är inklusive moms. Obligatoriskt. Exempel: true - total (number, valfri): Vad kunden betalade inklusive moms. Krävs när pricesIncludeVat är true, så baklängesräkningen kan stämmas av. Exempel: 1249 - customer (object, krävs): name, och gärna email, orgNumber, vatNumber och countryCode. - rows (array, krävs): description, quantity, unitPrice, vatRate och gärna articleNumber. Exempel på anrop: ```json { "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 } ] } ``` Exempel på svar: ```json { "ok": true, "invoiceId": "7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a", "customerId": "3c9a1b2d-4e5f-4a6b-8c7d-9e0f1a2b3c4d" } ``` Statuskoder: - 200: Utkastet skapades — eller ordern var redan mottagen (`duplicate: true`). - 400: Kroppen är inte giltig JSON. - 413: Ordern är större än 1 MB. - 422: 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. - 403 insufficient_scope: Nyckeln är giltig men saknar behörigheten rutten kräver. - 401 unauthorized: Saknad, felformad eller okänd nyckel. - 401 key_revoked: Nyckeln är återkallad av installationens ägare. - 429 rate_limited: Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs. - 503 server_misconfigured: Installationen saknar konfiguration för API:et. curl: ```bash curl -X POST "https://din-installation.se/api/inbound/order" \ -H "Authorization: Bearer dk_live_DIN_NYCKEL" \ -H "Content-Type: application/json" \ -d '{ "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 } ] }' ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_API_KEY; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/inbound/order`, { method: "POST", headers: { Authorization: `Bearer ${nyckel}`, "Content-Type": "application/json", }, body: JSON.stringify({ "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 } ] }), }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_API_KEY"] bas = "https://din-installation.se" svar = requests.post( f"{bas}/api/inbound/order", headers={ "Authorization": f"Bearer {nyckel}", }, json={ "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 } ] }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ### POST /api/inbound/peppol E-faktura in (UBL, Peppol BIS Billing 3.0) 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. Idempotensen 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. Behörighet: intake:write (Ta emot underlag) Takt: 600 anrop per timme och nyckel (ställbart per nyckel). Bakåtkompatibelt: miljövariabeln PEPPOL_INBOUND_SECRET fungerar parallellt. Huvuden: - Content-Type (string, krävs): application/xml för UBL:en direkt, eller application/json för kuverteringen. Exempel: application/xml Kropp (JSON): - document (string, valfri): UBL:en som text, när kroppen är JSON. - documentBase64 (string, valfri): UBL:en base64-kodad, när kroppen är JSON. Exempel på svar: ```json { "ok": true, "draftId": "7d8e9f0a-1b2c-4d3e-8f4a-5b6c7d8e9f0a", "invoiceNo": "F-2026-118", "supplier": "Leverantören AB", "total": 12500, "warnings": [] } ``` Statuskoder: - 200: Utkastet skapades — eller dokumentet var redan mottaget (`duplicate: true`). - 400: Dokumentet går inte att läsa, eller är ställt till någon annan. - 413: Dokumentet är större än 4 MB. - 502: Dokumentet kunde inte lagras. Leverera om. - 403 insufficient_scope: Nyckeln är giltig men saknar behörigheten rutten kräver. - 401 unauthorized: Saknad, felformad eller okänd nyckel. - 401 key_revoked: Nyckeln är återkallad av installationens ägare. - 429 rate_limited: Nyckelns kvot för timmen är förbrukad. Retry-After säger när den återställs. - 503 server_misconfigured: Installationen saknar konfiguration för API:et. curl: ```bash curl -X POST "https://din-installation.se/api/inbound/peppol" \ -H "Authorization: Bearer dk_live_DIN_NYCKEL" \ -H "Content-Type: application/xml" \ --data-binary @faktura.xml ``` JavaScript: ```javascript import { readFileSync } from "node:fs"; const nyckel = process.env.DEBEA_API_KEY; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/inbound/peppol`, { method: "POST", headers: { Authorization: `Bearer ${nyckel}`, "Content-Type": "application/xml", }, body: readFileSync("faktura.xml"), }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_API_KEY"] bas = "https://din-installation.se" svar = requests.post( f"{bas}/api/inbound/peppol", headers={ "Authorization": f"Bearer {nyckel}", "Content-Type": "application/xml", }, data=open("faktura.xml", "rb").read(), ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ## Byrå ### POST /api/byra/token Växla en byrånyckel mot en kortlivad inloggning 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. Kontraktet ä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. Behörighet: varje giltig nyckel Takt: 60 anrop per timme och avsändaradress. FRYST KONTRAKT: läses av byråportalen, får bara utökas bakåtkompatibelt. Huvuden: - Authorization (string, krävs): Bearer följt av byrånyckeln (dkb_…). Exempel: Bearer dkb_… Exempel på svar: ```json { "access_token": "eyJhbGciOi…", "token_type": "Bearer", "expires_in": 3600, "expires_at": 1789000000, "scopes": [ "stats:read" ], "agency": "Redovisningsbyrån AB" } ``` Statuskoder: - 200: En token som lever en timme. - 401 unauthorized: Okänd eller felformad byrånyckel. - 401 key_revoked: Klienten har dragit in åtkomsten. - 405 method_not_allowed: Endast POST. En token är inte en resurs som kan hämtas om. - 429 rate_limited: För många växlingar från samma adress. - 503 token_unavailable: Nyckeln är giltig men sessionen kunde inte skapas. curl: ```bash curl -X POST "https://din-installation.se/api/byra/token" \ -H "Authorization: Bearer dkb_…" ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_BYRA_NYCKEL; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/byra/token`, { method: "POST", headers: { Authorization: `Bearer ${nyckel}`, }, }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_BYRA_NYCKEL"] bas = "https://din-installation.se" svar = requests.post( f"{bas}/api/byra/token", headers={ "Authorization": f"Bearer {nyckel}", }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ### GET /api/stats/byra Aggregatet en byrå får läsa 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. Må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. Svaret 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. Behörighet: varje giltig nyckel Takt: 60 anrop per timme och avsändaradress. FRYST KONTRAKT: läses av byråportalen, får bara utökas bakåtkompatibelt. Huvuden: - Authorization (string, krävs): Bearer följt av token från POST /api/byra/token. Exempel: Bearer eyJhbGciOi… Exempel på svar: ```json { "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 } ``` Statuskoder: - 200: Aggregatet. - 401 unauthorized: Saknad, felformad eller avvisad token. - 403 no_access: Token duger men öppnar ingenting — åtkomsten kan vara återkallad. - 429 rate_limited: För många anrop från samma adress. - 500 db_error: Aggregatet kunde inte läsas. curl: ```bash curl -X GET "https://din-installation.se/api/stats/byra" \ -H "Authorization: Bearer eyJhbGciOi…" ``` JavaScript: ```javascript const nyckel = process.env.DEBEA_BYRA_TOKEN; const bas = "https://din-installation.se"; const svar = await fetch(`${bas}/api/stats/byra`, { method: "GET", headers: { Authorization: `Bearer ${nyckel}`, }, }); if (!svar.ok) { // Felkoden i `error` är stabil; `message` är förklaringen på svenska. const fel = await svar.json(); throw new Error(`${fel.error}: ${fel.message} (${fel.request_id})`); } const data = await svar.json(); ``` Python: ```python import os, requests nyckel = os.environ["DEBEA_BYRA_TOKEN"] bas = "https://din-installation.se" svar = requests.get( f"{bas}/api/stats/byra", headers={ "Authorization": f"Bearer {nyckel}", }, ) if not svar.ok: # Felkoden i "error" är stabil; "message" är förklaringen på svenska. fel = svar.json() raise RuntimeError(f'{fel["error"]}: {fel["message"]} ({fel["request_id"]})') data = svar.json() ``` ## Ändringar ### 2026-09-06 — Idempotensen håller nu även för samtidiga omförsök Bakåtkompatibel Berör: /api/v1/verifikat, /api/v1/kundfakturor Idempotency-Key skyddade hittills en omsändning som gjordes EFTER att det första anropet svarat. Men det fel huvudet finns för är att nätverket tappar svaret medan anropet fortfarande pågår: din klient ser en timeout efter några sekunder och skickar om, och båda anropen var då igång samtidigt. Fem samtidiga anrop med samma nyckel gav fem verifikat. NYCKELN TAS NU I ANSPRÅK INNAN ARBETET BÖRJAR, och kapplöpningen avgörs i databasen: exakt ett anrop får skriva. NY FELKOD: 409 `idempotency_in_progress` betyder att ett anrop med samma nyckel behandlas just nu. Skicka om exakt samma anrop om några sekunder så får du det första svaret. BYT INTE NYCKEL och skapa inte ett nytt anrop — det första kan mycket väl ha bokfört. Koden är ny och tillkommer vid sidan av `idempotency_conflict`, som fortsatt betyder samma nyckel med en ANNAN kropp. ETT NEKAT ANROP SLÄPPER NYCKELN, precis som förut: får du 400 eller 422 kan du rätta kroppen och skicka om med samma referens. Inga fält, statuskoder eller befintliga felkoder ändrar betydelse. ### 2026-09-06 — Belopp och texter i ett verifikat har uttalade gränser Bakåtkompatibel Berör: /api/v1/verifikat Ett enskilt belopp och verifikatets summa måste rymmas i 9 999 999 999,99 kronor — kolumnernas verkliga gräns, som tidigare gav ett svårläst databasfel i stället för ett besked. Beskrivningen får vara högst 500 tecken, motparten 200 och radens anteckning 500. Gränserna avvisas med 400 och en mening som säger vilket fält som är för stort. Ett anrop som höll sig inom det rimliga påverkas inte. ### 2026-09-06 — POST /api/v1/verifikat — bokför en affärshändelse från ditt eget system Bakåtkompatibel Berör: /api/v1/verifikat Adressen som hittills bara gick att läsa tar nu emot skrivningar. En nyckel med behörigheterna Bokföra och Läsa kan skriva ett verifikat direkt i huvudboken — serie, datum, beskrivning, motpart och minst två rader med konto, debet och kredit. Kostnadsställe och projekt går att sätta per rad. VERIFIKATET SKRIVS GENOM MOTORNS EGEN FUNKTION, alltså samma väg som bokföringsformuläret i programmet: balanskravet, 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 gäller identiskt. Ett anrop som stoppas av ett lås får 422 med skälet utskrivet; en kropp som inte går att läsa får 400. IDEMPOTENCY-KEY ÄR OBLIGATORISKT, därför att ett verifikat inte går att ta bort — en dubbelpost rättas med ett ändringsverifikat och syns för alltid. Skicka din egen externa referens som nyckel: samma referens igen ger samma verifikat tillbaka, med svarshuvudet Idempotent-Replay. SVARET BÄR VÄGEN TILLBAKA: verifikatets id, serie, nummer, etiketten som står i huvudboken (A12) och en länk till posten i din egen installation. RUTTEN SKAPAR ALDRIG KONTON och låter dig aldrig välja källa — ett konto som saknas ger 422 med numret utskrivet, och systemkällorna är förbehållna programmets egna bokningar. GET på samma adress är oförändrad, ned till fältnamnen. ### 2026-09-05 — API v1: egna nycklar, tre nya endpoints och ett enhetligt felformat Bakåtkompatibel Berör: /api/v1/meta, /api/v1/verifikat, /api/v1/kundfakturor, /api/stats/overview, /api/stats/monthly, /api/stats/daily, /api/inbound/order, /api/inbound/peppol Installationen har nu ett eget API med nycklar du skapar själv under Inställningar → API-nycklar. Nyckeln bär identitet, behörighet och en kvot, syns i en lista med när och varifrån den senast användes, och återkallas med ett klick — återkallelsen biter i samma sekund, även mitt i ett pågående anrop. TRE NYA ENDPOINTS: /api/v1/meta säger vilken version installationen kör och vad din nyckel får göra, /api/v1/verifikat lämnar ut affärshändelser med sina rader och markörsidindelning, och /api/v1/kundfakturor skapar ett fakturautkast som kan bokföras direkt. INGENTING GAMMALT SLUTAR FUNGERA. STATS_API_KEY, ORDER_INBOUND_SECRET och PEPPOL_INBOUND_SECRET fortsätter fungera oförändrat, ned till felkropparna, och kan användas parallellt med de nya nycklarna. De befintliga rutterna svarar likadant som förut; det som tillkommit är en andra väg in. FELFORMATET är nu detsamma på hela ytan: en stabil maskinkod i error, en förklaring på svenska i message, strukturerad detail och ett request_id i både kropp och X-Request-Id. Befintliga felkoder behåller sin betydelse ordagrant — enhetligheten kom genom att message lades till där den saknades, aldrig genom att error ändrades. ### 2026-09-05 — Byråkontraktet oförändrat Bakåtkompatibel Berör: /api/byra/token, /api/stats/byra De två byrårutterna är orörda: samma form, samma felkoder, samma schema_version. De använder ett eget nyckelformat (dkb_) i en egen tabell och berörs inte av de nya API-nycklarna. Kontraktet är fryst och får bara utökas bakåtkompatibelt — den här loggen kommer aldrig att visa en brytande rad för de två adresserna. ## Mer - OpenAPI 3.1: /openapi.json - Referens på webben: /api-docs/referens - Kom igång: /api-docs/kom-igang Dokumentationen och OpenAPI-specen genereras ur samma datafiler som den här texten, och ett prov i bygget jämför dem med rutternas verkliga beteende. De kan alltså inte glida isär utan att bygget blir rött.