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} ◀──────────────────┘1. Subscribe to the order events
Section titled “1. Subscribe to the order events”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.
2. Receive and acknowledge
Section titled “2. Receive and acknowledge”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.
import express from 'express';import { verifyKreezalidSignature } from './verify.mjs'; // see Verifying signaturesimport { 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);});<?php$raw = file_get_contents('php://input');
if (!verifyKreezalidSignature($_SERVER, $raw, getenv('KREEZALID_WEBHOOK_SECRET'))) { // see Verifying signatures http_response_code(401); exit;}
$eventId = $_SERVER['HTTP_X_KREEZALID_EVENT_ID'] ?? '';if ($eventId !== 'test') { // Deduplicate: the same event can be delivered more than once. $stmt = $pdo->prepare('INSERT IGNORE INTO kreezalid_events (id) VALUES (?)'); $stmt->execute([$eventId]);
if ($stmt->rowCount() === 1) { $event = json_decode($raw, true); $queue->push('kreezalid-order', ['type' => $event['type'], 'orderId' => $event['data']['id']]); }}
http_response_code(200);from flask import Flask, requestfrom verify import verify_kreezalid_signature # see Verifying signaturesfrom infra import db, queueimport json, os
app = Flask(__name__)
@app.post("/kreezalid/webhooks")def receive(): if not verify_kreezalid_signature(request, os.environ["KREEZALID_WEBHOOK_SECRET"]): return "", 401
event_id = request.headers.get("X-Kreezalid-Event-Id") if event_id != "test" and db.insert_ignore("kreezalid_events", id=event_id): event = json.loads(request.get_data()) queue.push("kreezalid-order", {"type": event["type"], "orderId": event["data"]["id"]})
return "", 200The verification function is in Verifying signatures, and the retry schedule in Delivery and retries.
3. Create the sales order once
Section titled “3. Create the sales order once”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.
-
Read the order:
GET /api/v1/orders/{id}. It returns the customer, the vendor, the items with theirlisting_sku, the shipping address and amounts, and the order’smetafields. -
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. -
Create it in the ERP, using the order’s
order_numberas your external reference. -
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"}'
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.
4. Write the fulfilment back
Section titled “4. Write the fulfilment back”When the ERP ships or closes an order, update it in Kreezalid with
PATCH /api/v1/orders/{id}:
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 }statusmust be one of the order statuses; any other value answers404with the list of valid ones.fulfillment_statustakesstarted,scheduled,prepared,shipped,in_transit,available,delivered,executed,canceledorerror.- Send one order item per request when you update items: only the last item of an
order_itemsarray is applied.
5. Catch up what you missed
Section titled “5. Catch up what you missed”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:
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.
Checklist
Section titled “Checklist”- Signatures are verified and events deduplicated on
X-Kreezalid-Event-Id. - Your endpoint answers
200within a few seconds and works from a queue. - Each order is created once, guarded by the
erp.sales_ordermetafield. - 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.