Skip to content

Sync orders to an ERP

This guide connects a marketplace to an ERP, an order management system or an accounting tool: each order the customer pays is created once in your system, and its shipping and completion flow back to Kreezalid. It uses webhooks to learn about orders and the Admin API to read and update them.

Kreezalid ── order.status.new ──▶ your endpoint ──▶ queue ──▶ ERP
▲ │
└──────────── PATCH /api/v1/orders/{id} ◀──────────────────┘

In Settings → Notifications (/admin/settings/notifications), add one webhook per event, all pointing to the same URL:

Event Use it to
order.status.new Create the sales order: the customer has paid or validated.
order.status.accepted Release it for preparation, when vendors accept orders first.
order.status.declined, order.status.canceled Cancel it in the ERP.
order.status.completed Close it.

Do not create anything on order.status.draft: a draft is an unpaid checkout that is often abandoned. Which statuses an order goes through depends on the marketplace; the full list is in Events.

Your endpoint verifies the signature, records the event and answers 200 right away. The work with the ERP happens afterwards, from a queue, so a slow ERP never makes Kreezalid retry.

webhooks.mjs
import express from 'express';
import { verifyKreezalidSignature } from './verify.mjs'; // see Verifying signatures
import { db, queue } from './infra.mjs';
const app = express();
app.post('/kreezalid/webhooks', express.raw({ type: '*/*' }), async (req, res) => {
if (!verifyKreezalidSignature(req, process.env.KREEZALID_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const eventId = req.get('X-Kreezalid-Event-Id');
if (eventId === 'test') return res.sendStatus(200);
// Deduplicate: the same event can be delivered more than once.
const isNew = await db.insertIgnore('kreezalid_events', { id: eventId });
if (isNew) {
const event = JSON.parse(req.body.toString('utf8'));
await queue.push('kreezalid-order', { type: event.type, orderId: event.data.id });
}
res.sendStatus(200);
});

The verification function is in Verifying signatures, and the retry schedule in Delivery and retries.

The worker reads the order from the Admin API rather than trusting the payload alone: a webhook carries the order in its state at delivery time, and a retry can arrive after later changes.

  1. Read the order: GET /api/v1/orders/{id}. It returns the customer, the vendor, the items with their listing_sku, the shipping address and amounts, and the order’s metafields.

  2. Check whether it already exists in the ERP. Look for the sales order number you stored on the order (next step) in metafields.erp.sales_order. If it is there, the order was already created: stop.

  3. Create it in the ERP, using the order’s order_number as your external reference.

  4. Store the ERP’s number on the order with a metafield:

    Terminal window
    curl -X POST https://acme.mykreezalid.com/api/v1/orders/90311/metafields \
    -u "$KREEZALID_API_KEY:$KREEZALID_API_SECRET" \
    -H "Content-Type: application/json" \
    -d '{"namespace": "erp", "title": "sales_order", "value": "SO-2026-11873"}'
worker.mjs
const ADMIN = 'https://acme.mykreezalid.com/api/v1';
const headers = {
Authorization: 'Basic ' + Buffer.from(`${process.env.KREEZALID_API_KEY}:${process.env.KREEZALID_API_SECRET}`).toString('base64'),
'Content-Type': 'application/json',
};
export async function handleNewOrder(orderId, erp) {
const { order } = await (await fetch(`${ADMIN}/orders/${orderId}`, { headers })).json();
if (order.metafields?.erp?.sales_order) return; // already in the ERP
if (['draft', 'pending', 'failed', 'canceled'].includes(order.status)) return;
const salesOrder = await erp.createSalesOrder({
reference: order.order_number,
customerEmail: order.customer.email,
lines: order.order_items.map((i) => ({ sku: i.listing_sku, qty: i.qty, unitPrice: i.amount })),
shipping: order.shippings,
total: order.amount,
currency: order.currency,
});
await fetch(`${ADMIN}/orders/${orderId}/metafields`, {
method: 'POST',
headers,
body: JSON.stringify({ namespace: 'erp', title: 'sales_order', value: salesOrder.number }),
});
}

Your ERP may also offer a unique external reference: use order_number there too, so that two workers racing on the same order cannot both create it.

A cart with products from several vendors becomes several orders, one per vendor, each with its own order_number and its own order.status.new. Create one sales order per Kreezalid order: each vendor accepts, ships and is paid for their order separately.

When the ERP ships or closes an order, update it in Kreezalid with PATCH /api/v1/orders/{id}:

Terminal window
curl -X PATCH https://acme.mykreezalid.com/api/v1/orders/90311 \
-u "$KREEZALID_API_KEY:$KREEZALID_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"status": "shipped", "fulfillment_status": "shipped", "tracking_number": "6A12345678901"}'
{ "message": "Order updated", "success": true }
  • status must be one of the order statuses; any other value answers 404 with the list of valid ones.
  • fulfillment_status takes started, scheduled, prepared, shipped, in_transit, available, delivered, executed, canceled or error.
  • Send one order item per request when you update items: only the last item of an order_items array is applied.

Webhooks stop after repeated failures and can be switched off by mistake. Run a periodic check, every hour for instance, that lists the recent orders and processes those your system does not know:

Terminal window
curl "https://acme.mykreezalid.com/api/v1/orders?status=new&limit=100&page=1" \
-u "$KREEZALID_API_KEY:$KREEZALID_API_SECRET"

Orders come newest first by checkout date. Page until you reach orders older than your last check, and run each one through the same handler as the webhook: the metafield check makes it safe to process an order twice. Repeat for accepted if your ERP takes orders at that stage.

  • Signatures are verified and events deduplicated on X-Kreezalid-Event-Id.
  • Your endpoint answers 200 within a few seconds and works from a queue.
  • Each order is created once, guarded by the erp.sales_order metafield.
  • Status written back by the API is followed by your own customer message when needed.
  • A scheduled catch-up job covers missed or disabled webhooks.