| Plan ID | Capacity | Validity | Price |
|---|
Get started Amigo Developer Platform
Two APIs that share one auth, one wallet, and one rate limit: Data API for mobile-data top-ups across every Nigerian network, and Commerce API New for selling physical products from the Amigo catalog. Built for your backend, not your browser.
X-API-Key covers both APIs.Idempotency-Key.Your first call — buy 1GB of MTN data:
curl --location 'https://amigo.ng/api/data/' \
--header 'X-API-Key: YOUR_API_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"network": 1,
"mobile_number": "09012345678",
"plan": 1001,
"Ported_number": true
}'
Overview
Amigo Developers gives you two APIs that share the same auth, token, and wallet:
- Data API — buy mobile data on MTN, Glo, Airtel, and 9mobile. Per-plan pricing, idempotent retries, real-time efficiency scores.
- Commerce API New — sell physical products from the Amigo catalog (Amigo + Oraimo accessories). Wallet-paid, fulfilled by Amigo, with signed webhooks for every order event.
Base URL: https://amigo.ng/api.
All responses are JSON. Money is integer Naira (no decimals) and most responses also include a pre-formatted display string like "₦4,000".
Authentication
Pass your token in any of these headers — whichever your HTTP client makes easiest:
X-API-Key: amg_live_xxxxxxxxxxxxxxxx # --or-- Authorization: Token amg_live_xxxxxxxxxxxxxxxx # --or-- Authorization: Bearer amg_live_xxxxxxxxxxxxxxxx
Each token belongs to a single Amigo user + API client. The user_id identifies whose wallet gets debited; the client_id identifies which merchant owns the resulting work.
Test mode New
Build and integrate against a sandbox before touching real money. Test-mode tokens are intentionally familiar (Stripe-style) — the token prefix decides the environment:
| Token prefix | Mode | Wallet | Order references | Inventory |
|---|---|---|---|---|
amg_test_… |
test | Sandbox wallet — fake money, starts at ₦1,000,000. | AMG-COM-TEST-… |
Never touched — test orders don't affect live availability. |
amg_live_… (or legacy) |
live | Your Amigo wallet (real funds). | AMG-COM-… |
Decremented at order time, restored on cancel. |
Total isolation. A test token only sees test orders and test webhook endpoints; a live token only sees live ones. The two namespaces never cross. Every webhook payload includes a livemode boolean so subscribers can route or filter.
Refill the sandbox wallet:
Refills the sandbox wallet back to ₦1,000,000. Requires an amg_test_ token — live tokens get 403 live_token_not_allowed.
curl -X POST "https://amigo.ng/api/commerce/sandbox/reset-balance/" \
-H "X-API-Key: $TEST_TOKEN"
# → { "success": true, "data": { "test_balance": 1000000, "display": "₦1,000,000", "livemode": false } }
Errors
Every non-2xx response uses the same envelope:
{
"success": false,
"error": "insufficient_balance",
"message": "Wallet balance is below the order total."
}
| HTTP | error | When |
|---|---|---|
| 401 | invalid_token | Missing, expired, or revoked token. |
| 403 | ip_not_whitelisted | Your client has a whitelist and your IP isn't on it. |
| 404 | not_found | Product, order, or endpoint doesn't exist (or isn't yours). |
| 422 | invalid_* | Body validation failed; message explains. |
| 422 | insufficient_balance | Wallet is below the order total. |
| 422 | out_of_stock | Item ran out between catalog read and order create. |
| 429 | rate_limited | Too many requests in this minute window. |
| 500 | server_error | Unexpected. Logged on our end; retry with backoff. |
Idempotency
Both APIs accept an Idempotency-Key header on state-changing POSTs (buying data, creating orders). Retry with the same key and you get back the original response — no double-debit, no duplicate work.
curl -X POST https://amigo.ng/api/commerce/orders/ \ -H "X-API-Key: $TOKEN" \ -H "Idempotency-Key: ord-2026-05-17-7f3a" \ -H "Content-Type: application/json" \ --data @order.json
Keys are opaque strings up to 80 chars. Scope is per-token; reuse across tokens has no effect. Cache TTL is 24h.
Data API Mobile data top-ups
One endpoint: POST /api/data/. Pick a plan from the live catalog below, send it with the customer's mobile number, and we route the top-up through the carrier.
POST /api/data/
Body:
{
"network": 1, // 1=MTN, 2=Glo, 4=Airtel, 3=9mobile
"mobile_number": "09012345678",
"plan": 1001, // from /plans_catalog.php
"Ported_number": true // if MNP'd
}
<?php
$ch = curl_init('https://amigo.ng/api/data/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: YOUR_API_TOKEN',
'Content-Type: application/json',
'Idempotency-Key: '.bin2hex(random_bytes(16))
],
CURLOPT_POSTFIELDS => json_encode([
'network' => 1,
'mobile_number' => '09012345678',
'plan' => 1001,
'Ported_number' => true
])
]);
echo curl_exec($ch);
fetch('https://amigo.ng/api/data/', {
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_TOKEN',
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID()
},
body: JSON.stringify({
network: 1,
mobile_number: '09012345678',
plan: 1001,
Ported_number: true
})
}).then(r => r.json()).then(console.log);
curl -X POST https://amigo.ng/api/data/ \
-H "X-API-Key: $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data '{"network":1,"mobile_number":"09012345678","plan":1001,"Ported_number":true}'
Live plan catalog
These tables refresh on every page load from the same plan APIs your integration can use. Use the Plan ID column when calling POST /data/.
| Plan ID | Capacity | Validity | Price |
|---|
| Plan ID | Capacity | Validity | Price |
|---|
Plan efficiency
Reliability scores so you can pick plans that are safer for production traffic. A score ≥ 98% is excellent; < 75% is poor and we recommend avoiding.
| Plan ID | Capacity | Efficiency |
|---|
| Plan ID | Capacity | Efficiency |
|---|
| Plan ID | Capacity | Efficiency |
|---|
Commerce API New
Sell from the Amigo catalog through your own app or storefront. Browse products,
place orders charged against your Amigo wallet, and get webhook events for every
state change — from order.created to order.delivered.
What's in the catalog? 1,297 products across 14 consolidated categories — Amigo and Oraimo accessories (cables, power banks, earbuds, smart wear, etc.). Real prices, real stock counts, refreshed continuously.
How orders are charged. Each order is debited from your Amigo account balance inline at order-creation time. Pre-fund your account so orders just clear — your app never needs to handle card data at checkout.
List products
Paginated product listing with filters.
| Param | Type | Notes | |
|---|---|---|---|
| category | string | optional | Filter by category slug (e.g. power-banks). |
| brand | string | optional | oraimo or amigo. |
| in_stock | bool | optional | true to return only in-stock items. |
| q | string | optional | Free-text search across name + description. |
| sort | string | optional | price_asc, price_desc, name, new (default). |
| limit | int | optional | Default 20, max 100. |
| offset | int | optional | Default 0. |
curl "https://amigo.ng/api/commerce/products/?category=power-banks&in_stock=true&limit=5" \ -H "X-API-Key: $TOKEN"
{
"success": true,
"data": [
{
"id": 42,
"slug": "oraimo-traveler-3-10000mah-power-bank",
"brand": "oraimo",
"name": "Oraimo Traveler 3 10000mAh Power Bank",
"description": "Slim aluminum body with 22.5W fast charge…",
"price": { "amount": 14500, "display": "₦14,500", "currency": "NGN", "market": {…} },
"category": { "id": 5, "slug": "power-banks", "name": "Power Banks" },
"stock": { "in_stock": true, "count": 27 },
"rating": { "value": 4.6, "count": 312 },
"images": [ { "position": 1, "url": "https://res.cloudinary.com/…/traveler-3.jpg" } ],
"updated_at": "2026-05-16T18:32:11+01:00"
}
],
"pagination": { "total": 142, "limit": 5, "offset": 0, "has_more": true }
}
Get a single product
Single product. Same shape as a list entry. Use slug for stable URLs in your storefront — slugs don't change when IDs are renumbered.
curl "https://amigo.ng/api/commerce/products/?slug=oraimo-traveler-3-10000mah-power-bank" \ -H "X-API-Key: $TOKEN"
List categories
All 14 consolidated categories with product counts. Useful for rendering a nav or filter sidebar in your storefront.
{
"success": true,
"data": [
{ "id": 1, "slug": "charging-accessories", "name": "Charging Accessories", "product_count": 183 },
{ "id": 2, "slug": "power-banks", "name": "Power Banks", "product_count": 142 }
]
}
Curation lists
Admin-curated product lists for your storefront — "Featured", "New arrivals", "Bestsellers", "Deals", plus any custom lists Amigo's catalog team adds. Use them to populate your home page, category banners, and discover-style surfaces.
The same admin-curated pool is shared across all developer apps, but the products returned to your token are token-seeded random — so two apps built on Amigo's catalog don't show identical home pages. The order is stable per token: same token + same list = same order every call (cache-friendly, consistent customer UX). Different tokens see different subsets.
Discover available lists
Call with no type param to enumerate what's currently active:
curl 'https://amigo.ng/api/commerce/curation/' \ -H "X-API-Key: $TOKEN"
{
"success": true,
"data": [
{ "slug": "featured", "name": "Featured", "source_type": "manual", "description": "Admin-picked highlights.", "default_limit": 8, "pool_size": 50, "product_count": 12 },
{ "slug": "new", "name": "New arrivals", "source_type": "new", "description": "Recently added products.", "default_limit": 8, "pool_size": 50, "product_count": 23 },
{ "slug": "bestsellers", "name": "Bestsellers", "source_type": "bestseller", "description": "Top sellers, last 90 days.", "default_limit": 8, "pool_size": 50, "product_count": 9 },
{ "slug": "deals", "name": "Deals", "source_type": "deals", "description": "Products with market discount.", "default_limit": 8, "pool_size": 50, "product_count": 17 }
]
}
Fetch a list's products
Pass type=<slug> (or slug=<slug> — alias) to get the token-seeded subset:
curl 'https://amigo.ng/api/commerce/curation/?type=featured&limit=8' \ -H "X-API-Key: $TOKEN"
Query params:
type = slug of the list (required for products; omit to discover)
slug = alias of `type`
limit = optional override of the list's default_limit
(capped server-side at the list's pool_size)
Response (200):
{
"success": true,
"data": {
"list": {
"slug": "featured",
"name": "Featured",
"source_type": "manual",
"description": "Admin-picked highlights.",
"default_limit": 8,
"pool_size": 50
},
"products": [
// Same shape as GET /api/commerce/products/ — id, slug, brand, name,
// description, price, category, stock, rating, images, updated_at
{ "id": 42, "slug": "...", "name": "...", "brand": "oraimo", "price": {...}, "images": [...], … }
]
}
}
Eligibility filters (server-side):
- List must not be archived.
- If the list has an
active_from/active_untilwindow, the current time must be inside it (lets the team schedule "Black Friday picks" etc.). - Only
in_stock = trueproducts surface. - Pool is capped at the list's
pool_sizebefore shuffle;limitis then applied to the shuffle result.
Cache-Control: private, max-age=60 and Vary: X-API-Key. Safe to cache in the browser; refreshed at most every minute. If you need real-time freshness (e.g. for an admin preview), append a cache-buster query param.
Errors:
404 list_not_found // slug doesn't exist or list is outside its active window 405 method_not_allowed // only GET is supported 401 invalid_token // missing or bad X-API-Key
Empty pool (no in-stock products in the list) is 200 with products: [], not a 404 — the list itself exists, it just has nothing to show right now.
States & cities catalog
Master catalog of the 36 Nigerian states + FCT and their 774 Local Government Areas (called cities in the storefront). Public, no auth, cached 5 minutes — safe to call from a browser checkout flow.
Query parameters:
type = "states" (default) | "lgas"
state_id = int // optional, when type=lgas
state_code = "LA" | "KD" | "FCT" // optional, when type=lgas
search = "lagos" // typeahead across both — returns {states:[…], lgas:[…]}
List states:
curl 'https://amigo.ng/api/commerce/locations.php?type=states'
{
"success": true,
"data": [
{ "id": 25, "code": "LA", "name": "Lagos", "capital": "Ikeja", "region": "South-West", "sort_order": 250 },
{ "id": 19, "code": "KD", "name": "Kaduna", "capital": "Kaduna", "region": "North-West", "sort_order": 190 }
// …35 more rows
],
"count": 37
}
List cities in a state:
curl 'https://amigo.ng/api/commerce/locations.php?type=lgas&state_code=KD'
{
"success": true,
"data": [
{ "id": 142, "state_id": 19, "name": "Kaduna North", "slug": "kaduna-north", "is_capital": 1, "state_code": "KD", "state_name": "Kaduna" },
{ "id": 155, "state_id": 19, "name": "Zaria", "slug": "zaria", "is_capital": 0, "state_code": "KD", "state_name": "Kaduna" }
// …21 more rows
],
"count": 23
}
Typeahead search:
curl 'https://amigo.ng/api/commerce/locations.php?search=zar'
{
"success": true,
"query": "zar",
"states": [],
"lgas": [
{ "id": 155, "name": "Zaria", "state_code": "KD", "state_name": "Kaduna" }
]
}
Delivery quote
Quote shipping for a cart at a specific address. Returns prices for both Standard and Home delivery (premium) tiers plus an ETA range, so your checkout can show the customer their options before they commit. Same resolver as create order — the quote you show is what they'll be charged.
Body:
{
"state_code": "KD", // or "state_id": 19
"lga_name": "Zaria", // optional · or "lga_id": 155
"items": [
{ "product_id": 1, "quantity": 2 },
{ "product_id": 5, "quantity": 1 }
]
}
Up to 100 line items. quantity isn't used for pricing (delivery is per scheme, not per unit) but is required for shape consistency with /orders/.
curl -X POST https://amigo.ng/api/commerce/delivery/quote/ \ -H "Content-Type: application/json" \ --data @quote.json
Success (200):
{
"success": true,
"data": {
"location": {
"state_id": 19,
"state_code": "KD",
"state_name": "Kaduna",
"lga_id": 155,
"lga_name": "Zaria"
},
"serviceable": true,
"schemes": [
{
"scheme_id": 1,
"scheme_name": "Default Domestic",
"product_ids": [1, 5],
"product_count": 2,
"all_free": true,
"serviceable": true,
"tier_resolution": "state", // 'lga' | 'state' | 'fallback' | 'free'
"standard": { "price": 1500, "free_for_customer": true, "eta_min": 0, "eta_max": 2 },
"premium": { "price": 5000, "eta_min": 0, "eta_max": 2 }
}
],
"totals": { "standard": 0, "premium": 5000 },
"eta": { "min": 0, "max": 2 },
"warnings": []
}
}
Resolution chain — for each unique scheme in the cart, the resolver walks:
1. LGA-specific scheme group covering the customer's LGA → tier_resolution: "lga" 2. State-level scheme group covering the customer's state → tier_resolution: "state" 3. The scheme's "rest of Nigeria" fallback row → tier_resolution: "fallback" 4. No match found → serviceable: false
Products with no scheme assigned aggregate into a single scheme_id: null bucket and are always free (tier_resolution: "free").
free_delivery=true. The premium tier is always paid — free_delivery never zeros it. ETA reported at the cart level is the slowest scheme's range (the order is "delivered" when the last item arrives).
Unserviceable (200 with serviceable=false): when a scheme in the cart doesn't ship to the customer's area, the quote still returns 200 so the UI can show which scheme failed. Order create returns 422 unserviceable_delivery for the same cart — see below.
Create an order
Create a paid order. Your wallet is debited inline; if the balance is short, the order is
not created and you get 422 insufficient_balance. Prices are snapshotted from the catalog at order time, so you can't be surprised by a price change between read and write.
Body:
{
"customer": {
"name": "Aisha Bello", // required, ≤ 120 chars
"phone": "08012345678", // required, ≤ 20 chars
"email": "aisha@example.com" // optional
},
"delivery": {
"address": "Plot 12, Sabon Gari, off PZ Road", // required
"state_code": "KD", // optional · or "state_id": 19
"lga_name": "Zaria", // optional · or "lga_id": 155
"tier": "standard", // optional · "standard" (default) or "premium"
"city": "Zaria", // optional legacy free-text — auto-filled from lga_name
"state": "Kaduna", // optional legacy free-text — auto-filled from the resolved state
"notes": "Call on arrival"
},
"items": [
{ "product_id": 42, "quantity": 1 },
{ "product_id": 8, "quantity": 2 }
]
}
Up to 50 distinct line items per order; quantities 1–1000.
state_id / state_code / lga_id / lga_name in delivery to trigger the scheme-based shipping fee. The server walks the same chain as delivery quote and snapshots the result onto the order. Omit all four for free shipping (legacy back-compat — existing integrations keep working unchanged).
Success (201):
{
"success": true,
"data": {
"id": 1037,
"reference": "AMG-COM-20260520-AB12CD",
"status": "paid",
"payment_state": "paid",
"subtotal": { "amount": 29000, "display": "₦29,000" },
"delivery_fee": { "amount": 0, "display": "₦0" },
"total": { "amount": 29000, "display": "₦29,000" },
"customer": { "name": "Aisha Bello", "phone": "08012345678", "email": "aisha@example.com" },
"delivery": {
"address": "Plot 12, Sabon Gari, off PZ Road",
"city": "Zaria",
"state": "Kaduna",
"state_id": 19,
"lga_id": 155,
"scheme_id": 1,
"scheme_name": "Default Domestic",
"tier": "standard",
"was_free": true,
"eta_min_days": 0,
"eta_max_days": 2,
"breakdown": [
{ "scheme_id": 1, "scheme_name": "Default Domestic", "tier": "standard", "price": { "amount": 0, "display": "₦0" }, "eta_min_days": 0, "eta_max_days": 2, "product_count": 2 }
],
"notes": "Call on arrival"
},
"items": [ { "product_id": 42, "name": "Oraimo Traveler 3 …", "quantity": 1, "unit_price": {…}, "line_total": {…} }, … ],
"created_at": "2026-05-20 03:12:08",
"paid_at": "2026-05-20 03:12:08"
}
}
On legacy orders (no state_id/lga_id sent), delivery.scheme_id, delivery.tier, delivery.was_free, and delivery.eta_* are all null and breakdown is an empty array.
Unserviceable delivery (422): the cart resolves to a scheme that doesn't ship to the given area, and the scheme has no "rest of Nigeria" fallback configured.
{
"success": false,
"error": "unserviceable_delivery",
"message": "One or more products in the cart can't ship to Zaria, Kaduna. Call POST /api/commerce/delivery/quote/ first to see which.",
"warnings": ["Scheme 'Lagos Metro Only' does not ship to this area."]
}
Invalid delivery location (422): bad state_id / state_code / lga_id. The response carries a field hint pointing at the offending key.
{
"success": false,
"error": "invalid_delivery_location",
"field": "state_code",
"message": "state_code 'XYZ' is not recognised"
}
Insufficient balance (422):
{
"success": false,
"error": "insufficient_balance",
"message": "Wallet balance is below the order total.",
"required": 29000,
"balance": 12500
}
Get an order
Fetch one of your own orders. Another merchant's reference returns 404 not_found.
List orders
Paginated list of YOUR orders, newest first.
| Param | Type | Notes | |
|---|---|---|---|
| status | string | optional | paid | accepted | shipped | delivered | cancelled |
| payment_state | string | optional | paid | refunded | unpaid |
| from | YYYY-MM-DD | optional | created_at ≥ this date 00:00:00 |
| to | YYYY-MM-DD | optional | created_at ≤ this date 23:59:59 |
| q | string | optional | Matches reference, customer_name, customer_phone. |
| limit | int | optional | Default 20, max 100. |
| offset | int | optional | Default 0. |
curl "https://amigo.ng/api/commerce/orders/?status=shipped&limit=10" \ -H "X-API-Key: $TOKEN"
Register a webhook endpoint
Register a URL that should receive event POSTs. The plaintext signing secret is returned once in this response — store it then; later GETs will never include it.
{
"url": "https://your-app.com/amigo/webhook", // required, http(s), ≤ 512 chars
"events": "*" // "*" all, "order.*" prefix, or "order.shipped,order.delivered"
}
{
"success": true,
"data": {
"id": 12,
"url": "https://your-app.com/amigo/webhook",
"events": "*",
"status": "active",
"secret": "a3f1…64-char hex…" // ← save this NOW
},
"message": "Save this secret now — it will not be shown again."
}
List, update, delete
List your registered endpoints (no secrets). Pass ?id=N for one endpoint plus its 20 most recent delivery attempts (status, attempts, response code, scheduled retry).
Update url, events, or status (active/disabled). Disabled endpoints stop receiving new events but keep their delivery history.
Permanently remove the endpoint and its delivery history (FK cascade).
Event reference
| Event | When | Payload data |
|---|---|---|
| order.created | Order was placed and your wallet debited. | Full order object |
| order.accepted | Order entered fulfillment. | Full order object |
| order.shipped | Order shipped to the customer. | Full order object |
| order.delivered | Order was delivered. | Full order object |
| order.cancelled | Order was cancelled and your wallet refunded. | Full order object incl. cancelled_reason |
Wire format:
POST https://your-app.com/amigo/webhook HTTP/1.1
Host: your-app.com
Content-Type: application/json
User-Agent: Amigo-Commerce-Webhook/1.0
X-Amigo-Event: order.shipped
X-Amigo-Delivery: 4831
X-Amigo-Timestamp: 1747451291
X-Amigo-Signature: sha256=<hex>
{
"event": "order.shipped",
"livemode": true,
"created_at": "2026-05-17T03:48:11+01:00",
"data": { …full order… }
}
Verify signatures
Every request includes X-Amigo-Timestamp (unix seconds) and X-Amigo-Signature: sha256=<hex>, computed as hmac_sha256(secret, "$ts.$body"). Verify both — anyone could POST to your URL, and the timestamp keeps an attacker from replaying a captured webhook forever.
Reject any request whose timestamp is more than ~5 minutes in the past.
<?php
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_AMIGO_SIGNATURE'] ?? '';
$ts = (int) ($_SERVER['HTTP_X_AMIGO_TIMESTAMP'] ?? 0);
$secret = getenv('AMIGO_WEBHOOK_SECRET'); // the one returned at register-time
// Reject stale timestamps (> 5 min skew) — kills replay attacks.
if (abs(time() - $ts) > 300) {
http_response_code(401);
exit('stale timestamp');
}
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('bad signature');
}
$event = json_decode($body, true);
// $event['event'] → "order.shipped"
// $event['data'] → full order object
// $event['created_at'] → ISO 8601
const crypto = require('crypto');
app.post('/amigo/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const body = req.body; // Buffer — verify against bytes!
const sig = req.get('X-Amigo-Signature') || '';
const ts = parseInt(req.get('X-Amigo-Timestamp') || '0', 10);
const secret = process.env.AMIGO_WEBHOOK_SECRET;
// Reject stale timestamps (> 5 min skew).
if (Math.abs(Date.now() / 1000 - ts) > 300) {
return res.status(401).send('stale timestamp');
}
const signed = ts + '.' + body.toString();
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(signed).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send('bad signature');
}
const event = JSON.parse(body.toString());
// handle event.event …
res.status(200).send('ok');
});
import hmac, hashlib, time
@app.route('/amigo/webhook', methods=['POST'])
def webhook():
body = request.get_data() # raw bytes, NOT request.json
sig = request.headers.get('X-Amigo-Signature', '')
ts = int(request.headers.get('X-Amigo-Timestamp', '0'))
secret = os.environ['AMIGO_WEBHOOK_SECRET'].encode()
if abs(time.time() - ts) > 300:
return 'stale timestamp', 401
signed = f"{ts}.".encode() + body
expected = 'sha256=' + hmac.new(secret, signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return 'bad signature', 401
event = json.loads(body)
# handle event['event'] …
return 'ok', 200
Retry behaviour
If your endpoint doesn't return a 2xx within 4 seconds, the delivery is queued for retry. The schedule (in seconds since the previous attempt):
- Attempt 1 → +60s
- Attempt 2 → +300s (5m)
- Attempt 3 → +1800s (30m)
- Attempt 4 → +7200s (2h)
- Attempt 5 → +43200s (12h)
- Attempt 6 → +86400s (24h)
- Attempt 7 → marked
exhausted, no further attempts
The X-Amigo-Delivery header is the immutable id of that delivery — use it to dedupe. Two requests with the same delivery id are the same event being retried; idempotent processing on your side is essential.
order.created event will land within seconds.