SAP

SAP ERP APIs, S/4HANA Cloud OData v2 A2X services with communication users or OAuth, Business One Service Layer sessions, and ByDesign OData, read and write.

SAP is the largest enterprise ERP vendor in the world and is not one API but a family of them. A commerce brand will be sitting on one of four very different products: SAP S/4HANA Cloud (public or private edition), SAP S/4HANA on premise, SAP Business One (the small and mid-market product, very common in Indian and South East Asian distribution businesses), or the legacy SAP Business ByDesign. Each of these exposes sales orders, invoices, materials, stock and business partners, but the protocol, the base path and the authentication are different in each. This page treats them separately and says which product each endpoint belongs to. Getting this wrong is the single most common failure when building an SAP connector, so establish the product and the release first.

At a glance

Warning

api.sap.com and help.sap.com render their reference content in JavaScript and returned an empty body to our fetcher on 2026-09-21. Everything below was taken from SAP's own published OpenAPI specifications as mirrored in the Ballerina connector repositories, from SAP-samples repositories on GitHub, and from the SAP Cloud Application Programming Model documentation. Field names and paths are therefore accurate, but SAP's prose caveats and the exact wording of limits could not be read. Treat any number quoted here as needing confirmation against the tenant.

What it is

SAP S/4HANA is SAP's current ERP suite. The public cloud edition is a multi-tenant SaaS product where the only supported integration route is the published allowlist of APIs, so an integrator gets a clean, versioned, documented surface. The private cloud and on premise editions run the same ABAP stack but the customer can switch extra services on, which means on premise tenants often expose more than the cloud list and also often expose nothing at all until someone activates the service. SAP Business One is a separate code line for companies roughly under 250 employees, sold through partners and very common in Indian distribution, and it exposes the Service Layer, an OData-flavoured HTTP API over the DI API object model. SAP Business ByDesign is a legacy cloud suite in maintenance mode with a meaningful residual installed base in mid-market manufacturing.

For a commerce operation the practical consequence is this: SAP is almost never the system of record for marketplace listings. It is the system of record for the item master, for stock by plant and storage location, for the customer master, and for the financial documents (invoices, credit memos). Orders usually flow into SAP from a channel or an order management layer, and stock flows out of SAP to the channels. Design the connector in that direction.

API access

There is no self-serve developer signup. Credentials are issued inside the customer's own SAP system, so every step below needs the customer's SAP basis or administration team.

S/4HANA Cloud (public edition)

  1. The customer's administrator opens the Communication Systems Fiori app and creates a communication system representing your integration (host name, and either a user with password or an inbound client certificate).
  2. The administrator opens Communication Arrangements and creates an arrangement from a communication scenario. The scenario is what actually exposes a set of OData services. Verified scenario identifiers include SAP_COM_0109 (sales order integration) and SAP_COM_0008 (business partner integration). Each scenario page lists the inbound services it switches on.
  3. The arrangement page then shows the exact service URLs for that tenant, of the form https://<tenant>-api.s4hana.cloud.sap/sap/opu/odata/sap/<SERVICE_NAME>.
  4. Nothing outside an activated communication arrangement is reachable. If an endpoint returns 403 on a cloud tenant, the usual cause is a missing arrangement rather than a missing role.

S/4HANA on premise and private cloud

The same OData services exist under /sap/opu/odata/sap/<SERVICE_NAME> on the customer's gateway host, but each service must be activated in transaction /IWFND/MAINT_SERVICE and the technical user needs the relevant authorisation objects. Service names on premise are often suffixed with a version, for example OP_API_PRODUCT_SRV_0001, and the OData v4 variants use /sap/opu/odata4/sap/....

Business One

The Service Layer runs on the customer's Business One server, by default on HTTPS port 50000, at https://<server>:50000/b1s/v1 (a v2 path https://<server>:50000/b1s/v2 exists on newer releases and speaks OData v4). It is usually not exposed to the public internet, so a connector needs a tunnel, a VPN or a reverse proxy agreed with the customer.

ByDesign

