Quickstart
Sandbox key to live mode in four steps. Every step shows curl, Node, Python and PHP — copy the one that matches your stack.
Get your API key
Mint your own key from the dashboard — no admin email required. Sign in → open API & Webhooks → pick the environment (sandbox / live) → click New key.
- Sign in to the dashboard. Open API keys →
- Open API & Webhooks → New key.
- Pick the environment + label. The plaintext is shown EXACTLY ONCE — copy it into env / vault before closing the modal.
Key format: mk_test_* for sandbox · mk_live_* for live. Prefixes never cross. Max 5 active keys per account.
Create your first shipment
POST to /v1/shipments. Include an Idempotency-Key header — retries within 24h return the original response without creating a duplicate shipment.
curl https://merdiexpress.com/api/v1/shipments \ -H "Authorization: Bearer mk_test_rh9ABekJfP31cN7sQ8jKvLwY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "service_tier_id": "HKASTDGGSTCM", "recipient": { "contact_name": "John Doe", "phone": "+14155551234", "country_code": "US", "city": "San Francisco", "district": "CA", "address": "1 Hacker Way", "postal_code": "94025" }, "parcels": [{ "weight_kg": 0.5 }], "items": [{ "description": "Sample item", "quantity": 1, "unit_price_hkd": 100 }], "goods_category": "general", "payment_type": "prepaid" }'
Expected response:
{ "status": 201, "message": "Created", "data": { "id": "IM-2026-0528-9ZNCP3-1", "tracking_number": "IM-2026-0528-9ZNCP3-1", "status": "label_created", "service_tier_id": "HKASTDGGSTCM", "livemode": false, "created_at": "2026-05-28T07:44:23.908Z" } }
Verify a webhook
When you register a webhook you receive a one-time signing_secret. Every event we POST carries an X-Merdi-Signature header in the form t=<unix>,v1=<hex>.
import crypto from 'node:crypto'; export default function handler(req, res) { const raw = req.rawBody; // unparsed body const sig = req.headers['x-merdi-signature']; // "t=...,v1=..." const [tPart, v1Part] = sig.split(','); const ts = tPart.split('=')[1]; const sent = v1Part.split('=')[1]; const expected = crypto .createHmac('sha256', process.env.MERDI_WEBHOOK_SECRET) // whsec_... .update(`${ts}.${raw}`) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected))) { return res.status(400).send('invalid signature'); } res.status(200).send('ok'); }
See Webhooks guide for retry policy and event catalog.
Switch to live mode
Swap mk_test_ for mk_live_ — the URL doesn't change. Before flipping, run the dashboard's "last-call lint" to confirm every POST in the last 24h carried an Idempotency-Key.
- export MERDI_KEY="mk_test_rh9ABekJfP31cN7sQ8jKvLwY" + export MERDI_KEY="mk_live_8ZqAvR2nLkPyWxV4DfHcM6tB" # URL stays the same: https://merdiexpress.com/api/v1/shipments