Skip to content

Magento SAP Integration: How Orders Actually Reach SAP

EST. 2008800+ BUILDSHYVÄ PARTNER

indexation: WAIVER unset

Magento SAP integration is three separate jobs sharing one name. An order bound for SAP Business One goes in through the Service Layer, which is OData v4 and session-based. An order bound for SAP ECC goes in as an IDoc, or through a BAPI over RFC. An order bound for S/4HANA goes in as OData through SAP Gateway, or through a flow you build in SAP Integration Suite. Different protocols, different object names, different ways of telling you it failed. So the first thing to settle is which SAP sits on the other end. Business One gets the most room below, because that is what most Magento stores mean when they say SAP.

Which SAP are you integrating with: Business One, ECC, or S/4HANA?

Three products, not three editions of one.

WHAT YOU BUILD AGAINSTWHO RUNS IT
SAP BUSINESS ONEService Layer (OData v4), or the older DI APISmall and mid-market merchants
SAP ECCIDoc, or BAPI over RFCThe legacy estate, already wired for ALE document exchange
SAP S/4HANAOData through SAP Gateway, or SAP Integration SuiteCurrent releases, often with Cloud Integration in the middle

Failure looks different on each one, and that is the part that costs you. An IDoc parked at status 51 on ECC and a 401 from a Business One session that quietly aged out are the same event seen from the storefront: an order the shopper completed and SAP never recorded. They share no diagnostic step.

So if a connector, an agency or a page offers to integrate Magento with SAP without asking which SAP you run, it has not started work yet. The Business One dedupe field does not exist on ECC. IDoc status codes do not exist on Business One. One price covering all three is a price for whichever one the vendor has actually built.

If the system on the other end is not SAP, the argument keeps its shape and loses every object name in it: Magento NetSuite integration and Magento ERP integration cover those. Two SAP products come up in these conversations and are not this page. SAP Business ByDesign is a fourth surface of its own. SAP Commerce Cloud is a storefront, not somewhere you post orders.

How does data physically get into SAP?

Settle what SAP actually accepts before anybody says the word sync.

SAP Business One: the Service Layer, or the DI API. The Service Layer is a REST service speaking OData v4 over HTTPS, and every request hangs off a versioned base path: /b1s/v2/ for v4, /b1s/v1/ for the older endpoint. POST a username, a password and a company database name to /b1s/v2/Login, take the session cookie it hands back, and send that cookie on everything afterward until it expires. The resources are Business One objects under their own names: /b1s/v2/Orders for Sales Orders, /b1s/v2/BusinessPartners for customers, /b1s/v2/Items for item master data, /b1s/v2/Invoices, /b1s/v2/DeliveryNotes. A Sales Order is a document header plus a DocumentLines collection, and each line carries an ItemCode, which is your SKU if the two catalogs were ever kept aligned.

The DI API is the older path: a COM layer running on Windows against the Business One client libraries. Plenty of existing add-ons sit on it. For a web integration it is the wrong shape, because it is not HTTP, it needs a Windows host with those libraries installed, and it holds a connection to the company database while it works. If your version exposes the Service Layer, use the Service Layer.

SAP ECC: IDoc, or BAPI over RFC. IDoc is the format ECC already uses to exchange documents with everything else it talks to. An order arrives as basic type ORDERS05 under message type ORDERS, routed by ALE and posted asynchronously by an inbound function module. BAPI over RFC is the synchronous option: BAPI_SALESORDER_CREATEFROMDAT2 is the call that creates the order, and it answers in the same breath. An answer is not the same as a posted document. The BAPI hands back a RETURN table, and a message of type E in that table means nothing was created however healthy the call looked. Nothing commits until you call BAPI_TRANSACTION_COMMIT either, so a caller that skips the commit and drops the connection has done precisely nothing. Most ECC estates take orders as IDocs, because the ports, partner profiles and error workflow already exist. BAPI is tempting because it answers immediately, which is also how it makes checkout wait on a free ECC work process.