Custom OData services are published per tenant at https://<tenant>/sap/byd/odata/cust/v1/<servicename>/, with a $metadata document at https://<tenant>/sap/byd/odata/cust/v1/<servicename>/$metadata?sap-label=true&sap-language=en. The customer creates and uploads the service definition in the Application and User Management, OData Services work centre view. SAP publishes sample service definitions and a Postman collection at SAP-samples/byd-api-samples.

SDKs

  • SAP Cloud SDK for Java and for JavaScript/TypeScript, which generate typed clients from the published service metadata and handle destination lookup and token refresh on SAP BTP.
  • SAP Cloud Application Programming Model (CAP), which imports the $metadata EDMX of any of these services with cds import and gives a generated remote service.
  • Community connectors such as the Ballerina sap.s4hana.* and sap.businessone modules, which ship the OpenAPI specifications used as the primary source for this page.

Authentication

S/4HANA Cloud, communication user (basic auth)

This is the most common path and the one to build first.

  1. The administrator creates a communication user with a password in the Maintain Communication Users app.
  2. The administrator binds that user to a communication arrangement, which grants it exactly the inbound services of that scenario.
  3. Every request sends Authorization: Basic base64(user:password).
  4. There is no token and no refresh. The credential is a long lived username and password, so store it encrypted and rotate on the customer's schedule.
  5. Any state changing request (POST, PATCH, DELETE) on an OData v2 A2X service additionally needs a CSRF token. Fetch one with a GET that carries x-csrf-token: Fetch, then replay the returned token value and the returned session cookies on the write.
  # 1. fetch a CSRF token and the session cookies
  # the response headers carry x-csrf-token and set-cookie: SAP_SESSIONID_...
curl -i -u 'COMM_USER:secret' -H 'x-csrf-token: Fetch' -c cookies.txt \
  'https://mytenant-api.s4hana.cloud.sap/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder?$top=1'

  # 2. authenticated read
curl -u 'COMM_USER:secret' -H 'Accept: application/json' \
  'https://mytenant-api.s4hana.cloud.sap/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder?$top=2&$filter=CreationDate%20ge%20datetime%272026-09-01T00:00:00%27'

S/4HANA Cloud, OAuth 2.0

The published OpenAPI specifications for the A2X services declare two security schemes: BasicAuth and OAuth2Auth, the latter an authorization code flow whose scope is the service identifier, for example API_SALES_ORDER_SRV_0001. The token, authorization and refresh URLs are tenant specific and are shown on the communication arrangement, so they are not reproduced here. On SAP BTP the usual arrangement is a destination of type OAuth2ClientCredentials or OAuth2SAMLBearerAssertion, where the SDK or CAP runtime fetches and refreshes the token and you never see it. Token lifetime is configured per tenant and is not published.

API Business Hub sandbox

For exploration only, SAP hosts a shared sandbox. Sign in to api.sap.com, copy the API key from the "Show API Key" button, and call:

GET https://sandbox.api.sap.com/s4hanacloud/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder?$top=2 HTTP/1.1
APIKey: <your sandbox api key>
Accept: application/json

The sandbox is read-mostly, shared between all developers, and its data is reset periodically. Do not build against its identifiers.

Business One Service Layer

  1. POST /b1s/v1/Login with the company database, the Business One user and the password.
  2. The response carries a SessionId in the body and a B1SESSION cookie plus a ROUTEID cookie in Set-Cookie. The ROUTEID matters when the Service Layer is load balanced across nodes; drop it and later calls hit a node that does not know the session.
  3. Send both cookies on every subsequent call.
  4. Sessions expire after an idle timeout configured on the server, 30 minutes by default. Re-login on a 401.
  5. POST /b1s/v1/Logout ends the session. The Service Layer has a licensed session limit, so a connector that logs in per request will exhaust it. Keep one session per company database and refresh it.
POST /b1s/v1/Login HTTP/1.1
Host: b1server.example.com:50000
Content-Type: application/json

{
  "CompanyDB": "SBODEMOIN",
  "UserName": "manager",
  "Password": "secret"
}
HTTP/1.1 200 OK
Set-Cookie: B1SESSION=8a4f3f4e-1c2b-11f0-9c1f-000c29a1b2c3; HttpOnly; Secure
Set-Cookie: ROUTEID=.node1; path=/

