Skip to main content

Sync orders to your ERP

Goal

Push every Sumeru-tracked order into your ERP / accounting system in real time, with attribution data attached for revenue analytics.

Architecture

Step 1 — Subscribe to webhook

curl -X POST \
-H "Authorization: Bearer copt_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/sumeru-webhook",
"events": ["order.attributed"],
"secret": "auto"
}' \
"https://api.sumeru.systems/api/v1/webhook-subscriptions"

Response includes the secret — save it (shown once):

{
"data": {
"id": "whk_abc123",
"secret": "wsec_xY7aB3cKz9..."
}
}

We subscribe to order.attributed (not order.created) so we get attribution data with the order.

Step 2 — Receive + verify

import express from 'express';
import crypto from 'node:crypto';
import { ERP } from './your-erp-client.js';

const app = express();
const ATLANTIS_SECRET = process.env.ATLANTIS_WEBHOOK_SECRET;
const seenEvents = new Map(); // production: use Redis

function verifySumeruSignature(rawBody, signature) {
const expected = 'sha256=' + crypto
.createHmac('sha256', ATLANTIS_SECRET)
.update(rawBody)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature || ''),
Buffer.from(expected)
);
} catch {
return false;
}
}

app.post(
'/sumeru-webhook',
express.raw({ type: 'application/json' }),
async (req, res) => {
// 1. Verify signature on RAW body
const sig = req.headers['x-sumeru-signature'];
if (!verifySumeruSignature(req.body, sig)) {
return res.status(401).send('Invalid signature');
}

// 2. Parse + dedup
const event = JSON.parse(req.body.toString());
if (seenEvents.has(event.id)) {
return res.status(200).end(); // Already processed
}

// 3. Process
try {
await syncOrderToErp(event.data);
seenEvents.set(event.id, Date.now());
return res.status(200).end();
} catch (err) {
console.error('ERP sync failed:', err);
return res.status(500).end(); // Sumeru will retry
}
}
);

app.listen(3000);

Step 3 — Push to ERP

async function syncOrderToErp(orderData) {
// orderData has shape from order.attributed event
const erpPayload = {
external_order_id: orderData.shopify_order_id,
customer_email: orderData.customer.email,
total: orderData.total,
currency: orderData.currency,
line_items: orderData.line_items.map(li => ({
sku: li.sku,
quantity: li.quantity,
price: li.price,
})),
attribution: {
first_touch_channel: orderData.attribution.first_touch.channel,
last_touch_channel: orderData.attribution.last_touch.channel,
multi_touch_credits: orderData.attribution.touchpoints.map(t => ({
channel: t.channel,
weight: t.credit_pct,
})),
},
};

// Idempotency on your ERP side
await ERP.upsertOrder(orderData.shopify_order_id, erpPayload);
}

Step 4 — Backfill historical orders

For initial backfill (orders before webhook subscription):

import { SumeruES } from '@sumeru/sdk';

async function backfillHistoricalOrders() {
const sumeru = new SumeruES({ apiKey: process.env.ATLANTIS_API_KEY });

let cursor = null;
do {
const { data, pagination } = await sumeru.orders.list({
filter: { date_from: '2026-01-01' },
include: ['attribution'],
limit: 100,
cursor,
});

for (const order of data) {
await syncOrderToErp(order);
}

cursor = pagination.has_next ? pagination.next_cursor : null;
} while (cursor);
}

Run once; webhook handles ongoing.

Step 5 — Test

Trigger a test webhook from admin:

curl -X POST \
-H "Authorization: Bearer copt_live_..." \
-d '{"event_type": "order.attributed"}' \
"https://api.sumeru.systems/api/v1/webhook-subscriptions/whk_abc123/test"

Sends a sample event to your URL. Verify:

  • HMAC verification passes
  • Order appears in ERP
  • 200 returned

Operational notes

Monitoring

Track in your ops dashboard:

  • Webhook arrivals per minute
  • HMAC failures (should be 0)
  • ERP sync failures + retries
  • Time from order creation → ERP arrival (P50, P99)

P99 target: < 5 minutes after attribution window closes.

Failure handling

  • 5xx from your endpoint → Sumeru retries 5×
  • Persistent failures → Sumeru auto-disables webhook after 100 fails in 24h
  • Use admin failures dashboard to monitor

Security

  • Endpoint should be HTTPS only
  • Verify signature on every request
  • Verify on RAW body (before parse)
  • Use timing-safe comparison
  • Rotate secret quarterly

Backpressure

If your ERP is slow, add a queue between webhook receiver and ERP push:

import Queue from 'bull';
const orderQueue = new Queue('orders-to-erp');

app.post('/sumeru-webhook', ..., async (req, res) => {
// Verify signature, then enqueue
await orderQueue.add(event);
res.status(200).end();
});

orderQueue.process(async (job) => {
await syncOrderToErp(job.data.data);
});

This decouples webhook ack (fast) from ERP push (potentially slow).

See also