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:

  1. Idempotency key required on every create
  2. Schedule lines in the response — partial confirmation is normal, not an error
  3. 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.

Next