// erp · payments · peru
How to make every charge invoice itself in SAP Business One
The promise sounds simple: every charge in your system should show up invoiced in SAP Business One without anyone keying anything. And the part everyone looks at —calling the API— is the easy one. What sinks these projects is something else: a payment is not one object in SAP, it is a chain, and if one link is missing the ERP still returns a 200 while your accounting quietly stops balancing. This is the guide to that chain, written from having put it in production for a club of 50,000+ members.
3 objects
for every charge you want invoiced
Service Layer
the modern route: REST over OData
CDR
in Peru the invoice ends at SUNAT, not at SAP
You orchestrate
sessions, retries and duplicates are yours
The three doors into SAP B1
Service Layer is the modern interface: REST over OData, HTTP and JSON. DI API is the older layer, COM-based and heavier to operate. B1if is SAP’s integration framework. For a new web system, the Service Layer is almost always the right starting point.
The detail that saves arguments: DI API and Service Layer share the same data core, so the objects and their properties are identical in both. Choosing one over the other does not change the business model you have to respect; it changes the transport. Object documentation written for the DI API still helps you understand which fields exist.
The Service Layer follows the OData protocol —versions 3 and 4, with 4 as the primary one in recent releases— and that carries a practical consequence: it is comfortable for creating and reading documents, but it is not designed for complex SQL queries or for wrapping several operations in a transaction the way you would in a database. That limit shapes the design, and it is better known before than during.
What actually takes work: a payment is a chain
Invoicing a payment in SAP requires three chained pieces: business partner, invoice and payment applied to that invoice. Creating the invoice without applying the payment is the most common mistake — and the quietest, because nothing fails.
| Link | What it is | If it’s missing |
|---|---|---|
| Business partner | The customer must exist in SAP, with their tax ID and fiscal data. | The invoice cannot be issued to anyone. |
| Invoice | The sales document, with its series, its currency and its VAT. | You charged, but accounting-wise the sale never happened. |
| Applied payment | The payment recorded and —this is what gets forgotten— applied against that invoice. | The customer shows as owing forever, with an unapplied balance. |
The third link is the one that gets forgotten, and its symptom takes weeks to appear: accounting reports customers owing money they already paid, while the money sits in the bank. Nobody looks at the integration because it “works” — invoices are being created. The problem is that creating the payment is not applying it: they are two different things and only the second one closes the debt.
The trap: HTTP 200 does not mean the accounting balances
The Service Layer validates the shape of the document, not the coherence of your process. You can create a perfectly valid invoice against the wrong customer, with the wrong series, or without its payment applied — and get a 200 in all three cases.
That is why verification cannot be “the API answered fine”. It has to be a reconciliation: what your gateway charged against what ended up invoiced and applied in SAP, by period. It is the same principle we apply when integrating Culqi and Niubiz: your system is the operational source of truth, and what closes the month is the reconciliation, not the response code.
In Peru, the invoice does not end at SAP
SAP B1’s Peruvian localization covers VAT, the chart of accounts and the electronic books, but the document only truly exists once SUNAT returns its CDR. That trip —from SAP to the OSE, from the OSE to SUNAT and back— is part of your integration, even if an add-on operates it.
This changes your state machine. An invoice created in SAP is not an invoice delivered to the customer: it can be rejected. Your system needs an intermediate state —issued, pending CDR— and something that reacts when the response arrives, or when it does not. We wrote separately about how SUNAT electronic invoicing works and the role of the CDR.
Practical consequence for scoping: ask on day one which electronic invoicing add-on the client uses and how it is triggered. Issuing automatically when the document is created is not the same as depending on a scheduled process — and that decides whether your customer gets their receipt in seconds or the next day.
The Service Layer does not orchestrate for you
The documentation is explicit about something that gets underestimated: the Service Layer does not constitute a complete integration on its own. Authentication, sessions, timeouts, duplicate requests, retries, logs and alerts all have to be handled by your solution.
Two details that bite in production. First: sessions expire, so your HTTP client needs to renew them on its own rather than fail. Second, and the costlier one: retries duplicate documents. If your process retries an invoice because it got no response —when it was actually created— you end up with two documents for one sale, and in a system with fiscal numbering that is not deleted: it is voided, with a credit note.
The defense is the usual one: store the reference of the document created in SAP in your own database before treating the retry as safe, and query before creating again. It is exactly the idempotency pattern we use with gateways, applied to the ERP.
Common mistakes when integrating payments with SAP B1
None of the ones that hurt most are coding errors: they are modeling decisions nobody made at the start.
do it like this
- Model the full chain: business partner, invoice and payment applied to that invoice.
- Store the SAP document reference in your database before considering the send closed.
- Query before retrying: in an ERP with fiscal numbering, duplicating costs a credit note.
- Reconcile by period what was charged against what was invoiced and applied — don’t trust the response code.
- Ask during scoping which electronic invoicing add-on exists and how it is triggered.
avoid this
- Creating the invoice and not applying the payment: the customer owes forever.
- Assuming a 200 from the Service Layer means the accounting balances.
- Treating the invoice as delivered the moment SAP creates it, without waiting for SUNAT’s CDR.
- Letting expired sessions surface as intermittent errors.
- Designing around database-style transactions: OData does not work that way.
How we do it
We integrate payments with SAP Business One in production: in the enrollment system of a club with 50,000+ members, every enrollment invoices itself in SAP B1 — with the full chain and the reconciliation solved.
We know both ends of the problem: the gateway that charges and the ERP that invoices. We did it for Club de Regatas Lima, where it also had to hold up on peak enrollment day. If your operation charges on one side and invoices on the other, let’s talk.
In summary
Integrating payments with SAP Business One is not won at the API call: it is won by respecting the model. Every charge needs its business partner, its invoice and its payment applied against that invoice — and the link that gets forgotten is the third one, whose symptom takes weeks to surface. Add that the Service Layer does not orchestrate for you (sessions, retries and duplicates are yours) and that in Peru the document only exists once SUNAT returns the CDR. Model the chain and set up the periodic reconciliation, and the promise keeps itself: every charge, invoiced, with nobody keying anything.
Do you charge on one side and invoice on the other?
We have connected gateways with SAP Business One in production, with the full chain and the reconciliation solved. You talk with the engineer who would build the integration, not a salesperson.
