SAP-litetest system · not SAP

SAP-lite API help

SAP-lite imitates the SAP S/4HANA OData V2 APIs Arve uses. It is a test system, not SAP software. Back to the SAP-lite screens · Download the Postman collection

Ports and addresses

WhatAddressServes
SAP APIs (HTTPS)https://localhost:44300 from this PC, https://sap-lite:44300 from Arve's Docker containers/sap/opu/odata/sap/..., /health, /sap-lite-ca.crt
Screens and admin API (HTTP)http://localhost:4180 (this PC only; SAP_LITE_HTTP_PORT)These screens and /api/v1/...
Tunnel port (optional, HTTP)SAP_LITE_TUNNEL_PORT, bound to 127.0.0.1SAP APIs only, for a tunnel that adds public HTTPS

The HTTPS certificate is signed by the SAP-lite CA. Trust sap-lite-ca.crt in Arve (the Docker setup mounts it for Arve's containers). The connection details, including the password, are on the SAP connection page.

Cloud Arve: start SAP-lite with SAP_LITE_TUNNEL_PORT=44380 (any free port) and point a tunnel (for example cloudflared tunnel --url http://localhost:44380) at it. Use the tunnel's HTTPS URL as the base URL in Arve. The tunnel port answers only the SAP APIs, never the screens or the admin API, and still needs the SAP sign-in.

SAP OData APIs

Every service lives at /sap/opu/odata/sap/<SERVICE>/. Requests need HTTP Basic authentication of the communication user (default ARVE_COMM) and sap-client=100 as a query parameter (or the sap-usercontext cookie). Send Accept: application/json; OData V2 JSON comes back wrapped in {"d": ...}, decimals as strings and dates as /Date(ms)/. Errors use the SAP error body {"error": {"code", "message": {"value"}, "innererror"}}.

ServiceEntity sets
API_BUSINESS_PARTNERA_BusinessPartner, A_Supplier, A_Customer, addresses, roles, bank, tax numbers, supplier/customer company and purchasing/sales data
API_PURCHASEORDER_PROCESS_SRVA_PurchaseOrder, A_PurchaseOrderItem
API_MATERIAL_DOCUMENT_SRVA_MaterialDocumentHeader, A_MaterialDocumentItem (goods receipts)
API_SALES_ORDER_SRVA_SalesOrder, A_SalesOrderItem, pricing elements, partners
API_BILLING_DOCUMENT_SRVA_BillingDocument, A_BillingDocumentItem, function GetPDF
API_SUPPLIERINVOICE_PROCESS_SRVA_SupplierInvoice (POST to create), A_SuplrInvcItemPurOrdRef, A_SupplierInvoiceItemGLAcct, A_SupplierInvoiceTax, function Cancel

The SAP connection page lists every entity set with its keys.

Query options

$metadata.../API_SUPPLIERINVOICE_PROCESS_SRV/$metadata (EDMX XML)
Key read.../API_BUSINESS_PARTNER/A_Supplier('17300001')
$filter$filter=Supplier eq '17300001' and CompanyCode eq '1710'
$select$select=PurchaseOrder,Supplier (an unknown property answers 400)
$expandA_PurchaseOrder('4500001001')?$expand=to_PurchaseOrderItem
$top, $skip, $orderby$top=2&$skip=1&$orderby=PurchaseOrder desc
$inlinecount$inlinecount=allpages adds __count
$count.../A_PurchaseOrder/$count returns a plain number

Changes: CSRF token and session cookie

Before a POST, fetch a token with a GET that sends x-csrf-token: Fetch. The answer carries the token in x-csrf-token and sets the session cookies (SAP_SESSIONID_S4L_100, sap-usercontext). Send both the token and the cookies with the POST; otherwise SAP-lite answers 403 with x-csrf-token: Required. Fetch again after a 403.

GET  /sap/opu/odata/sap/API_SUPPLIERINVOICE_PROCESS_SRV/?sap-client=100
     Authorization: Basic ...   x-csrf-token: Fetch
  <- x-csrf-token: <token>      set-cookie: SAP_SESSIONID_S4L_100=...; sap-usercontext=...

POST /sap/opu/odata/sap/API_SUPPLIERINVOICE_PROCESS_SRV/A_SupplierInvoice?sap-client=100
     Authorization: Basic ...   x-csrf-token: <token>   Cookie: <both cookies>
     Content-Type: application/json
{
  "CompanyCode": "1710",
  "DocumentDate": "/Date(1792368000000)/",
  "PostingDate": "/Date(1792368000000)/",
  "SupplierInvoiceIDByInvcgParty": "APE-2026-1234",
  "InvoicingParty": "17300001",
  "DocumentCurrency": "USD",
  "InvoiceGrossAmount": "5184",
  "TaxIsCalculatedAutomatically": true,
  "DocumentHeaderText": "ARVE-0001",
  "to_SuplrInvcItemPurOrdRef": { "results": [ {
    "SupplierInvoiceItem": "1", "PurchaseOrder": "4500001001", "PurchaseOrderItem": "10",
    "TaxCode": "I1", "DocumentCurrency": "USD", "SupplierInvoiceItemAmount": "4800",
    "PurchaseOrderQuantityUnit": "PC", "QuantityInPurchaseOrderUnit": "100" } ] }
}
  <- 201 Created, d.SupplierInvoice = 51056....   sap-message: {"message":"Document no. ... created"}

SAP-lite checks the posting like SAP: supplier, company code, PO, units, goods receipt quantity, tax codes, open posting period, balance (M8/534) and duplicates (M8/108). Price or quantity variances post the invoice with PaymentBlockingReason = "R". Refusals are listed under Supplier invoices · Rejected by SAP-lite.

Status lookup and reversal

GET  .../API_SUPPLIERINVOICE_PROCESS_SRV/A_SupplierInvoice?$filter=DocumentHeaderText eq 'ARVE-0001'&sap-client=100

POST .../API_SUPPLIERINVOICE_PROCESS_SRV/Cancel?PostingDate=datetime'2026-10-19T00:00:00'&ReversalReason='01'
     &SupplierInvoice='5105600011'&FiscalYear='2026'&sap-client=100   (with token and cookies)

After a timeout or HTTP 500, look the invoice up by DocumentHeaderText (or SupplierInvoiceIDByInvcgParty) before posting again: with fault injection SAP-lite can save the document and still answer an error.

Admin API (SAP-lite screens and scripts)

Base URL http://localhost:4180/api/v1. Every call needs Authorization: Bearer <key>; the key is in data/api-key.txt (or SAP_LITE_API_KEY). Bodies are JSON. Cross-origin calls are refused.

RoutePurpose
GET /stateAll SAP-lite data with revision (also the ETag).
PUT /stateReplace all data: {"data": ...} with If-Match: "<revision>"; 409 when the data changed meanwhile.
GET /sap/connectionBase URLs, client, user, password, CA download, services.
GET / DELETE /sap/logSAP API call log {"entries": [...]} (last 500), or clear it.
GET / PUT /sap/settings{"settings": {"duplicateCheck", "priceTolerancePct", "priceToleranceAbs", "csrf", "openPeriods", "faults": {"mode", "remaining", "delayMs"}}}; GET also returns faultModes.
POST /test-documentsGenerate a test document: {"order": "4500001004", "scenario": "clean", "reference": "optional"}.
GET /test-resultsSummary and one row per test document: expected vs actual outcome and fields.
GET /test-documents/answer-keyExpected outcome and fields per test document.
GET /test-documents/pdfs.zipAll test PDFs plus answer-key.json.
GET /invoices/(ap|ar)/:id/pdfOne invoice PDF.
POST /invoices/ap/:id/releaseRelease the payment block of a posted supplier invoice (MRBR). 409 when it is not posted, not blocked or reversed. The test run still scores it as posted and blocked.
POST /invoices/ap/:id/reverseReverse a posted supplier invoice (MR8M): {"postingDate": "2026-10-19", "reason": "01"}, both optional (today, 01). 422 when the posting period is closed, 409 when it is already reversed.
POST /test/reset-runRemove what Arve posted through the API (and reversals of it) and the rejections.
POST /test/resetReplace all data with the sample data: {"confirm": "RESET"}.

Scenarios: clean, price-variance, over-receipt, missing-po, duplicate, unit-mismatch, wrong-total, unknown-po, wrong-supplier, no-goods-receipt. Layouts ("layout" in POST /test-documents, optional): standard, stacked, german. Fault modes: none, reject-500, commit-then-500, commit-then-timeout, timeout-no-commit, csrf-expired, unavailable-503, slow.

curl -s -H "Authorization: Bearer $(cat data/api-key.txt)" http://localhost:4180/api/v1/test-results