SAP S/4HANA: OData through SAP Gateway, or Integration Suite. S/4HANA publishes OData services through SAP Gateway and still accepts IDocs, because the ECC-era plumbing survived the upgrade. Writing at Gateway directly means a service path shaped like /sap/opu/odata/sap/<SERVICE_NAME>/ for an OData V2 service, or one under /sap/opu/odata4/ for the newer V4 ones, so check which of the two you were handed before you copy a path shape from anywhere. A write also needs a token: GET first to collect an X-CSRF-Token, then replay it on the POST. Skip that and every write comes back 403 while every read keeps working perfectly. The managed path is SAP Integration Suite, whose Cloud Integration capability is the part people still call CPI, and it is where a lot of S/4HANA shops keep the mapping, the retries and the monitoring instead of writing them. If you are on S/4HANA, the Business One material below does not transfer. Different product, different ingress, different objects.

The far end also picks your idempotency key for you, before you have written a line of code.

What are the three ways to connect Magento to SAP?

Flat file, scheduled poll, event push. For orders the answer is event push. The other two both have a job, and it is not orders.

LATENCYRIGHT ANSWER FORFAILURE MODE
FLAT-FILE DROPNext scheduled pickupOvernight catalog and price feedsA pickup that silently did not happen
SCHEDULED API POLLThe poll interval, by definitionStores that cannot run a receiverEmpty requests the store still pays for
EVENT PUSHAs fast as your receiverOrdersAn endpoint you now have to keep up, answer fast, and make idempotent
Panel A - scheduled poll

[ middleware ]  --(1) cron fires every N minutes-->  [ Magento REST API ]
                <--(2) "what changed since T?"------
                <--(3) 0 rows. 0 rows. 0 rows. 1 order.

   the store serves every one of those requests, including the empty ones
Panel B - event push

[ Magento ] --(1) sales_order_place_after fires--> [ message queue ]
            --(2) POST order payload------------>  [ your endpoint ]
                                                          |
                                        (3) answer 200 inside 10s, work async
                                                          |
                                                          v
                                    [ SAP Business One / Service Layer ]
                                       POST /b1s/v2/Orders
                                       Sales Order (oOrders)
                                       U_MagentoId = increment_id  <- dedupe key

   failed delivery --> 3 attempts, 1 min then 2 --> marked Failed in the log
   SAP down --------> checkout completes, your queue grows, nothing is lost

Two of those three are mechanisms and not products, so name them: on Business One the file goes in through the Data Transfer Workbench, and on ECC through a file port that feeds IDocs. Polling is worth defending in one situation the table cannot hold: a store whose middleware is already bought, and whose scheduler is where the ops team already looks. You buy nothing new, and the failure surfaces in a console that is already open on a screen. Orders go on the queue, and you pay for that with a receiver you own and operate. It is the right thing to spend the money on, because nothing else on that list gets an order into SAP while the shopper is still on the success page.

Orders are one direction. Stock levels and prices travel the other way, out of SAP and into Magento, and that traffic has a different shape: thousands of SKUs at once, overnight. Send it through Magento's asynchronous bulk endpoints under /async/bulk/V1/ instead of thousands of synchronous REST calls, and the update stops competing with shoppers for the same PHP processes. The general trade-off between asking and being told is worked through in webhooks vs API, and the rest of the surface sits on the Magento integration hub.

Which Magento records map to which SAP objects?

Business One first, because that is the variant most of these projects turn out to be.

SAP B1 OBJECTSERVICE LAYER RESOURCEKEY FIELD
ORDER (SALES_ORDER)Sales Order (oOrders)/b1s/v2/OrdersU_MagentoId = increment_id
ORDER ITEMDocument LineDocumentLinesItemCode = SKU
CUSTOMER (REGISTERED)Business Partner, type Customer/b1s/v2/BusinessPartnersCardCode, from customer_id
INVOICEA/R Invoice/b1s/v2/InvoicesBaseType, BaseEntry, BaseLine per line
SHIPMENTDelivery/b1s/v2/DeliveryNotesThe same three copy fields
CREDIT MEMOA/R Credit Memo, or Return then Credit Memo/b1s/v2/CreditNotes, /b1s/v2/ReturnsDepends on whether stock comes back
PRODUCTItem Master Data/b1s/v2/ItemsItemCode = SKU
STOCK SOURCE (MSI)Warehouse/b1s/v2/WarehousesWarehouseCode, one per MSI source
CUSTOMER GROUPPrice List/b1s/v2/PriceListsDrives customer-specific pricing
STORE VIEWSeries, or a user-defined fieldnoneNo native equivalent