{
  "SessionId": "8a4f3f4e-1c2b-11f0-9c1f-000c29a1b2c3",
  "Version": "1000200",
  "SessionTimeout": 30
}

An authenticated Business One read then looks like curl -b 'B1SESSION=...; ROUTEID=.node1' -H 'Prefer: odata.maxpagesize=100' 'https://b1server.example.com:50000/b1s/v1/Orders?$select=DocEntry,DocNum,CardCode,DocDate,DocTotal'.

Multi-account

Every SAP tenant is a separate customer system with its own host, its own credentials and often its own activated service list. There is no concept of one application acting for many SAP tenants under one credential. The credential store therefore keys on (customer, product, tenant host) and must also record which services are activated, because two tenants of the same product will not expose the same set.

Objects we can read

Unless stated otherwise the endpoints below are S/4HANA Cloud A2X OData v2 services, relative to https://<tenant>-api.s4hana.cloud.sap/sap/opu/odata/sap/. OData v2 collection responses are wrapped as {"d": {"results": [...]}} and each entity carries a __metadata object. Dates in OData v2 JSON come back as /Date(1477267200000)/ epoch milliseconds, which every SAP client has to normalise.

Orders

GET API_SALES_ORDER_SRV/A_SalesOrder

Query parameters: $top, $skip, $filter, $select, $expand, $orderby, $inlinecount=allpages (which populates d.__count). Incremental sync should filter on LastChangeDateTime or LastChangeDate. The service exposes 101 paths including the child collections to_Item, to_Partner, to_PricingElement, to_Text, to_ScheduleLine and to_BillingPlan.

{
  "d": {
    "__count": "1842",
    "results": [
      {
        "__metadata": {
          "uri": "https://mytenant-api.s4hana.cloud.sap/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder('3016329')",
          "type": "API_SALES_ORDER_SRV.A_SalesOrderType"
        },
        "SalesOrder": "3016329",
        "SalesOrderType": "OR",
        "SalesOrganization": "1710",
        "DistributionChannel": "10",
        "OrganizationDivision": "00",
        "SoldToParty": "1000021",
        "CreationDate": "/Date(1757894400000)/",
        "LastChangeDateTime": "/Date(1758024131000)/",
          "SalesOrderDate": "/Date(1757894400000)/",
        "RequestedDeliveryDate": "/Date(1758240000000)/",
        "TransactionCurrency": "INR",
        "TotalNetAmount": "12499.00",
        "OverallSDProcessStatus": "A",
        "OverallDeliveryStatus": "A",
        "OverallOrdReltdBillgStatus": "A",
        "OverallSDDocumentRejectionSts": "A",
          "PaymentMethod": "",
        "ShippingCondition": "01",
        "IncotermsClassification": "EXW",
        "DeliveryBlockReason": ""
      }
    ]
  }
}

Status fields are single character SAP codes, not words. The common values on OverallSDProcessStatus, OverallDeliveryStatus and OverallOrdReltdBillgStatus are A (not processed), B (partially processed) and C (completely processed). Map those onto the unified status rather than trying to read them literally.

Business One equivalent: GET /b1s/v1/Orders, key DocEntry, human visible number DocNum, customer CardCode, totals DocTotal and VatSum, status DocumentStatus (bost_Open / bost_Close), lines under DocumentLines.

Order items

GET API_SALES_ORDER_SRV/A_SalesOrderItem or GET API_SALES_ORDER_SRV/A_SalesOrder('3016329')/to_Item

