Skip to content
v0.3

Connect an ERP

The commonest integration: a system of record on one side, Forge on the other. Stock and prices flow in; orders flow out.

ERP ──(commands)──▶ Forge stock levels, prices, catalogue
ERP ◀──(events)─── Forge orders, payments, shipments

Two directions, two mechanisms, and they are not interchangeable. Pushing is a command; hearing is an event.

1. A key for the integration, and only for it

Section titled “1. A key for the integration, and only for it”

Issue one named for the system — ERP Nimbus — with the scopes it actually needs. See issue & rotate API keys.

Do not reuse a key that something else also uses. Rate limits are per credential, revocation is per key, and the audit trail is only useful if the name means one thing.

import { createClient } from '@forgecommerce/sdk';
const forge = createClient({ baseUrl: process.env.FORGE_URL!, token: process.env.FORGE_TOKEN! });
const result = await forge.call(
'inventory.set_level',
{ sku_id: 'sku_…', warehouse_id: 'wh_…', quantity: 42 },
{ idempotencyKey: `stock-${sku}-${syncRunId}` },
);
if (!result.ok) {
// 'conflict' — the state will not have it; retrying unchanged changes nothing
// 'forbidden' — the key lacks the scope; a different key, not a retry
// 'internal' — retry with backoff
}

Every command declares the events it emits, and they are written in the same transaction as the change. If the order exists, the event exists. See events and the outbox.

Your consumer will see the same event twice. Delivery is at-least-once — after a retry, a redeploy, a timeout that succeeded on the far side. So make the handler idempotent: key your side-effect on something in the event, not on the fact that you received it.

async function onOrderPaid(event) {
if (await erp.hasInvoiceFor(event.order_id)) return; // seen it
await erp.createInvoice(event.order_id);
}

4. Rate limits are a contract, not a surprise

Section titled “4. Rate limits are a contract, not a surprise”

Every authenticated response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset — on success, not only on a 429. Read them in your client and slow down before you are told to.

The subject is the credential per tenant, so your ERP’s budget is its own: a nightly sync running hot cannot take the storefront’s other integrations down.

Do not poll for changes you could hear as events. It costs your rate budget and it is always behind.

Do not write around the port. There is no direct database access, and there is no second write path to find. Everything an integration needs is a command; if something seems to be missing, it is a conversation rather than a workaround.

Do not put business rules in the ERP that the kernel should ask about. If Forge needs to know something only your system can answer — which warehouse serves this order, whether this customer gets this price — that is a decision, answered inside the transaction, not a value you sync ahead of time and hope is current.

  • The audit trail in the admin, filtered by the key’s name. Every call is there, refusals included.
  • whoami — settles “does this key still carry what I think”.
  • The errors page — six codes, and each one means a different thing to do.