Magento store views have no Business One counterpart, so the last row is a decision and not a lookup: a document series per store view, a user-defined field carrying the store code, or nothing at all and a reporting gap that surfaces at year end. Deciding it late is how a two-store rollout turns into a data cleanup.

The invoice row carries a trap. Business One will post a standalone A/R Invoice, so nothing stops your receiver creating one, and if it does, the Sales Order you posted an hour earlier stays open with stock still committed against it. To bill the order instead of inventing a second document, copy from it: on each DocumentLines entry set BaseType to the source object type, BaseEntry to the order's DocEntry, and BaseLine to the line you are billing. Omit those three and you get an invoice with no parent, a Sales Order nobody will ever close, and inventory that reads as committed until a person goes looking for why. Deliveries copy the same way.

Order of operations matters too, because /b1s/v2/Orders will not accept a Sales Order without a valid CardCode. The receiver runs like this:

  1. Authenticate at /b1s/v2/Login and hold the session cookie.
  2. Resolve the Business Partner for this buyer, and create one if it is missing.
  3. Query the Sales Orders for the Magento order number you are about to write.
  4. POST the Sales Order to /b1s/v2/Orders with its DocumentLines.
  5. Copy the A/R Invoice and the Delivery from that order, never from scratch.

Build it in another order and your first live order comes back rejected on a field nobody populated.

Guest checkout decides step 2 for you, so decide it first. A guest order carries customer_is_guest: 1 and no customer_id, so there is no Business Partner sitting there to resolve. Two answers work. If you sell B2B, match on customer_email and create a Business Partner the first time you see one, because those buyers come back and finance wants them on the ledger anyway. If you sell one-off consumer orders, post them all against a single one-time-customer CardCode and carry the real name and address on the document, which stops the Business Partner master growing by one record per checkout. The price of the second option is that Business One reporting on repeat guest buyers stops being possible. Pick one before go-live, or pick it at 2am when the first guest order fails at the POST.

ECC is a different transport, so it gets its own table.

IDOC BASIC TYPEMESSAGE TYPE
ORDERORDERS05ORDERS
CUSTOMERDEBMAS06DEBMAS
PRODUCT / MATERIALMATMAS05MATMAS
DELIVERYDESADV01DESADV
INVOICEINVOIC02INVOIC

One caveat on the Delivery row. DESADV01 is the original basic type for message type DESADV, and live ECC estates commonly run outbound delivery on a DELVRY basic type such as DELVRY03 against that same message type. Check WE20 on the actual system before you build against that row.

What does a Magento order payload actually look like?

This is our default Order Placed template, rendered. Real field names, an order invented for the page.

{
  "event": "sales_order_place_after",
  "triggered_at": "2026-07-30 09:41:52",
  "order": {
    "increment_id": "000000318",
    "grand_total": 1734.85,
    "currency": "USD",
    "customer_email": "procurement@example.com",
    "items": [
      {
        "sku": "OSC-DS1104Z",
        "name": "100 MHz 4-Channel Digital Oscilloscope",
        "qty": 2,
        "row_total": 1398.00
      },
      {
        "sku": "PRB-TA150-10X",
        "name": "10x Passive Probe, 150 MHz",
        "qty": 6,
        "row_total": 336.85
      }
    ]
  }
}

What is missing from that is the point of it. The default template emits a curated subset and not a serialized order: no entity_id, no state or status, no address blocks, no per-line tax percent. Anything your SAP mapping needs and cannot find there is a template edit rather than a missing feature. Templates are sandboxed Smarty held in the database, every event exposes the same {$meta.*} variables alongside its own, and the payload template documentation lists what each one puts on the table. One trap in there is worth reading twice: |json encodes a value together with its own quotes and brackets, while |escape_json strips the quotes so a scalar can sit inside quotes you wrote yourself. Reversing those two is the usual reason a template emits JSON that will not parse.

