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
| What | Address | Serves |
|---|---|---|
| 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.1 | SAP 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"}}.
| Service | Entity sets |
|---|---|
API_BUSINESS_PARTNER | A_BusinessPartner, A_Supplier, A_Customer, addresses, roles, bank, tax numbers, supplier/customer company and purchasing/sales data |
API_PURCHASEORDER_PROCESS_SRV | A_PurchaseOrder, A_PurchaseOrderItem |
API_MATERIAL_DOCUMENT_SRV | A_MaterialDocumentHeader, A_MaterialDocumentItem (goods receipts) |
API_SALES_ORDER_SRV | A_SalesOrder, A_SalesOrderItem, pricing elements, partners |
API_BILLING_DOCUMENT_SRV | A_BillingDocument, A_BillingDocumentItem, function GetPDF |
API_SUPPLIERINVOICE_PROCESS_SRV | A_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) |
$expand | A_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.
| Route | Purpose |
|---|---|
GET /state | All SAP-lite data with revision (also the ETag). |
PUT /state | Replace all data: {"data": ...} with If-Match: "<revision>"; 409 when the data changed meanwhile. |
GET /sap/connection | Base URLs, client, user, password, CA download, services. |
GET / DELETE /sap/log | SAP 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-documents | Generate a test document: {"order": "4500001004", "scenario": "clean", "reference": "optional"}. |
GET /test-results | Summary and one row per test document: expected vs actual outcome and fields. |
GET /test-documents/answer-key | Expected outcome and fields per test document. |
GET /test-documents/pdfs.zip | All test PDFs plus answer-key.json. |
GET /invoices/(ap|ar)/:id/pdf | One invoice PDF. |
POST /invoices/ap/:id/release | Release 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/reverse | Reverse 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-run | Remove what Arve posted through the API (and reversals of it) and the rejections. |
POST /test/reset | Replace 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