Common API tasks, by job
Ninety percent of what an ERP integrator builds here is four jobs. This article is the recipe for each: which interface, which resource, roughly what comes back, and the thing that goes wrong.
items and data
depend on your installed apps — read the exact shape from your tenant's
https://api.revenexx.com/v1/openapi.json, and take topic names from Events ›
Event Catalog. Do not code against the field names below.The four jobs at a glance
| Job | Direction | Volume | Latency needed | Interface |
|---|---|---|---|---|
| Orders out | Platform → ERP | One at a time | Immediate | Webhook, plus a reconciliation poll |
| Stock in | ERP/WMS → Platform | Thousands of rows | Minutes to hours | Bulk import, or a live read for the few that matter |
| Prices in | ERP → Platform | Thousands of rows | Nightly | Bulk import |
| Customer master in | ERP → Platform | Hundreds of rows | Nightly, plus live for credit | Bulk import, plus a live read at checkout |
Read the pattern: exactly one of the four wants the REST API in a loop, and it is the one that handles single records. The other three are files. That is not a limitation of the platform — it is what the shape of the data is telling you. See Integration patterns.
Job 1: Orders out
Every order placed here has to reach your ERP, once, with everything the ERP needs to book it.
The webhook route — the right one
Subscribe to the order-created topic in Events › Event Catalog and have the ERP receive it. The delivery carries the event id, the topic, a timestamp and a signature — see Set up webhooks.
The event body tells you that an order exists and which one. Most integrations then fetch the full order, because an event payload is deliberately small:
GET /v1/orders/{id} HTTP/1.1
Host: api.revenexx.com
X-Revenexx-Tenant: acme-eu
X-Revenexx-Api-Key: rvxk_…
The pull route — as the safety net, not the mechanism
List orders that have appeared since your last successful run:
GET /v1/orders?limit=100&order=created_at.desc HTTP/1.1
Paging is limit and offset, sorting is order=<field>.<direction>, and the
answer is always the same envelope:
{ "items": [ ... ], "total": 248, "limit": 100, "offset": 0 }
Run this hourly as reconciliation, not every thirty seconds as a substitute for the webhook. A webhook is a delivery attempt, not a guarantee; without the reconciliation run, a receiver that was down through its whole retry window loses orders silently.
Acknowledging back
Once the ERP has booked the order, say so. The order resource exposes actions for exactly this:
POST /v1/orders/{id}/acknowledge
POST /v1/orders/{id}/cancel
POST /v1/orders/{id}/complete
Acknowledgement is what closes the loop: it is how you distinguish "the platform sent it" from "the ERP has it", and it is the join your reconciliation report runs on.
What the ERP actually needs
Do not let the integrator take only the header. B2B orders carry per-position data that the ERP needs to book the line at all:
| Carried on | What | Why it matters |
|---|---|---|
| Order | customer_order_number | The buyer's own PO number. Without it, their accounting cannot match the invoice |
| Order | The three statuses | status, payment_status and fulfillment_status are independent — see The three order statuses |
| Position | Cost centre | One order can span three departments' budgets |
| Position | The buyer's own article number | Their part number, not yours |
| Position | Position text | The machine, floor or Kostenstelle note |
The trap
An order transmitted twice is two deliveries. Deduplicate on the event id, which is stable across retries, and never on a generated one of your own. And remember that ownership of the order moves at transmission: after the ERP has it, the ERP owns its status — see Deciding your system of record.
Job 2: Stock in
Your warehouse or ERP knows what is on the shelf. The shop needs to show it without lying.
The model, first
| Field | Meaning |
|---|---|
on_hand | What is physically there, per location and article |
reserved | What is already promised to somebody |
available | on_hand − reserved. Computed, never stored, never written |
reorder_point | Your own purchasing trigger (Meldebestand), not the customer's |
available is the number a buyer should see. Pushing available as if it were
on_hand double-counts your reservations and produces phantom stock-outs.
The bulk route — the right one for the whole warehouse
Push a full-state stock file on a schedule. Full state, not deltas: a delta stock file applied twice leaves stock wrong by exactly one run's movements, silently, with no error anywhere.
The bulk plane takes the file as a job rather than a request — you register it, it processes, you poll its state:
POST /v1/io/uploads → a short-lived signed upload URL
POST /v1/io/imports → register the uploaded file against a profile
GET /v1/io/bulk-jobs/{job_id} → rows read, written, rejected
Or drop the file on SFTP and let a scheduled profile pick it up, which is what most ERPs actually do.
The REST route — for the few articles that matter
Reading or adjusting individual stock levels:
GET /v1/inventories/stock?limit=100
GET /v1/inventories/availability?sku=4711-A
POST /v1/inventories/movements
Use this for corrections and for the handful of high-value articles where a nightly figure is not good enough. Do not use it in a loop over your catalog — see Rate limits.
The event alternative, in the other direction
The platform can tell you when stock crosses a threshold, so your purchasing system does not have to poll. Subscribe to the low-stock topic and you get a push when an article falls under its reorder point.
The trap
Never let the shop write stock. Stock is a running total of a movement ledger; a figure somebody typed over the top is a figure nobody can explain a week later. Correct with a booked adjustment, and see When stock numbers drift.
Job 3: Prices in
Prices are the field where being wrong costs a credit note, so this one gets the most care.
The model, first
A price is not a field on a product. It is a row in a price list, and the list carries the scope: currency, customer, channel, market, validity window, priority. That indirection is what lets one article have a catalogue price, a contract price, a scale price and a Swiss price simultaneously — see How pricing works.
So "push prices" always means: push rows into the right list.
The bulk route — the only sensible one at volume
POST /v1/io/uploads
POST /v1/io/imports → against a price import profile
GET /v1/io/bulk-jobs/{job_id}
A price import file is typically list code, SKU, quantity-from, amount, currency and validity dates. The mapping — which column becomes which field, and which fields the file may not touch — lives in the profile, see Field mapping and Import prices.
Reading list structure back is an ordinary REST call:
GET /v1/prices/lists?limit=50
Resolving one price
When something needs the price for one buyer, one article and one quantity — an order-list conversion, a quotation tool, a check — that is a resolve call rather than a lookup, because it has to walk the whole priority chain:
POST /v1/prices/resolve
Use this rather than reimplementing the priority rules in the ERP. Two implementations of price resolution will disagree, and the disagreement will surface on an invoice.
The trap
Expiring validity windows fail open, not closed. A contract price whose
valid_until passes does not raise an error; the next-best list applies and the
buyer pays more. Put the validity dates of contract prices on a review calendar
— see Price list hygiene.
Job 4: Customer master in
The ERP owns who your customers are. The platform owns how they log in.
Split the entity before you write any code
This is the field-ownership conversation, and it is per field, not per entity:
| Field | Owner | Direction |
|---|---|---|
| Customer number | ERP | ERP → Platform |
| Company name, addresses | ERP | ERP → Platform |
| Payment terms | ERP | ERP → Platform |
| Credit limit, blocked status | ERP | Read live at checkout |
| Shop logins, contacts, roles | Platform | Platform only |
| Approval rules, cost centres, budgets | Platform | Platform only |
The ERP has no concept of a shop login, and the platform has no business inventing a credit limit. See Deciding your system of record.
The bulk route
Organizations and their addresses arrive nightly as a file, matched on the customer number — a stable business key, never an internal id. Every import should mean "make the record look like this", so re-running converges instead of duplicating.
The REST route
GET /v1/customers/organizations?limit=100
GET /v1/customers/contacts?limit=100
GET /v1/customers/organization_metrics
The metrics resource is worth knowing about for BI: it carries per-account
revenue and order counts over 30, 90 and 365 days plus last_order_at,
maintained for you rather than computed on the fly. See The metrics that
matter in B2B.
The live read that is not optional
Credit limit and blocked status must be current at checkout, not from last night. Delivering to a customer your Buchhaltung blocked this morning is the one failure in this article that costs real money. Make it a live call, and decide what happens when the ERP does not answer — refuse, or let it through and flag. Both are defensible; not deciding is not.
The trap
Do not sync contacts from the ERP. The ERP's contact table is invoice recipients and a phone number from 2014. Shop contacts have roles, permissions, approval limits and cost-centre assignments that no ERP models. Let the platform own them — see Contacts and roles.
The two rules that apply to all four
Match on a stable business key. SKU, customer number, order number. An internal database id the other system has never seen is not a key you can match on.
Make every write idempotent. Things fail halfway. A timeout tells you
nothing about whether the other side processed the request. If running the
operation twice has the same effect as running it once, the recovery from any
failure is "run it again" — and there is no Idempotency-Key header here, so
idempotency has to be a property of your data, not of a header. See Test an
integration end to end.
Next
- Bulk exports — the outbound half: BI extracts and nightly files.
- Test an integration end to end — proving all four before go-live.