increment_id earns its position at the top of that order object. sales_order_place_after is dispatched by Magento\Sales\Model\Order::place(), which fires the event and returns without persisting. At the moment of dispatch entity_id is unassigned and so is every per-item item_id. increment_id is already populated, because the sequence hands it out during quote-to-order conversion. Key an observer off entity_id and you key off null. Key off increment_id and you are fine, which is also why increment_id is the value that ends up on the SAP document.

Two things sit outside that body and both decide whether the receiver should believe a byte of it. Neither is a JSON field. Both arrive on the request as headers.

WHAT IT CARRIES
X-WEBHOOK-SIGNATUREThe literal sha256=, then a hex HMAC-SHA256 taken over the raw request body. Configure no secret and the header is not sent at all.
X-WEBHOOK-DELIVERY-IDA UUID. The first send and every retry of that same send carry the identical value.
X-WEBHOOK-EVENTThe event that fired
X-WEBHOOK-IDThe webhook that fired it
X-WEBHOOK-ATTEMPTWhich attempt this is

Verify before you parse. Recompute the HMAC over the exact bytes that arrived, then compare with PHP's hash_equals() and never with ==, because == returns the moment two bytes differ and the time it took to return tells a forger how much of the signature they already had right. Only once that comparison passes does the delivery id mean anything, so run it in that order: verify, then ask whether you have seen this UUID before. The rest of that path is in the module's security documentation.

The two keys catch different failures, so a working receiver keeps both. X-Webhook-Delivery-Id catches one delivery landing on your endpoint twice. The SAP-side key catches the same order arriving as a genuinely new delivery, which is what a replayed batch or a reprocessed day looks like.

po_number is not in the default template, and if you sell B2B it is the first field you will add to it. It belongs in NumAtCard on the Business One document, which is the standard field for the customer's own reference number. That is exactly why NumAtCard is not where your Magento order number goes: on a B2B order those are two different values wanting one field, and whichever one loses gets typed in by hand later. Send the PO as null and the document waits for that same person, which is the manual step the integration was bought to delete.

What breaks in production, and what does the customer see?

The shopper sees almost none of this. That is what makes it expensive: the storefront stays correct while SAP stays empty.

WHAT THE CUSTOMER SEESWHAT THE INTEGRATION HAS TO DO
SAP UNREACHABLE DURING CHECKOUTNothing. The order completesQueue the event, return the shopper to the success page, deliver out of band
A DELIVERY FAILS OR TIMES OUTNothingRetry a 5xx and a 429, refuse the rest, and reconcile whatever is still Failed after the last attempt
THE SAME DELIVERY ARRIVES TWICENothing. Two Sales Orders now exist in Business OneQuery for the Magento order number before creating a document
AN ORDERS05 POSTS NOTHING AND PARKS AT 51A confirmed order that does not exist in SAPWatch IDoc status, subscribe to the ALE error workflow, or reconcile on a schedule
THE SERVICE LAYER SESSION EXPIRES MID-BATCHHalf the catalog on last week's pricesRenew on the 401 and resume from the last record that committed
THE QUEUE CONSUMER EXITSNothing at all, for daysSupervise the process, alert on queue depth, reconcile order counts

SAP is down mid-checkout. The customer sees nothing. That is the whole answer, and it is a design requirement: order placement must not depend on SAP answering. If checkout calls the Service Layer inline and waits, a Business One maintenance window becomes a storefront outage and you learn about it on a Saturday. Put the event on a queue, return the shopper to the success page, and let delivery fail as often as it needs to while the Service Layer is dark.

Then comes the hour after SAP is back, when everything the store took during the outage arrives at once. The questions there are whether the consumer survived the backlog, whether ordering still holds, and whether your check-before-post survives the same order presented twice inside one second.

Retries. Retry a 5xx. That class says the far end fell over holding a request that may be perfectly valid, so the same bytes ten seconds later can go through untouched. Do not retry most 4xx: the message was read and objected to, and an identical message earns an identical objection.

A 429 is the carve-out, and the expensive one to get wrong. A 429 says a rate limit was hit, not that the payload is wrong. On this path the 429 almost always comes from your own side of the wire: your receiver, or the gateway or middleware sitting in front of SAP. It fires during exactly the traffic spike the integration exists to survive. Widen the interval and send the same message again. Treat a 429 as terminal and the orders you throw away are the ones from your busiest hour of the year.