{
  "d": {
    "results": [
      {
        "SalesOrder": "3016329",
        "SalesOrderItem": "10",
        "SalesOrderItemCategory": "TAN",
        "SalesOrderItemText": "Cold Pressed Groundnut Oil 1L",
        "Material": "FG-OIL-GN-1000",
        "RequestedQuantity": "24.000",
        "RequestedQuantityUnit": "EA",
        "ConfdDelivQtyInOrderQtyUnit": "24.000",
        "TransactionCurrency": "INR",
        "NetAmount": "10592.00",
        "TaxAmount": "1907.00",
        "ProductionPlant": "1710",
        "StorageLocation": "171A",
        "ShippingPoint": "1710",
        "SDProcessStatus": "A",
        "DeliveryStatus": "A",
        "OrderRelatedBillingStatus": "A",
        "SalesDocumentRjcnReason": ""
      }
    ]
  }
}

The item has no unit price field. Unit price is derived from NetAmount / RequestedQuantity, or read properly from to_PricingElement (A_SalesOrderItemPrElement), where each condition type carries ConditionRateValue, ConditionCurrency and ConditionAmount.

Business One equivalent: the DocumentLines array inside an Orders entity, with LineNum, ItemCode, ItemDescription, Quantity, Price, PriceAfterVAT, DiscountPercent, WarehouseCode, TaxCode, VatGroup.

Products and listings

GET API_PRODUCT_SRV/A_Product, with child entity sets to_Description, to_Plant, to_ProductSales, to_SalesDelivery, to_ProductUnitsOfMeasure, to_ProductStorage, to_Valuation.

Sample response not published in a form we could verify. These are the header field names taken from the service's own CDS/EDMX metadata: Product (the material number, the key), ProductType (material type, for example FERT finished goods and HAWA trading goods), BaseUnit, ProductGroup, ProductHierarchy, Division, Brand, GrossWeight, NetWeight, WeightUnit, MaterialVolume, VolumeUnit, ProductStandardID (the GTIN or EAN), InternationalArticleNumberCat, CountryOfOrigin, IsBatchManagementRequired, CrossPlantStatus (blocking status across plants), IsMarkedForDeletion (treat as inactive), CreationDate, LastChangeDateTime (the incremental sync key).

Product descriptions per language live on A_ProductDescription (Product, Language, ProductDescription). Sales view data (item category group, delivering plant, tax classification) lives on A_ProductSales and A_ProductSalesDelivery. Storage location assignment lives on A_ProductStorageLocation, keyed by Product, Plant, StorageLocation.

SAP has no concept of a marketplace listing. There is no ASIN, no channel listing id, no channel price. The unified listings table can only be populated from SAP for sku, list_price (from a pricing condition record) and stock; channel_listing_id and url have no SAP source.

Business One equivalent: GET /b1s/v1/Items, key ItemCode, with ItemName, ForeignName, BarCode, ItemsGroupCode, Manufacturer, SalesVATGroup, QuantityOnStock, QuantityOrderedByCustomers, ManageBatchNumbers, ManageSerialNumbers, Valid, Frozen, and a per-warehouse child collection ItemWarehouseInfoCollection.

Inventory

GET API_MATERIAL_STOCK_SRV/A_MaterialStock?$expand=to_MatlStkInAcctMod

This service is read only. It has exactly two collections, A_MaterialStock and A_MaterialSerialNumber. The header carries only Material and MaterialBaseUnit; the actual quantities live on the expanded to_MatlStkInAcctMod (material stock in account modification), one row per combination of plant, storage location, batch, stock type and special stock type.

{
  "d": {
    "results": [
      {
        "Material": "FG-OIL-GN-1000",
        "MaterialBaseUnit": "EA",
        "to_MatlStkInAcctMod": {
          "results": [
            {
              "Material": "FG-OIL-GN-1000",
              "Plant": "1710",
              "StorageLocation": "171A",
              "InventoryStockType": "01",
              "InventorySpecialStockType": "",
              "MaterialBaseUnit": "EA",
              "MatlWrhsStkQtyInMatlBaseUnit": "1482.000"
            }
          ]
        }
      }
    ]
  }
}

InventoryStockType distinguishes the stock buckets: 01 unrestricted use, 02 quality inspection, 03 blocked. Only 01 should feed quantity_available. Note that this service gives physical stock, not available to promise. There is no reserved quantity on this service; reservations are a separate object (API_RESERVATION_DOCUMENT_SRV).

