APIs in ERP
Modern ERPs are API-first, or claim to be. Understanding what’s actually available — and what it costs — is a core skill.
API styles you’ll meet
| Style | Where | Notes |
|---|---|---|
| OData v2/v4 | SAP, Dynamics 365 | Query in the URL: $filter, $expand, $select, $top. Powerful and verbose |
| REST/JSON | Most modern ERPs | The default expectation |
| SOAP | Legacy enterprise, still everywhere | WSDL, XML, WS-Security |
| GraphQL | Rare in core ERP, common in the extension layer | Good for composing views |
| RFC / BAPI | SAP-specific remote function calls | The classic SAP integration mechanism |
| IDoc | SAP asynchronous document exchange | Effectively SAP’s internal EDI |
| SuiteTalk / SuiteQL | NetSuite | SOAP/REST + a SQL-like query language |
| XML-RPC / JSON-RPC | Odoo | Direct model access with search_read, create, write |
| Database views | Everywhere | Read-only reporting access. Not an API, but treated as one |
SAP specifics (because you’ll meet them)
| Mechanism | Use |
|---|---|
| BAPI | Business API — a released, stable function module (BAPI_SALESORDER_CREATEFROMDAT2). Requires an explicit BAPI_TRANSACTION_COMMIT |
| RFC | Remote Function Call, the transport underneath BAPIs |
| IDoc | Intermediate Document — asynchronous, queued, with status tracking and reprocessing. The workhorse for high-volume async |
| OData / SAP Gateway | The modern synchronous REST-ish layer, used by Fiori |
| CDS views | Core Data Services — define a semantic view in the DB, expose it as OData |
| RAP | ABAP RESTful Application Programming model — the current framework for building APIs |
| SAP API Business Hub | Catalogue of released APIs. Only use what’s listed there |
IDoc status codes are worth knowing if you touch SAP: 03 = sent, 51 = application error, 53 = posted successfully, 64 = ready to process. IDoc monitoring and reprocessing is a routine support task.
Designing ERP APIs
Resource modelling
GET /api/v1/sales-orders?customerId=100&status=open&limit=50
GET /api/v1/sales-orders/4711
POST /api/v1/sales-orders
PATCH /api/v1/sales-orders/4711
POST /api/v1/sales-orders/4711/reject
GET /api/v1/sales-orders/4711/document-flow
Business actions are POST sub-resources, not PATCHes with magic fields. POST /sales-orders/4711/reject is clearer, more auditable, and easier to authorise than PATCH {status: "rejected"}.
The header/item shape
POST /api/v1/sales-orders
{
"idempotencyKey": "shopify-order-88213",
"companyId": 1000,
"salesOrg": "1000",
"distributionChannel": "10",
"orderType": "OR",
"soldTo": "C-100455",
"shipTo": "C-100455-02",
"customerReference": "PO-2026-9981",
"requestedDeliveryDate": "2026-04-01",
"currency": "BDT",
"items": [
{ "itemNumber": 10, "material": "FAB-1000", "quantity": 500, "uom": "EA",
"plant": "DHK1", "requestedDeliveryDate": "2026-04-01" }
]
}
Response:
{
"salesOrderNumber": "0004500123",
"status": "created",
"netValue": 6250.00,
"currency": "BDT",
"items": [
{ "itemNumber": 10, "confirmedQuantity": 400, "confirmedDate": "2026-04-01" },
{ "itemNumber": 10, "confirmedQuantity": 100, "confirmedDate": "2026-04-12" }
],
"messages": [
{ "type": "warning", "code": "SD_PARTIAL_CONFIRM",
"text": "Item 10: 100 units confirmed for a later date due to availability." }
]
}
Three things this gets right:
- Idempotency key required on every create
- Schedule lines in the response — partial confirmation is normal, not an error
- A messages array — ERP operations produce warnings that aren’t failures. HTTP status alone cannot express “created, but with three caveats”
Error responses
HTTP 422
{
"error": "validation_failed",
"messages": [
{ "type": "error", "code": "SD_CREDIT_BLOCK", "field": null,
"text": "Credit limit exceeded for payer C-100455. Exposure 590,000 of limit 450,000.",
"detail": { "exposure": 590000, "limit": 450000, "currency": "BDT" } },
{ "type": "error", "code": "MM_MATERIAL_BLOCKED", "field": "items[1].material",
"text": "Material FAB-9000 is blocked for sales in sales org 1000." }
]
}
Rules: stable machine-readable codes (clients switch on them), human-readable text, the field path where applicable, and all errors at once — not one at a time, forcing the client into a fix-and-retry loop.
Bulk and async
Synchronous single-record APIs don’t work for 50,000 records.
POST /api/v1/materials/batch → 202 Accepted, { jobId }
GET /api/v1/jobs/{jobId} → { status, processed, failed, resultUrl }
GET /api/v1/jobs/{jobId}/errors → per-record errors
Or the file pattern: upload a file, get a job ID, poll for status, download results and an error report. Less elegant, and often what business users actually want because they work in Excel.
Never accept a synchronous request that will take 40 minutes. The connection times out, the client retries, and you process it twice.
Pagination
GET /api/v1/journal-lines?companyId=1000&postingDateFrom=2026-03-01&limit=500&cursor=eyJ...
Cursor-based, not offset-based. Offset pagination on a table receiving inserts skips and duplicates rows. On ERP tables — which are append-heavy by definition — this is guaranteed to happen.
Always: a maximum page size, a mandatory bounded filter (date range or org), and a documented default.
Rate limiting and quotas
ERPs enforce limits, and they matter:
| Limit | Typical |
|---|---|
| Requests per second | 10–100 per tenant |
| Concurrent connections | 5–20 |
| Records per request | 100–1,000 |
| Query timeout | 30–120s |
| Daily quota | Sometimes |
Communicate limits in headers (X-RateLimit-Remaining, Retry-After) and design clients to back off. A client that hammers on 429 makes the outage worse.
Also check the licence implications — see Licensing. Documents created via API may carry a per-document cost.
Versioning
/api/v1/sales-orders → URL versioning. Clear, explicit, the pragmatic default
Accept: application/vnd.erp.v1+json → header versioning. Cleaner URLs, harder to debug
ERP API consumers are slow to change — a warehouse system, a 3PL, a customer’s integration. Rules:
- Additive changes only within a version (new optional fields are fine)
- Never remove a field, rename a field, change a type, or tighten validation within a version
- 12+ months of deprecation notice before removing a version
- Track consumers per version so you know who to call
Authentication
| Method | Use |
|---|---|
| OAuth 2.0 client credentials | Service-to-service. The standard |
| OAuth 2.0 authorization code + PKCE | User-delegated access |
| mTLS | High-security, bank and government interfaces |
| API key | Simple, weakest, rotate frequently |
| SAML/OIDC SSO | Human users |
| Basic auth | Only inside a trusted network, never over the internet |
Authorisation is separate from authentication. An API token must be scoped:
scope: sales_orders:read, sales_orders:create
company: 1000, 2000 ← organisational scope
plant: DHK1 ← further restriction
The same authorisation model that applies to users must apply to API clients. See Security Model and RBAC.
Webhooks — outbound events
POST https://customer.example.com/webhooks/erp
X-ERP-Signature: sha256=...
X-ERP-Event-Id: evt_88213
{
"eventType": "SalesOrder.Shipped",
"eventId": "evt_88213",
"occurredAt": "2026-03-15T14:22:11Z",
"tenantId": 1001,
"data": { "salesOrderNumber": "0004500123", "deliveryNumber": "0080001234" }
}
Requirements:
- Signed payloads (HMAC) so the receiver can verify authenticity
- At-least-once delivery with retries and exponential backoff
- Event ID so the receiver can deduplicate
- A replay endpoint for missed events
- Delivery status visible to the subscriber
- Automatic disabling of endpoints failing persistently, with notification
- Publish via the transactional outbox — see Architecture Patterns
Documentation
Minimum viable API documentation:
- OpenAPI/Swagger spec, generated from the code so it can’t drift
- Authentication guide with a working example
- Real request/response examples for every endpoint
- Error code reference with remediation guidance
- Rate limits and quotas
- Changelog and deprecation notices
- A sandbox environment with realistic seeded data
A sandbox with realistic data is worth more than beautiful documentation. Integration developers learn by calling, not by reading.
Testing APIs
- Contract tests: the response matches the schema
- Idempotency: same key twice → one document, same response
- Concurrency: parallel creates with different keys
- Authorisation: a token scoped to company 1000 cannot read company 2000
- Rate limiting: returns 429 with Retry-After
- Large payloads: 1,000-line order
- Partial failure: what happens when line 7 of 40 fails?
- Period closed, credit block, material blocked — all the business error paths
That last one matters: most API bugs in ERP are unhandled business error paths, not technical failures. The happy path always works.