Retrying has to end somewhere, and ours ends fast: the last attempt goes out about three minutes after the first. Against a SAP outage that is nothing, and it is supposed to be nothing. An IDoc at 51 will not clear itself, and a Service Layer session past its lifetime refuses the hundredth attempt exactly as it refused the second unless something in between goes and fetches a new cookie. Neither one is waiting on more patience from us. The attempt budget covers a receiver that blinked, not SAP being dark, which is the second reason your endpoint should answer 200 quickly and hold the work itself. Your endpoint can sit out a maintenance window. Three attempts cannot.

Ten seconds is the whole budget for one attempt. Go past it and the module drops the connection, counts that attempt spent, and leaves you two. On a SAP path that ceiling sits closer than it reads, because reaching Business One at all costs two round trips before a document exists: POST to /b1s/v2/Login for a cookie, then POST to /b1s/v2/Orders. Resolve the Business Partner and run the dedupe query inside the same request and you are at four, against a system that answers when it answers. Do all of that before you reply and the failure takes an ugly shape: SAP was healthy, the order was valid, and the attempt died on a clock. Take the delivery, write it down, answer, and go to SAP afterward. The only work left inside the ten seconds is a write to storage you own.

The schedule is short enough to print. Three attempts including the original send, spaced by retry_base_delay * 2^(failed_attempt - 1) off a base that ships at 60 seconds:

GOES OUTIF IT FAILS
1when the event fireswait 1 minute
21 minute after attempt 1wait 2 minutes
32 minutes after attempt 2nothing further is sent

A retry cron runs every minute and republishes whatever has come due, so those waits are floors and not stopwatches. The timeout, the base and the attempt limit are all configurable at Stores > Configuration > OCM Labs > Webhooks > Delivery, so a receiver that needs a longer leash can have one. Template render errors never get an attempt at all, which is the right call: a template that will not render is broken identically on every pass, so spending the budget on it buys nothing.

The third failure is terminal. In the Delivery Log the row goes to Failed and stays there, with no dead-letter queue behind it, no alert, and no replay button. Delivery is at-least-once and it is not exactly-once, so counting orders in Magento against Sales Orders in Business One stays the thing that finds what fell through, on our side of the wire exactly as on SAP's. The module's delivery documentation carries the attempt log and the configuration fields.

Duplicate delivery. At-least-once delivery means the same order arrives twice, and not rarely. Any timeout where your endpoint did the work but the acknowledgment went missing produces a second attempt byte-identical to the first. The Service Layer will cheerfully create a second Sales Order for it. Your receiver cannot tell new from already-done unless you hand it a key, which is the next section.

The IDoc that posts nothing and tells nobody. IDoc processing is asynchronous. You hand ECC an ORDERS05 and ECC takes it. Status 53 means posted. Status 51 means application error, and the IDoc stops there: nothing calls Magento back, the storefront shows a completed order, the customer has a confirmation email, and in SAP that order does not exist. A 51 is not frozen, and that is worth knowing before you design around it. It is reprocessable, either by hand in BD87 or by a scheduled reprocessing job, and WE02 is where you go to see the one you are chasing. What a 51 will never do is announce itself to the Magento side. So pick one: watch IDoc status, subscribe to the ALE error workflow, or run a reconciliation job comparing Magento increment IDs against what actually posted. Doing none of the three is the default, and finance finds it days later when the numbers do not tie.

The Service Layer session expires mid-batch. You authenticate, you hold a cookie, and the cookie has a lifetime. A long catalog or price push that never renews stops partway through, and because it arrives as a 401 instead of a crash it does not look like an outage. It looks like half your prices updated. A load-balanced Service Layer adds a second trap: it returns a routing cookie, ROUTEID, alongside the session cookie, B1SESSION, and both have to go back on every request. Keep B1SESSION, drop ROUTEID, and you get intermittent 401s that look random until you go through the cookie jar.