Business One equivalent: the ItemWarehouseInfoCollection on an item, where each row has WarehouseCode, InStock, Committed, Ordered, MinimalStock, MaximalStock. InStock minus Committed is the usual available figure.

Shipments and tracking

GET API_OUTBOUND_DELIVERY_SRV;v=0002/A_OutbDeliveryHeader and /A_OutbDeliveryItem.

Note the version segment in the path: the service URL is .../sap/opu/odata/sap/API_OUTBOUND_DELIVERY_SRV;v=0002.

Useful header fields: DeliveryDocument (the key), DeliveryDocumentType, DeliveryDate, ActualGoodsMovementDate, ActualGoodsMovementTime, BillOfLading, MeansOfTransport, MeansOfTransportType, ActualDeliveryRoute, HeaderGrossWeight, HeaderNetWeight, HeaderWeightUnit, HeaderVolume, LoadingDate, OverallGoodsMovementStatus, OverallPickingStatus, OverallSDProcessStatus, OverallDelivReltdBillgStatus, OverallProofOfDeliveryStatus, CreationDate, LastChangeDate.

Useful item fields: DeliveryDocument, DeliveryDocumentItem, Material, Batch, ActualDeliveryQuantity, ActualDeliveredQtyInBaseUnit, DeliveryQuantityUnit, BaseUnit, ItemGrossWeight, GoodsMovementStatus, GoodsMovementType, IsCompletelyDelivered, InternationalArticleNumber.

Sample response not published in a form we could verify; the field lists above come from the service's own OpenAPI specification.

There is no carrier tracking number field in the standard outbound delivery API. BillOfLading and DeliveryDocumentBySupplier are the closest standard fields, and most customers put the AWB in a custom field or in a text element (A_OutbDeliveryHeaderText). Expect to ask the customer which field holds the AWB, and expect the answer to be a Z extension field. This is the single biggest gap when using SAP as a shipment source.

Delivery actions are exposed as OData function imports on the same service, for example POST /PostGoodsIssue, POST /ReverseGoodsIssue, POST /PickAllItems, POST /ConfirmPickingAllItems, POST /SetPickingQuantityWithBaseQuantity, POST /AssignHandlingUnitToDelivery.

Returns and cancellations

Two separate objects, and both matter. Physical returns come back through API_CUSTOMER_RETURNS_DELIVERY_SRV;v=0002, the customer returns delivery service. Financial returns are credit memos, which are billing documents: GET API_BILLING_DOCUMENT_SRV/A_BillingDocument?$filter=BillingDocumentType eq 'G2' selects credit memo requests, the BillingDocumentCategory and SDDocumentCategory fields distinguish invoice from credit memo, and BillingDocumentIsCancelled plus CancelledBillingDocument identify cancellations.

Business One equivalent: GET /b1s/v1/Returns (goods returned against a delivery), GET /b1s/v1/ReturnRequest, and GET /b1s/v1/CreditNotes for the financial credit. Each supports POST /CreditNotes({docEntry})/Cancel and POST /CreditNotes({docEntry})/CreateCancellationDocument.

Payments and settlements

SAP is the authority for the invoice, not for a marketplace settlement. GET API_BILLING_DOCUMENT_SRV/A_BillingDocument with $expand=to_Item,to_PricingElement.

Header fields include BillingDocument, BillingDocumentType, BillingDocumentCategory, SDDocumentCategory, BillingDocumentDate, CreationDate, LastChangeDateTime, SoldToParty, PayerParty, CompanyCode, FiscalYear, AccountingDocument, TransactionCurrency, TotalNetAmount, TaxAmount, TotalGrossAmount, CustomerPaymentTerms, PaymentMethod, PaymentReference, AccountingPostingStatus, AccountingTransferStatus, InvoiceClearingStatus, OverallBillingStatus, BillingDocumentIsCancelled, CancelledBillingDocument, PurchaseOrderByCustomer.

Item fields include BillingDocument, BillingDocumentItem, Material, Batch, Plant, StorageLocation, BillingQuantity, BillingQuantityUnit, BillingQuantityInBaseUnit, BaseUnit, NetAmount, GrossAmount, TaxAmount, CostAmount, TransactionCurrency, SalesDocument, SalesDocumentItem, ReferenceSDDocument, ReferenceSDDocumentItem, MaterialGroup, ProfitCenter.

