SAP Business One Service Layer: login, $filter, $select and paging, with working examples
By Zain Abbas, Kodenova · Updated
What the Service Layer is
The Service Layer is SAP Business One’s web API. It exposes business objects such as business partners, items, orders and invoices over HTTPS, using the OData protocol. Any language that can send an HTTP request can use it: JavaScript, Python, C#, PHP, or a low-code tool.
It started on the HANA version of SAP Business One. Recent 10.0 feature packs also offer it for SQL Server. By default it listens on port 50000 over HTTPS:
https://your-b1-server:50000/b1s/v1/
https://your-b1-server:50000/b1s/v2/
v1 or v2
Both paths reach the same business objects.
- /b1s/v1 uses OData version 3. It still works and most older code uses it.
- /b1s/v2 uses OData version 4, which SAP now treats as the main protocol.
The difference you notice first is in responses. v1 returns odata.nextLink, v2 returns @odata.nextLink. For new projects, pick v2 and stick with it. The examples below use v1 paths because they are still the most common in the field. Swap v1 for v2 and they work the same way.
Step 1: Log in
Send a POST with your company database, user and password:
POST https://your-b1-server:50000/b1s/v1/Login
Content-Type: application/json
{
"CompanyDB": "SBODEMOUS",
"UserName": "manager",
"Password": "your-password"
}
A successful response contains the session ID, the version and the timeout in minutes:
{
"SessionId": "1c9e4b2a-....",
"Version": "1000xxx",
"SessionTimeout": 30
}
The server also sets two cookies: B1SESSION and ROUTEID.
- B1SESSION identifies your session.
- ROUTEID tells the Service Layer load balancer which node holds your session, so each request goes back to the same node.
A browser stores and sends these cookies for you. Outside a browser, you must add both to the Cookie header of every later call:
Cookie: B1SESSION=1c9e4b2a-....; ROUTEID=.node1
The session expires after 30 minutes of no activity by default. Every call resets the timer. Build your client so that a 401 response triggers one fresh login and a retry, not an endless loop.
Step 2: Read data with GET
A plain GET returns a page of records with all fields:
GET /b1s/v1/BusinessPartners
That is slow and heavy. Use the OData query options to ask only for what you need.
| Option | What it does | Example |
|---|---|---|
$select | Return only these fields | $select=CardCode,CardName,Balance |
$filter | Return only matching records | $filter=CardType eq 'cCustomer' |
$orderby | Sort the results | $orderby=DocDate desc |
$top | Return at most n records | $top=50 |
$skip | Skip the first n records | $skip=100 |
Put together:
GET /b1s/v1/Orders?$select=DocEntry,DocNum,CardCode,DocDate,DocTotal&$filter=DocDate ge '2026-10-01' and DocumentStatus eq 'bost_Open'&$orderby=DocDate desc&$top=50
A few things that trip people up:
- Field names are the Service Layer names, not the SQL column names. It is
DocumentStatus, notDocStatus. Enum values are names likebost_OpenandcCustomer, notOandC. - Strings and dates go in single quotes. Numbers do not.
- Filter operators are words:
eq,ne,gt,ge,lt,le, joined withandandor. - Text search uses functions, for example
startswith(CardName, 'Al'). - URL-encode spaces and quotes when your HTTP library does not do it for you.
To read one record, put its key in brackets: GET /b1s/v1/Orders(125) for the order with DocEntry 125, or GET /b1s/v1/BusinessPartners('C20000') for a business partner.
Our Service Layer request builder puts these URLs together for you, with the right quoting.
Step 3: Handle paging
The Service Layer never returns everything at once. The default page size is 20. When there are more records, the response ends with a link to the next page:
{
"value": [ ... 20 records ... ],
"odata.nextLink": "Orders?$select=DocEntry,DocNum&$skip=20"
}
You have two ways to deal with it.
- Follow the nextLink. Call the URL it gives (it is relative to
/b1s/v1/) until no nextLink comes back. This is the safest way to read a full list. - Ask for bigger pages. Send a
Preferheader:
GET /b1s/v1/Items?$select=ItemCode,ItemName,QuantityOnStock
Prefer: odata.maxpagesize=200
The server confirms the size it used in the Preference-Applied header. odata.maxpagesize=0 turns paging off, but avoid that on large tables like invoices or journal entries. A huge response can time out or put load on the server.
You can also page yourself with $top and $skip. If you do, always add $orderby on a key field, or records can move between pages.
Step 4: Create a sales order with POST
POST /b1s/v1/Orders
Content-Type: application/json
Cookie: B1SESSION=...; ROUTEID=...
{
"CardCode": "C20000",
"DocDueDate": "2026-10-30",
"Comments": "Created from web portal",
"DocumentLines": [
{ "ItemCode": "A00001", "Quantity": 5, "WarehouseCode": "01" },
{ "ItemCode": "A00002", "Quantity": 2, "UnitPrice": 150.0 }
]
}
A successful POST returns 201 Created with the full new document, including DocEntry and DocNum. Save the DocEntry. You need it for every later update.
Step 5: Update with PATCH
PATCH changes only the fields you send. Everything else stays as it is:
PATCH /b1s/v1/Orders(125)
Content-Type: application/json
{
"Comments": "Delivery moved to Friday",
"DocDueDate": "2026-11-06"
}
A successful PATCH returns 204 No Content. Be careful when you PATCH document lines. Test on a copy of the company first, and read the document back afterwards to confirm the lines look the way you expect.
Step 6: Log out
POST /b1s/v1/Logout
Logging out frees the session on the server. For a nightly job, log in once, do the work and log out. For a web portal, keep one session per user or a small pool, rather than logging in on every request.
Common errors and what they mean
| Error | Usual cause | Fix |
|---|---|---|
| 401 Unauthorized | Session expired, or cookies not sent | Log in again; check both cookies are in the request |
| CORS error in browser | Origin not allowed | Enable CORS in the Service Layer configuration (CorsEnable, CorsAllowedOrigins), or call from your backend |
| SSL or certificate error | Default self-signed certificate | Install a proper certificate; trust the self-signed one only in test |
| Login fails with a licence message | User has no suitable licence | Assign a licence to the user in License Administration |
| 400 with an error message | Missing field, wrong value or business rule | Read error.message.value; it names the field |
One more: if your logins work but calls fail at random behind a load balancer, check that ROUTEID is being sent. Without it, the request can reach a node that does not know your session.
DI API or Service Layer?
| DI API | Service Layer | |
|---|---|---|
| Type | COM library | REST / OData over HTTPS |
| Platform | Windows | Any client platform |
| Languages | Mainly .NET | Any language with HTTP |
| Best for | Classic add-ons next to the B1 client | Web portals, mobile apps, cloud integrations |
| Concurrency | One connection per process | Many sessions, load balanced |
For anything new that talks to SAP Business One from outside the client, we use the Service Layer. It is what we build on for business portals and mobile apps. The DI API still has a place in existing Windows add-ons.
To try these calls against your own system, start with the Service Layer request builder.
If you need a portal, app or integration built on the Service Layer, contact Kodenova with your SAP Business One version and what you want to connect.
Sources
- SAP Learning: Exploring various data retrieval options using GET request (Service Layer)
- SAP Help Portal: Working with SAP Business One Service Layer (user manual, PDF)
- SAP Help Portal: Service Layer API Reference
- SAP Community: Is it possible to log into B1 Service Layer with cookies?
- SAP Community: Service Layer CORS header Access-Control
- Apideck: SAP Business One API integration guide
Related services
Frequently asked questions
What is the difference between /b1s/v1 and /b1s/v2?
The v1 path uses OData version 3 and v2 uses OData version 4. SAP now treats v4 as the main protocol. The main visible difference in responses is the annotation prefix: v2 returns @odata.nextLink where v1 returns odata.nextLink.
How long does a Service Layer session last?
The default timeout is 30 minutes of inactivity. The Login response returns the value in SessionTimeout. Each call resets the timer, so a busy integration keeps its session alive.
Why do I only get 20 records back?
The Service Layer pages results, and the default page size is 20. Follow the nextLink in the response, or send the header Prefer: odata.maxpagesize=100 to get larger pages.
Should I use the DI API or the Service Layer for a new project?
For new web, mobile and cloud integrations, use the Service Layer. It works over HTTPS from any language and platform. The DI API still makes sense for existing .NET add-ons that run on Windows next to the SAP Business One client.
Why does my browser app fail with a CORS error when Postman works?
Browsers block cross-origin calls unless the server allows them. CORS is switched off in the Service Layer by default. Enable it in the Service Layer configuration and list your app's origin, or call the Service Layer from your own backend instead.
Ready to get started?
Tell us what you need. You get a clear technical answer, a fixed scope and a price, usually within 24 hours.