Nothing has reached SAP since Tuesday. bin/magento queue:consumers:start needs a supervisor, because a consumer that exits does it quietly. It reports no errors, since it is not attempting a delivery and has nothing to fail at. From the SAP end the picture is just as calm: the Service Layer answers, the inbound IDoc queue is clean, and nothing is arriving to be worked. The only signal is the gap between orders taken in Magento and Sales Orders that exist in Business One, which is why you alert on queue depth and reconcile those two counts on a schedule. An alert wired only to delivery failures stays quiet through the entire outage.

How do you stop SAP creating the same sales order twice?

Write the Magento increment_id onto the SAP document, check for it before you post, and make sure two workers can never hold the same order at the same moment. That third clause is the one most versions of this advice leave out, and without it the check is decoration.

WHERE THE MAGENTO ORDER NUMBER GOESHOW YOU CHECK FOR IT
SAP BUSINESS ONEU_MagentoId, a user-defined field on the Sales Order$filter on /b1s/v2/Orders
SAP ECCSegment E1EDK02, qualifier 001, field BELNR on the inbound ORDERS05Query the sales order on its customer purchase order number
SAP S/4HANAPurchaseOrderByCustomer on the sales order OData service API_SALES_ORDER_SRV$filter on that same service

On Business One, add the user-defined field to the Sales Order. U_MagentoId is the usual name. Before you POST, ask:

GET /b1s/v2/Orders?$filter=U_MagentoId eq '000000318'

Rows back means you have already done this: skip it, or update the document you found. No rows means POST. The user-defined field is the recommendation and not a workaround. The obvious standard alternative, NumAtCard, holds the customer's own reference number, and on a B2B order that is the buyer's purchase order rather than anything of yours. Put both values in it and one of them is gone. Sell only to consumers, with no PO number ever arriving, and NumAtCard is free for you to use. Otherwise leave it to the buyer and dedupe on U_MagentoId. What never works either way is DocNum, because that is Business One's own numbering and you do not control it.

One property of increment_id decides whether that check is enough. It is unique inside a store's own sequence and not across the install, so two store views can each produce 000000318 and mean two unrelated orders. Run a single store and this costs you nothing. Run several and you have two routes out, and the SAP end picks between them.

If each storefront lands in a SAP target of its own, a separate company database on Business One or its own company code on ECC, scope the subscriptions instead of untangling this downstream. A webhook can be restricted to named store views rather than left on all of them, and an event that fires outside a subscription's scope is never delivered to it. One subscription per storefront, each pointed at the receiver for its own SAP target, and the store is settled before the request leaves Magento.

If one receiver takes every storefront into one company database, leave the subscription on all stores and carry the store in the body: add {$meta.store_id} to the template, write it onto the document beside the order number, and filter on the pair. Either way it is the same decision the store view row in the mapping table above forces, arriving a second time.

That GET is a guard, not a lock. Nothing in the Service Layer reserves U_MagentoId between your query and your POST, so when the same order reaches two workers inside one second, both queries come back empty and both create a document. The race closes on your side, one of two ways.

Serialize per order. Run a single consumer on the order queue, or partition it so a given increment_id is only ever handled by one worker. If a key can only be in flight once, the query and the post behave as one step and the window is gone. This is the cheap answer and it is usually enough, because volume per key is one.

Or make the duplicate fail instead of succeed. Put a unique index on the Magento order number in your receiver's own datastore, written before the Service Layer call and not after it. The second worker's insert violates the constraint, so it never reaches /b1s/v2/Orders at all. Done that way, the Business One field is your audit trail and your recovery key, which is the right job for it. A user-defined field on a marketing document is not a constraint and will not behave like one.

The objection is fair: that is an extra round trip per order plus a lock on top of it. It is one GET against a system you are about to POST to anyway, plus an index on a table you already own, and the alternative is a duplicate Sales Order that gets reversed by hand after it has been picked, shipped and invoiced.

On ECC the message and the order are two identities, and conflating them is how ECC integrations end up with no key at all. The control record's DOCNUM numbers the IDoc, so two IDocs carrying one order get two DOCNUM values, and it can only ever identify traffic. Put the Magento order number where the document itself will carry it. On an inbound ORDERS05 that is segment E1EDK02 with qualifier 001, in field BELNR, which standard inbound processing maps to the customer purchase order number on the sales order header, BSTNK. It stays queryable there after posting, so your check runs against the order the same way it does on Business One.