Sample response not published in a form we could verify; the field list is from the service metadata.

SalesDocument and SalesDocumentItem on the billing item are the join back to the sales order, which is what lets you reconcile invoice to order.

Business One equivalent: GET /b1s/v1/Invoices (A/R invoices), GET /b1s/v1/DownPayments, GET /b1s/v1/SalesTaxInvoices, and the incoming payments entity in the banking service.

Customers

GET API_BUSINESS_PARTNER/A_BusinessPartner, with A_Customer, A_Supplier, A_BusinessPartnerAddress and A_CustomerSalesArea as related sets. In S/4HANA the customer master and the vendor master are both business partners; BusinessPartnerCategory is 1 for a person and 2 for an organisation, and the Customer field is populated only when the business partner has a customer role.

{
  "d": {
    "results": [
      {
        "BusinessPartner": "1000021",
        "Customer": "1000021",
        "Supplier": "",
        "BusinessPartnerCategory": "2",
        "BusinessPartnerFullName": "Bikes Pro Inc.",
        "BusinessPartnerGrouping": "BP02",
        "BusinessPartnerUUID": "00163e19-8846-1ed6-a6ba-a9305dba1b20",
        "OrganizationBPName1": "Bikes Pro Inc.",
        "CreationDate": "/Date(1477267200000)/",
        "CreationTime": "PT10H33M45S",
        "LastChangeDate": null,
        "BusinessPartnerIsBlocked": false,
        "BusinessPartnerIDByExtSystem": "",
        "ETag": "CB998000017220161024103345"
      }
    ]
  }
}

All name, address, email and phone fields here are PII. Email and phone are not on the header entity; they live on the address entity and its to_EmailAddress and to_PhoneNumber children. SAP does not mask them, so the connector must.

Business One equivalent: GET /b1s/v1/BusinessPartners, key CardCode, with CardName, CardType (cCustomer / cSupplier), Currency, PayTermsGrpCode, and a BPAddresses child collection.

Locations

SAP has a three level location model and all three matter for stock.

  • Plant: GET API_PLANT_SRV/A_Plant (on some releases plant data is only reachable through A_ProductPlant), the closest thing to a warehouse or fulfilment centre.
  • Storage location: on API_PRODUCT_SRV/A_ProductStorageLocation, keyed by Product, Plant, StorageLocation, and present on every stock and movement record.
  • Warehouse (EWM): only if the customer runs Extended Warehouse Management, which has its own services.

Business One equivalent: GET /b1s/v1/Warehouses, plus GET /b1s/v1/BinLocations if bin management is on.

Writing back: listings, price and stock

SAP does not hold listings, so "writing back a listing" means three separate things: pushing a sales order in, pushing an invoice or credit in, and adjusting stock.

Sales orders in

POST API_SALES_ORDER_SRV/A_SalesOrder accepts a deep insert: the header plus at least one child collection in the same request body. The specification explicitly supports deep insert of header partner, header partner address, header pricing element, header billing plan, header text, payment plan, related object, and items with their own partners, pricing elements, texts and schedule lines.

POST /sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder HTTP/1.1
Host: mytenant-api.s4hana.cloud.sap
Authorization: Basic <base64>
Content-Type: application/json
x-csrf-token: bJ7pQ1w0Zr9m5KcQ4bnvGA==
Cookie: SAP_SESSIONID_...

{
  "SalesOrderType": "OR",
  "SalesOrganization": "1710",
  "DistributionChannel": "10",
  "OrganizationDivision": "00",
  "SoldToParty": "1000021",
  "PurchaseOrderByCustomer": "D2C-98217",
  "RequestedDeliveryDate": "/Date(1758240000000)/",
  "TransactionCurrency": "INR",
  "to_Item": [
    {
      "SalesOrderItem": "10",
      "Material": "FG-OIL-GN-1000",
      "RequestedQuantity": "24",
      "RequestedQuantityUnit": "EA",
      "ProductionPlant": "1710"
    }
  ]
}

