# Asstio API + MCP — Complete Specification for AI Assistants > Detta dokument är skrivet för LLMs (Claude, GPT, Cursor m.fl.) och innehåller hela Asstios publika API i ett format som är direkt anvanbart som kontext. Allt under `/api/v1/*` är **read-only** och stabilt. > > **Senast uppdaterad:** 2026-05-21 > **API-version:** v1 (stable) > **Kontakt:** henrik@asstio.com --- ## 1. Översikt Asstio är ett AI-first ERP-system för svenska handels- och grossistföretag. Systemet exponerar samma underliggande data på två sätt: - **REST** — `GET`/`POST` mot `/api/v1/*` — för servrar, script, dashboards, Power BI, Slack-bottar och egna webbappar. - **MCP (Model Context Protocol)** — för Claude Desktop, Cursor, Cline och andra AI-assistenter. Implementerar streamable-HTTP enligt [modelcontextprotocol.io](https://modelcontextprotocol.io/). Båda lager talar mot **samma databas**, samma versionskontrakt, samma authmodell. Skillnaden är vem som anropar. ### Base URLs ``` REST: https://.asstio.com/api/v1 MCP: https://-mcp.asstio.com/mcp ``` `` är kundens egna subdomän. Tenanten **härleds från tokens `InstanceId`-claim** — den passas aldrig manuellt i URL eller header. --- ## 2. Autentisering Varje request bär en **Zitadel JWT** i `Authorization: Bearer`-headern. ### 2.1 App-lösenord → JWT (för servrar och script) 1. Logga in i Asstio → `Inställningar → Användare → App-lösenord`. Skapa nytt, ge det ett namn (t.ex. "Power BI"), kopiera lösenordet. 2. Växla in mot en Bearer-token via Zitadels token-endpoint: ```bash curl -X POST "https://auth.asstio.com/oauth/v2/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password" \ -d "username=$EMAIL" \ -d "password=$APP_PASSWORD" \ -d "scope=openid profile asstio.api" ``` 3. Cacha token tills strax innan den löper ut (default ~12h). Vid 401 → hämta ny. ### 2.2 In-browser-sessioner Användare i Asstios webbgränssnitt har redan en token i sessionen. In-browser-kod (t.ex. egna dashboards inbäddade i Asstio) kan läsa den från sessionen och göra API-anrop direkt. ### 2.3 MCP-klienter (AI-assistenter) Claude Desktop, Cursor och Cline sköter OAuth själva via vår **RFC 9728 OAuth Protected Resource discovery**. Bara peka klienten på `https://-mcp.asstio.com/mcp` — resten är automatiskt. För enklare klienter som inte stödjer OAuth-discovery finns paste-baserad fallback (klistra in en token i klientens config). --- ## 3. Konventioner | Område | Konvention | |---|---| | Datum/tider | ISO 8601 UTC (`2026-05-21T00:00:00Z`) | | Encoding | UTF-8 | | Paginering | Cursor-baserad — använd `nextCursor` från responsen som `?cursor=...` i nästa request | | Default `pageSize` | 50 (max 500) | | Versionsheader | Varje response har `X-Asstio-Api-Version: v1` | | Numeriska belopp | `decimal` med 2 decimaler för pengar, ingen tusentalsavgränsare | | Felformat | RFC 7807 Problem Details (`application/problem+json`) | | Rate limits | REST: inga idag. MCP: 60 tool-calls/min/användare. | ### Standard PageResponse-format De flesta list-endpoints returnerar: ```json { "items": [ /* ... */ ], "nextCursor": "AQABBBgAAAA=", "pageSizeApplied": 50, "truncated": true } ``` `truncated: true` betyder "fler resultat finns — använd `nextCursor`". ### Standard felresponse ```json { "type": "https://asstio.com/errors/not-found", "title": "Customer not found", "status": 404, "detail": "No customer with id 9999 in this tenant.", "instance": "/api/v1/customers/9999" } ``` Vanliga statuskoder: `200 OK`, `400 Bad Request` (valideringsfel), `401 Unauthorized` (saknar/utgången token), `403 Forbidden` (rätt token, fel scope), `404 Not Found`, `429 Too Many Requests` (bara MCP), `500 Internal Server Error`. --- ## 4. Endpoints (REST) / Tools (MCP) Tio endpoints i v1, en MCP-tool per endpoint. Alla är **read-only**. ### 4.1 `GET /api/v1/sales-orders` · `list_sales_orders` Säljorderhuvuden — orderboken. Defaultar till `IsOrderIntake=1` (riktig orderboken, inte offerter). **Query-parametrar (alla valfria):** | Param | Typ | Beskrivning | |---|---|---| | `customerId` | int | Filtrera på kund | | `statusId` | int | Filtrera på orderstatus | | `orderDateFrom` | date | ISO-datum | | `orderDateTo` | date | ISO-datum | | `deliveryDateFrom` | date | ISO-datum | | `deliveryDateTo` | date | ISO-datum | | `currencyIsoCode` | string | t.ex. `SEK`, `EUR` | | `isLate` | bool | Endast försenade (qty kvar att leverera och deliveryDate < idag) | | `pageSize` | int | 1–500, default 50 | | `cursor` | string | Pagineringscursor | **Response item:** ```json { "id": 18743, "name": "Webshop order 2026-05", "customerId": 412, "customerName": "Acme Ltd", "statusId": 31, "statusName": "Bekräftad", "dateOrder": "2026-05-12T00:00:00Z", "dateDelivery": "2026-05-25T00:00:00Z", "totalExVat": 18250.00, "totalIncVat": 22812.50, "openAmount": 22812.50, "isLate": false, "currencyIsoCode": "SEK" } ``` ### 4.2 `GET /api/v1/purchase-orders` · `list_purchase_orders` Aktiva inköpsorder mot leverantörer. Defaultar till `IsPurchase=1`. **Query-parametrar:** `vendorId`, `statusId`, `orderDateFrom`, `orderDateTo`, `deliveryDateFrom`, `deliveryDateTo`, `currencyIsoCode`, `pageSize`, `cursor`. **Response item:** ```json { "id": 92044, "purchaseNumber": 2026131, "vendorId": 78, "vendorName": "Northwind Components AB", "statusName": "Beställd", "dateOrder": "2026-05-02T00:00:00Z", "dateDelivery": "2026-05-30T00:00:00Z", "orderTotalExVat": 42500.00, "receivedValueExVat": 12700.00, "invoicedValueExVat": 0.00, "openToInvoiceExVat": 12700.00, "currencyIsoCode": "SEK" } ``` ### 4.3 `GET /api/v1/sales-invoices` · `list_sales_invoices` Säljfakturahuvuden. Defaultar till `IsSales=1`. Kreditfakturor inkluderas via `documentType=credit`. **Query-parametrar:** `customerId`, `statusId`, `invoiceDateFrom`, `invoiceDateTo`, `dueDateFrom`, `dueDateTo`, `isLate`, `isFullyPaid`, `documentType` (`invoice`/`credit`/`all`), `currencyIsoCode`, `pageSize`, `cursor`. **Response item:** ```json { "id": 88312, "invoiceNumber": 2026301, "customerId": 412, "customerName": "Acme Ltd", "statusName": "Skickad", "dateInvoice": "2026-05-01T00:00:00Z", "dateDue": "2026-05-31T00:00:00Z", "totalExVat": 12000.00, "totalIncVat": 15000.00, "openAmount": 15000.00, "paidAmount": 0.00, "isLate": false, "isFullyPaid": false, "currencyIsoCode": "SEK", "documentType": "invoice" } ``` ### 4.4 `POST /api/v1/sales-invoice-lines` · `list_sales_invoice_lines` Sålda artikelrader. **POST** används istället för GET eftersom filterytan är bred (datumintervall + multipla optionella filter). **Request body:** ```json { "from": "2026-01-01T00:00:00Z", "to": "2026-05-20T00:00:00Z", "customerId": 412, "productId": null, "statusId": null, "includeNonSales": false, "pageSize": 100, "cursor": null } ``` `from`/`to` är obligatoriska. Övriga fält valfria. `includeNonSales=true` tar med fakturor som inte är `IsSales=1`. **Response item:** ```json { "invoiceId": 88312, "invoiceNumber": 2026301, "lineId": 410551, "productId": 9921, "productNumber": "WID-12", "description": "Widget, large", "quantity": 4.0, "qtyDelivered": 4.0, "price": 3000.00, "discount": 0.0, "sumExVat": 12000.00, "sumIncVat": 15000.00, "customerId": 412, "customerName": "Acme Ltd", "currencyIsoCode": "SEK", "dateInvoice": "2026-05-01T00:00:00Z" } ``` ### 4.5 `GET /api/v1/open-order-lines` · `list_open_order_lines` Orderrader med **kvar att leverera**. `qtyLeftToDeliver` räknas på servern (`qtyOrdered - qtyDelivered`). **Query-parametrar:** `customerId`, `productId`, `warehouseId`, `pageSize`, `cursor`. **Response item:** ```json { "orderId": 18743, "orderName": "Webshop order 2026-05", "orderLineId": 220331, "productId": 9921, "productNumber": "WID-12", "description": "Widget, large", "qtyOrdered": 10.0, "qtyDelivered": 4.0, "qtyLeftToDeliver": 6.0, "dateDelivery": "2026-05-25T00:00:00Z", "customerId": 412, "customerName": "Acme Ltd", "statusName": "Bekräftad" } ``` ### 4.6 `GET /api/v1/warehouse-stock` · `get_warehouse_stock` Saldo per produkt per lagerplats. `qtyDistributable` är färdig­räknad (`qtyInStock - qtyOutgoing + qtyIncoming`). **Query-parametrar (minst en av `productId` eller `warehouseId` rekommenderas):** `productId`, `warehouseId`, `locationId`, `onlyInStock` (bool), `pageSize`, `cursor`. **Response item:** ```json { "productId": 9921, "productNumber": "WID-12", "productDescription": "Widget, large", "warehouseId": 1, "warehouseName": "Main", "locationId": 17, "locationName": "A-12-3", "qtyInStock": 47.0, "qtyIncoming": 20.0, "qtyOutgoing": 6.0, "qtyDistributable": 61.0 } ``` ### 4.7 `GET /api/v1/vendor-prices` · `get_vendor_prices_for_product` Vilka leverantörer har vilka priser på en specifik produkt. **Query-parametrar:** `productId` (obligatorisk), `currencyIsoCode` (valfri), `onlyActive` (bool, default true). **Response item:** ```json { "productId": 9921, "productNumber": "WID-12", "supplierId": 78, "supplierName": "Northwind Components AB", "supplierProductNumber": "NW-W-LRG", "price": 850.00, "currencyIsoCode": "SEK", "leadTimeDays": null, "validFrom": null, "validTo": null } ``` ### 4.8 `GET /api/v1/products/find` · `find_product` Sök produkt på namn, artikelnummer eller streckkod. Bra för disambiguering innan andra anrop. **Query-parametrar:** `q` (obligatorisk, fritext), `limit` (1–20, default 5), `includeInactive` (bool, default false). **Response:** array (inte PageResponse) av kandidater rankade efter relevans: ```json [ { "productId": 9921, "productNumber": "WID-12", "description": "Widget, large", "barcode": null, "isActive": true } ] ``` ### 4.9 `GET /api/v1/customers` · `list_customers` Sök eller lista kunder. Fritextsök mot id, contactNumber, namn eller orgnr. **Query-parametrar:** `q` (fritext), `includeInactive` (bool), `pageSize`, `cursor`. **Response item:** ```json { "id": 412, "contactNumber": 10042, "name": "Acme Ltd", "orgno": "556677-8899", "currencyIsoCode": null, "inactive": false, "openOrderCount": 3 } ``` ### 4.10 `GET /api/v1/customers/{id}` · `get_customer` Full kunddetalj. **Inte** ett PageResponse — ett enskilt objekt eller 404. **Path-parameter:** `id` (int, customer.id eller contactNumber). **Response:** ```json { "id": 412, "contactNumber": 10042, "name": "Acme Ltd", "orgno": "556677-8899", "creditLimit": 250000.00, "creditRemaining": 125000.00, "overdueOrderSum": 4200.00, "creditStop": false, "paymentTermName": "30 dagar netto", "deliveryTermName": "Fritt vårt lager", "priceListName": "Standard", "currencyIsoCode": "SEK", "inactive": false } ``` --- ## 5. MCP-konfiguration ### 5.1 Claude Desktop / Cline Lägg till i `claude_desktop_config.json`: ```json { "mcpServers": { "asstio": { "url": "https://-mcp.asstio.com/mcp" } } } ``` Claude kommer be om OAuth-godkännande första gången. Token cachas lokalt. ### 5.2 Cursor I Cursor settings → MCP, lägg till samma URL. Cursor använder samma OAuth-flöde. ### 5.3 Custom-klienter Servern annonserar OAuth-discovery enligt RFC 9728 på: ``` GET https://-mcp.asstio.com/.well-known/oauth-protected-resource ``` Använd `resource_server_metadata.authorization_servers[0]` för att hitta Zitadel-instansen. --- ## 6. Versions­kontrakt Inuti **v1** gör vi bara **additiva** ändringar: - ✅ Nya endpoints - ✅ Nya valbara fält i responses - ✅ Nya valbara query-parametrar - ❌ Existerande fältnamn byts inte - ❌ Typer ändras inte - ❌ Default-beteenden ändras inte - ❌ Required-status på fält ändras inte Breaking changes landar på `/api/v2/*`. v1 fortsätter köra tills sista integratorn har migrerat. Pin mot `X-Asstio-Api-Version: v1` om ni vill ha försvar mot oavsiktliga klient-uppgraderingar. --- ## 7. Use cases (för LLM-prompts) Konkreta exempel på vad ni kan be Claude/Cursor göra: - "Lista de tre största kunderna som har förfallna fakturor över 10 000 kr" → `list_customers` + `list_sales_invoices(isLate=true, isFullyPaid=false)` - "Vilka produkter har vi sålt till Acme den här månaden?" → `list_customers(q=Acme)` → `list_sales_invoice_lines(customerId=…, from=…, to=…)` - "Hur mycket av WID-12 har vi i lager just nu?" → `find_product(q=WID-12)` → `get_warehouse_stock(productId=…)` - "Vilka leverantörer säljer Widget large och vilket pris ger oss bäst marginal?" → `find_product` → `get_vendor_prices_for_product` → jämför mot senaste säljpriset från `list_sales_invoice_lines` - "Sammanfatta vår orderbok per kund för maj" → `list_sales_orders(orderDateFrom=2026-05-01)` - "Vad har Acme för kreditstatus?" → `list_customers(q=Acme)` → `get_customer(id=…)` --- ## 8. Vad som **inte** finns i v1 - ❌ Skrivande operationer (POST/PUT/DELETE av affärsdata). `POST /sales-invoice-lines` är read med body, inte create. - ❌ Bokföringsdata (huvudbok, kontoplan). Kommer i v1.x. - ❌ Lön (pågående utveckling). - ❌ Lagerrörelser detaljerat (bara aktuella saldon idag). - ❌ Bilagor och dokument. Skrivande endpoints kommer i **v2** efter att security-review är klar. --- ## 9. Kontakt Få access (app-lösenord + tenant-URL): **henrik@asstio.com** eller boka demo på [asstio.com/demo](https://www.asstio.com/demo). Buggar / förslag: skicka mail med rubrik `[API]` så hamnar det rätt direkt. --- **Slut på spec.** Klistra in detta dokument som kontext i Claude/ChatGPT/Cursor för att få hjälp att bygga integrationer mot Asstio.