S/4HANA lands the same idea on a different field name. The sales order OData service, API_SALES_ORDER_SRV, carries PurchaseOrderByCustomer for the buyer's own reference, so that is where increment_id goes and that is what you $filter on before you create anything. Same discipline as B1, different service, and worth confirming against the service metadata on your release rather than assuming the field is there.

Is a Magento SAP integration cheaper to build or to buy?

Buy it if you have no endpoint and nobody to run one. Build it if the order already has somewhere to land.

Start with the figure that gives the game away. One connector sold for this exact job bills about $3,000 as a separate line item just to install the SAP Business One Service Layer, roughly 1.5x its own connector price, and nowhere in that listing does it say what the Service Layer is. Three thousand dollars for a component the seller will not define. It is the interface this page describes under "How does data physically get into SAP?", and reading that costs nothing.

Subscriptions next. Connectors in this market publish monthly tiers instead of a single price, and one vendor publishes three: $99, $249 and $699 a month. That listing is for a different ERP, so read those as what a connector subscription costs, not as what a SAP connector costs.

The build side usually gets asserted instead of priced. One vendor does publish cost benchmarks by approach in its own ERP guide: $5,000 to $30,000 plus licensing for a pre-built connector, and $50,000 up to $250,000 and above for a custom API-led integration. Those figures are theirs. We are repeating them, not standing behind them.

COST AS PUBLISHEDYEAR ONETHREE YEARS
SUBSCRIPTION CONNECTOR, CHEAPEST PUBLISHED TIER$99 a month$1,188$3,564
SUBSCRIPTION CONNECTOR, TOP PUBLISHED TIER$699 a month$8,388$25,164
SERVICE LAYER INSTALLATION, BILLED AS ITS OWN LINE ITEMabout $3,000, one time$3,000$3,000
PRE-BUILT CONNECTOR, PUBLISHED BENCHMARK$5,000 to $30,000 plus licensing$5,000 to $30,000$5,000 to $30,000 plus three years of licensing
CUSTOM API-LED INTEGRATION, PUBLISHED BENCHMARK$50,000 up to $250,000 and above$50,000 and up$50,000 and up
OCM LABS MODULE, PLUS A RECEIVER YOU RUN$250, one timeno renewalno renewal

Our row is the narrow one, and it is narrow on purpose. The OCM Labs Magento 2 Webhooks module fires Magento events at an endpoint. The endpoint is usually something the business already has somewhere: a middleware box, a small service beside the store, or a function.

Now the catch, which is what makes this section an argument instead of a pitch. The module moves the transport and it does not write to SAP. Someone still builds the receiver, maps the fields in the table above, handles the session cookie or the IDoc status, and carries the pager when it breaks. The Business One license costs what it cost. The Service Layer still has to be stood up and kept running against the company database. On ECC or S/4HANA there is still ABAP to write, or a flow to build in Cloud Integration. No webhook has ever done any of those three things. If you have no developer and no endpoint, a monthly connector that includes the receiving side is the better buy, and you should buy it.

How OCM Labs does this

We build the Magento side, and we are direct about where our side stops.

The module fires on Magento order events and posts them out as they happen, so nothing polls the store on a timer and latency stops being a scheduling decision. What lands on your endpoint is the JSON above, signed, with a delivery id on the request. Everything after that is receiver work: authenticate, resolve the Business Partner, check the dedupe field, POST the Sales Order. On ECC it is building the ORDERS05 and then actually watching what happens to it. We write receivers too. The receiver is where the integration lives.

An electronics retailer runs on exactly that split: we fire the events, their receiver does the SAP side. Fulfillment at their end finishes in under an hour.

For orders it is push. The only real question is whether you can run and watch the receiving end. If you can, the module does the firing and the receiver stays yours. If you would rather we wrote both, tell us which SAP is on the other end and you get back the ingress path and the list of things the receiver has to survive, before you spend anything.

QUESTIONS MERCHANTS ASK

Magento SAP Integration: How Orders Actually Reach SAP FAQ.