A 201 returns the created A_SalesOrder entity including the assigned SalesOrder number. Updates use PATCH A_SalesOrder('3016329'), item updates PATCH A_SalesOrderItem(SalesOrder='3016329',SalesOrderItem='10'), and a soft cancel is normally done by setting SalesDocumentRjcnReason on the items rather than by DELETE.

To price-check or availability-check an order before committing it, API_SALES_ORDER_SIMULATION_SRV runs the same determination without creating a document.

Invoices in

Billing documents are normally created by the billing run from a delivery or an order, not by an inbound API call. API_BILLING_DOCUMENT_SRV in S/4HANA Cloud is predominantly read, with cancellation actions. If the requirement is to push an externally raised invoice into SAP, the supported route is a finance posting through API_JOURNALENTRYITEMBASIC_SRV or the journal entry bulk API, not the billing document API. Confirm this per tenant; it is one of the areas where cloud and on premise diverge most.

Business One is the opposite: POST /b1s/v1/Invoices creates an A/R invoice directly, and POST /b1s/v1/CreditNotes creates a credit note, both taking the same Document shape as Orders with a DocumentLines array.

Stock adjustments in

POST API_MATERIAL_DOCUMENT_SRV/A_MaterialDocumentHeader with a deep insert of to_MaterialDocumentItem posts a goods movement, which is how stock changes in SAP. You never set a quantity; you post a delta with a movement type.

POST /sap/opu/odata/sap/API_MATERIAL_DOCUMENT_SRV/A_MaterialDocumentHeader HTTP/1.1
Content-Type: application/json
x-csrf-token: <token>

{
  "GoodsMovementCode": "03",
  "to_MaterialDocumentItem": [
    {
      "Material": "FG-OIL-GN-1000",
      "Plant": "1710",
      "StorageLocation": "171A",
      "GoodsMovementType": "561",
      "EntryUnit": "EA",
      "QuantityInEntryUnit": "12"
    }
  ]
}

Movement type drives the semantics: 561 and 562 are initial stock entry and its reversal, 551 is scrapping, 311 is a storage location to storage location transfer, 601 is a goods issue for delivery. The customer's SAP team must confirm which movement types and which GoodsMovementCode your communication user is allowed to post.

Reversal is a function import on the same service: POST /Cancel for the whole document, POST /CancelItem for one line.

Business One equivalent: POST /b1s/v1/InventoryGenEntries (goods receipt), POST /b1s/v1/InventoryGenExits (goods issue), POST /b1s/v1/StockTransfers (warehouse to warehouse, with FromWarehouse, ToWarehouse and a StockTransferLines array), and POST /b1s/v1/InventoryPostings for a counted correction.

Price

Sales prices in S/4HANA are condition records, not a field on the material. API_SLSPRICINGCONDITIONRECORD_SRV reads and writes them: a record is keyed by condition type, condition table and the key combination (for example customer plus material, or price list plus material), and carries validity dates, ConditionRateValue and ConditionRateValueUnit. Writing a price therefore means creating a new condition record with a validity period, not updating an existing amount. Business One is simpler: PATCH /b1s/v1/Items({itemCode}) on the item price list entries, or POST /b1s/v1/SpecialPrices for a customer specific price.

Batching

Both S/4HANA A2X services and the Business One Service Layer expose POST $batch with multipart/mixed bodies. On S/4HANA this is the only way to get many writes into one transaction; each change set is atomic. Business One additionally supports Prefer: odata.continue-on-error.

Webhooks and notifications

S/4HANA business events

S/4HANA Cloud publishes business events to SAP Event Mesh, formatted as CloudEvents 1.0. Subscription is by creating a queue in Event Mesh and binding a topic, then either letting Event Mesh push to your HTTPS endpoint (webhook subscription) or pulling from the queue over AMQP or MQTT. The customer configures which events their tenant emits in the Enterprise Event Enablement apps.

Event type names follow sap.s4.beh.<object>.v1.<Object>.<Verb>.v1. Verified examples from SAP's own documentation:

{
  "type": "sap.s4.beh.salesorder.v1.SalesOrder.Created.v1",
  "specversion": "1.0",
  "source": "/default/sap.s4.beh/ER9CLNT001",
  "id": "0894ef45-7741-1eea-b7be-ce30f48e9a1d",
  "time": "2020-08-14T06:21:52Z",
  "datacontenttype": "application/json",
  "data": {
    "SalesOrder": "3016329"
  }
}

The critical property of SAP business events is that the request body is a key only notification. It tells you a sales order changed and gives you the sales order number; it does not give you the order. Every event must be followed by a read of the corresponding OData entity. Size the read budget accordingly.

Delivery is at least once with retry handled by Event Mesh queues, so consumers must be idempotent on the event id.

Business One

There are no webhooks and no event stream on the Service Layer. Poll. The practical pattern is a filter on UpdateDate plus UpdateTime, or on the monotonic DocEntry for creates:

GET /b1s/v1/Orders?$filter=UpdateDate ge '2026-09-21'&$orderby=DocEntry&$top=100

A 10 to 15 minute interval is reasonable for orders and invoices, hourly for the item master, and 5 minutes for stock if the customer's volumes allow it. Watch the licensed session count while doing this.

ByDesign

No push mechanism on the custom OData services. Poll on the LastChangeDateTime style field of each service, or use ByDesign's own web service outbound calls if the customer has configured them.

Rate limits and pagination

SAP does not publish a numeric request rate limit for S/4HANA Cloud OData services. The limit that actually bites is the tenant's ABAP work process pool: too many concurrent expensive queries and the gateway returns 503 or the request times out at the customer's configured value. Treat 4 to 8 concurrent requests per tenant as the safe default and negotiate upward with the customer's basis team.

Practical cadence for a full connector against one S/4HANA tenant: sales orders and deliveries every 10 minutes on LastChangeDateTime, billing documents every 30 minutes, material stock every 15 minutes filtered to the plants that matter, product and business partner master data nightly with a LastChangeDate window. Always filter with $select to the fields you actually map; these entities have 90 to 160 columns and the unfiltered read is many times more expensive on the ABAP side.

The sandbox at sandbox.api.sap.com is rate limited per API key and is shared, so it will throttle far sooner than a real tenant. Its exact limit is not published.

Mapping to the unified model

orders

order_items

products

listings

Only partially mappable. sku comes from Product, price, list_price and sale_price from validity dated condition records on API_SLSPRICINGCONDITIONRECORD_SRV, stock from A_MaterialStock, status approximated from CrossPlantStatus and IsMarkedForDeletion (a blocking status, not a listing status), updated_at from LastChangeDateTime. listing_id, channel_listing_id and url have no SAP source at all, because SAP is not a sales channel.

inventory

shipments

returns, settlements, customers, locations

Gaps and open questions

  • api.sap.com and help.sap.com could not be read by our fetcher on 2026-09-21. The authoritative per-field documentation, the deprecation calendar and SAP's own wording on limits therefore remain unverified. Re-check them from a browser before implementation.
  • No published numeric rate limit for S/4HANA Cloud OData. The practical ceiling has to be agreed with each customer's basis team.
  • A_MaterialStock has no change timestamp, so incremental stock sync is not possible on that service. Either read full stock per plant on a schedule, or subscribe to material document events and apply deltas. Which of those a given tenant supports needs confirming.
  • Carrier tracking numbers have no standard home in the outbound delivery API. Every customer will have put the AWB somewhere different.
  • Whether a tenant can accept an externally raised invoice, and by which service, differs between public cloud, private cloud and on premise. Unresolved here.
  • Business One Service Layer v2 speaks OData v4 with different response envelopes and different paging. Which version a customer runs must be established up front; the samples on this page are v1.
  • The Business One licensed session ceiling is a real constraint and the number is per installation. Ask before designing the poller.
  • ByDesign coverage here is thin. Its custom OData services are defined per tenant, so there is no fixed schema to document. The SAP-samples Postman collections are the best starting point.

Sources