Cookbook
What's here
Three worked recipes covering the most common integrations:
| Recipe | Use when |
|---|---|
| Sync orders to your ERP | You have a separate ERP / accounting system that needs Sumeru order data |
| Build custom attribution | You want a custom attribution pipeline (e.g. company-specific touchpoint weights) |
| Embed storefront widgets | You're not on Liquid; need to embed widgets in Hydrogen / Next.js / custom |
Each recipe is:
- Self-contained — code runs as-is
- Production-grade — auth, error handling, idempotency
- Documented — each line explained
Common patterns across recipes
Auth setup
All recipes assume a v1 admin token. Get one: Authentication & scopes.
const ATLANTIS_TOKEN = process.env.ATLANTIS_API_KEY; // copt_live_...
SDK vs. raw HTTP
Recipes show both. Pick whichever fits:
- SDK for typed methods + auto-retry + idempotency
- Raw HTTP for full control / non-supported language
Webhook-driven vs. poll-driven
For real-time integrations (ERP sync, custom attribution), use webhooks. Polling is rate-limit-hungry + slower.
Webhook setup pattern shown in each recipe.
Pattern: idempotent webhook handler
Re-used across recipes:
server/webhooks/sumeru.js
const seen = new Map(); // production: use Redis or DB
app.post('/webhook', async (req, res) => {
const eventId = req.body.id;
if (seen.has(eventId)) {
// Already processed; ack and skip
return res.status(200).end();
}
// Verify signature first — bail on mismatch before doing any work.
if (!verifySumeruWebhook(req.rawBody, req.headers['x-sumeru-signature'], SECRET)) {
return res.status(401).end();
}
try {
await handleEvent(req.body);
seen.set(eventId, Date.now());
return res.status(200).end();
} catch (err) {
console.error('Webhook processing failed:', err);
return res.status(500).end(); // We'll retry
}
});
Pattern: rate-limit-aware client
- cURL
- Node.js
- Python
- Ruby
# Bash with retry-on-429 (use this in shell scripts + CI)
for i in 1 2 3 4 5; do
response=$(curl -s -w "%{http_code}" \
-H "Authorization: Bearer $ATLANTIS_TOKEN" \
-H "Content-Type: application/json" \
"https://api.sumeru.systems/v1/customers")
status="${response: -3}"
if [ "$status" != "429" ]; then
echo "${response%???}"
break
fi
sleep "$((2 ** i))"
done
async function callSumeru(path, options = {}) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(`https://api.sumeru.systems${path}`, {
...options,
headers: {
'Authorization': `Bearer ${ATLANTIS_TOKEN}`,
'Content-Type': 'application/json',
...options.headers,
},
});
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') || '5', 10);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
return res;
}
throw new Error('Max retries exceeded');
}
import os, time, requests
ATLANTIS_TOKEN = os.environ["ATLANTIS_API_KEY"]
def call_sumeru(path, **kwargs):
for _ in range(5):
r = requests.request(
kwargs.pop("method", "GET"),
f"https://api.sumeru.systems{path}",
headers={
"Authorization": f"Bearer {ATLANTIS_TOKEN}",
"Content-Type": "application/json",
**kwargs.pop("headers", {}),
},
**kwargs,
)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 5)))
continue
return r
raise RuntimeError("Max retries exceeded")
require "net/http"
require "json"
def call_sumeru(path, method: :get, body: nil)
uri = URI("https://api.sumeru.systems#{path}")
5.times do
req = Net::HTTP.const_get(method.to_s.capitalize).new(uri)
req["Authorization"] = "Bearer #{ENV.fetch('ATLANTIS_API_KEY')}"
req["Content-Type"] = "application/json"
req.body = body.to_json if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
return res unless res.code == "429"
sleep res["Retry-After"].to_i.nonzero? || 5
end
raise "Max retries exceeded"
end
The language tab you pick is remembered across every code sample on the site — pick once and the rest follow.