Does Magento integrate with SAP?+
Yes, and not natively. Magento exposes REST and message-queue interfaces on its side; SAP exposes the Service Layer, the DI API, IDoc, BAPI over RFC or OData depending on which SAP you run. Something in the middle has to speak both, and that something is either a connector you buy or a receiver you write.
How do I connect Magento 2 to SAP Business One?+
Push the order out of Magento, then call the Business One Service Layer. Authenticate at /b1s/v2/Login and hold the session cookie. Resolve the customer at /b1s/v2/BusinessPartners first, creating one if it is not there, because /b1s/v2/Orders will not take a Sales Order without a valid CardCode. Check whether U_MagentoId already carries this order number, then POST to /b1s/v2/Orders with a DocumentLines array keyed on ItemCode. The DI API is the alternative on an older setup, or where Windows-hosted add-on code already exists.
What is the SAP Business One Service Layer?+
A REST service that exposes Business One objects as OData v4 resources over HTTPS, under a versioned base path: /b1s/v2/ for OData v4 and /b1s/v1/ for the older endpoint. You authenticate once, receive a session cookie, and then work against resources such as /b1s/v2/Orders, /b1s/v2/BusinessPartners and /b1s/v2/Items. It is not a separate product you buy from a third party, and knowing that changes how you read a quote to install it.
Do I need an IDoc to send orders to SAP?+
Only on ECC, or on S/4HANA where you are reusing the existing ECC-era interfaces. Business One has no IDoc: the equivalent is a POST to the Service Layer. If somebody proposes IDocs for a Business One project, they are describing a different system.
Is SAP Business One integration different from S/4HANA integration?+
Completely. Business One uses the Service Layer or the DI API against B1 objects like Sales Order and Business Partner. S/4HANA uses OData through SAP Gateway or SAP Integration Suite, with different services, a different data model and a CSRF token on every write. Auth, field mapping and error handling all get rebuilt, so a connector that claims both is two connectors in one box.
How do I stop duplicate sales orders in SAP?+
Write the Magento increment_id onto the SAP document and query for it before you post. On Business One that is a user-defined field, usually U_MagentoId, and the check is a $filter against /b1s/v2/Orders. Leave NumAtCard to the buyer's own purchase order number if you sell B2B, because one field cannot carry both. Then stop two workers holding the same order at once, with a single consumer per order key or a unique index in your own datastore, because the query alone loses a same-second race. On ECC, put the order number in the document itself instead of relying on DOCNUM, which numbers the IDoc.
Which SAP Magento connector should I buy?+
The one written for your SAP, which is the question most listings avoid. A connector built for Business One talks OData v4 to the Service Layer. A connector built for ECC builds IDocs and hands them to a port. Those are not the same software with a different checkbox ticked. Ask which SAP it was written against, and ask what it does with a guest order, before the subscription starts.
Do I need middleware to connect Magento and SAP?+
No, if you can run and monitor an endpoint. Middleware sells you retries, mapping, monitoring and somebody else's on-call rota, and it bills monthly for them forever. A direct push into your own receiver costs less and puts the retry and monitoring work on your team, which is fine if you were going to build it anyway.
What happens if SAP is down when a customer checks out?+
Nothing the customer can see, if the integration is built correctly. Magento takes the order, the event waits on the queue, and delivery keeps trying until the Service Layer or the IDoc port is accepting again. If a Business One maintenance window can take your checkout down with it, something is calling SAP inline during order placement, and that is the first thing to fix.
Does Magento Open Source support webhooks?+
No. Native webhooks and Adobe I/O Events are Adobe Commerce capabilities, and Adobe's own installation docs put Open Source outside support for both. The install path differs between the two, which matters if you are the one installing: webhooks come as a module you add with Composer on Adobe Commerce 2.4.4 and later, while the eventing modules load automatically from 2.4.6 and later and are a Composer install before that. On Open Source you get event push either by adding a module that observes order events and posts them, or by putting them on the message queue and running your own consumer with bin/magento queue:consumers:start.

Magento SAP Integration: How Orders Actually Reach SAP? Your store deserves a developer on speed dial.

Talk to a Magento developer