DevelopersRapidXpk Integration API
Send delivery orders from your store or system, follow every parcel to the door, and get told the moment its status changes.
Overview
Every request goes to https://api.rapidxpk.com/v1/… with your key in a header. You send each order with your own reference in external_id. RapidXpk answers with its order number (RXK-123456) and a public tracking link you can share with your customer.
Sending the same external_id twice never makes a second parcel: you get the first order back. That makes retries after a timeout safe.
curl https://api.rapidxpk.com/v1/health
Keys and auth
Send your key in the X-API-Key header on every call. RapidXpk issues two kinds of key when you are onboarded:
| Key | What happens to orders |
|---|---|
rxp_test_… | Accepted and visible to RapidXpk, never dispatched. Use it while you build. |
rxp_live_… | Real orders: picked up, delivered and billed. |
Keep keys on your server. Never put them in a website or mobile app. If a key leaks, ask RapidXpk to revoke it and issue a new one.
Order statuses
| Status | Meaning |
|---|---|
received | We have the order. RapidXpk confirms it next. |
packed | Confirmed and ready for a rider. |
dispatched | Out for delivery with a rider. |
delivered | Handed to the customer. |
failed_attempt | The rider reached the door and could not deliver. reason says why. RapidXpk tries again or returns the parcel. |
rto | Return to origin: the parcel is coming back to you. |
returned | The parcel is back with us or with you. |
cancelled | Cancelled before the rider took it. |
Charges follow attempts at the door. An order cancelled before it went out costs nothing.
Create an order
201 when created, 200 when that external_id already exists.| Field | Type | Notes |
|---|---|---|
external_idrequired | string | Your order reference. Unique per account; makes retries safe. |
customer.namerequired | string | |
customer.phonerequired | string | Pakistani mobile. 03XXXXXXXXX, +92… and 92… are accepted. |
customer.addressrequired | string | Full address. customer.area is accepted instead. |
customer.lat, customer.lng | number | Optional location hint. RapidXpk still confirms the exact door pin before dispatch. |
items | array | [{ "sku", "name", "qty" }]. Quantities are added up; several items are listed by name on the parcel. |
payment_method | string | cod (default) or prepaid. |
cod_amount | number | Rupees the rider collects. Ignored for prepaid. |
order_value | number | Goods value. Used as the COD amount when cod_amount is missing. |
notes | string | Shown to the rider: gate code, landmark, call first. |
service | string | Optional service name agreed with RapidXpk. |
curl -X POST https://api.rapidxpk.com/v1/orders \ -H "X-API-Key: rxp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "external_id": "SHOP-10482", "customer": { "name": "Sana Malik", "phone": "03001234567", "address": "House 12, Street 5, DHA Phase 6, Karachi" }, "items": [{ "sku": "VC-30", "name": "Vitamin C Serum 30ml", "qty": 2 }], "payment_method": "cod", "cod_amount": 4900, "notes": "Call on arrival" }'
{
"order_id": "RXK-482915",
"external_id": "SHOP-10482",
"status": "received",
"mode": "live",
"tracking_url": "https://rapidxpk.com/track.html?tn=RXK-482915",
"created_at": "2026-10-22T07:15:02Z"
}Create in bulk
207 with one result per order, in the order you sent them.{ "orders": [ { "external_id": "SHOP-10483", … }, { "external_id": "SHOP-10484", … } ] }{
"count": 2,
"results": [
{ "index": 0, "ok": true, "result": { "order_id": "RXK-482916", … } },
{ "index": 1, "ok": false, "result": { "error": "invalid_phone", "field": "customer.phone", … } }
]
}Resend only the ones with "ok": false after fixing them. The others are already in.
Get one order
{
"order_id": "RXK-482915",
"external_id": "SHOP-10482",
"status": "failed_attempt",
"reason": "Customer not home",
"attempts": 1,
"packed_at": "2026-10-22T08:02:11Z",
"dispatched_at": "2026-10-22T10:40:05Z",
"delivered_at": null,
"returned_at": null,
"updated_at": "2026-10-22T13:21:47Z",
"cod": { "amount": 4900, "collected": false, "settled": false },
"tracking_url": "https://rapidxpk.com/track.html?tn=RXK-482915"
}cod.collected turns true when the rider's cash reaches RapidXpk. cod.settled turns true when it is paid out to you.
Sync changes
If you cannot receive webhooks, poll for every order that changed since your last call.
limit is 1 to 500 (default 200). Results come oldest change first.{
"orders": [ { /* same shape as Get one order */ } ],
"count": 200,
"next_since": "2026-10-22T09:14:03.551210Z",
"next_id": 48211,
"has_more": true
}Call again with updated_since=next_since and after_id=next_id while has_more is true. Save both values for your next poll. Every five minutes is plenty.
Cancel an order
{ "reason": "Customer changed mind" } is optional.| Answer | When |
|---|---|
200 "status": "cancelled" | Cancelled. Calling it again answers the same with "already": true. |
409 too_late | The rider already has the parcel. Call RapidXpk to recall it. |
404 not_found | No order with that reference on your account. |
Webhooks
Give us one public HTTPS address and we post every status change to it, normally within a few minutes.
{ "url": "https://yourshop.com/rapidxpk/hook" }. Answers 201 with a signing secret, shown only once. Setting a new URL replaces the old one and issues a new secret.The address must be https:// on port 443 and reachable from the internet. Local and private addresses are refused.
Events
order.received, order.packed, order.dispatched, order.delivered, order.failed_attempt, order.rto, order.returned, order.cancelled.
{
"event": "order.delivered",
"order_id": "RXK-482915",
"external_id": "SHOP-10482",
"status": "delivered",
"previous_status": "dispatched",
"reason": null,
"attempts": 2,
"occurred_at": "2026-10-23T12:05:40Z",
"packed_at": "2026-10-22T08:02:11Z",
"dispatched_at": "2026-10-23T10:12:09Z",
"delivered_at": "2026-10-23T12:05:40Z",
"returned_at": null,
"cod": { "amount": 4900, "collected": false, "settled": false },
"tracking_url": "https://rapidxpk.com/track.html?tn=RXK-482915"
}| Header | Value |
|---|---|
X-RapidXpk-Event | The event name, same as event in the body. |
X-RapidXpk-Delivery | A delivery number. A retry keeps the same number, so you can ignore repeats. |
X-RapidXpk-Timestamp | Unix seconds when we signed it. |
X-RapidXpk-Signature | sha256= followed by HMAC-SHA256 of timestamp + "." + raw body, keyed with your secret. |
Answer quickly
Reply with any 2xx within 12 seconds, then do your own work. Anything else counts as a failure and we try again after 1, 2, 4, 8 minutes and so on, up to every 6 hours, for 12 attempts in all. GET /v1/webhooks shows the last error, and you can always catch up with Sync changes.
Verify a delivery
Check the signature against the raw request body before you trust it, and refuse anything signed more than 5 minutes ago.
const crypto = require('crypto'); // rawBody: the exact bytes we sent, before JSON parsing function isFromRapidXpk(headers, rawBody, secret) { const ts = headers['x-rapidxpk-timestamp']; const sig = headers['x-rapidxpk-signature'] || ''; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(ts + '.' + rawBody).digest('hex'); return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); }
$raw = file_get_contents('php://input'); $ts = $_SERVER['HTTP_X_RAPIDXPK_TIMESTAMP'] ?? ''; $sig = $_SERVER['HTTP_X_RAPIDXPK_SIGNATURE'] ?? ''; $ok = abs(time() - (int)$ts) <= 300 && hash_equals('sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret), $sig); if (!$ok) { http_response_code(401); exit; } http_response_code(200); // then process $raw
Shopify
No code needed. Your RapidXpk portal gives you a personal webhook address under Connect orders → Shopify.
- In Shopify admin open Settings → Notifications → Webhooks and choose Create webhook.
- Event: Order creation. Format: JSON. Paste the address from your portal.
- Copy the signing secret Shopify shows under the webhooks list into the portal (Connect orders → Store settings). We use it to check every order really came from your store.
- Place a test order. It appears in the portal under Connect orders → Incoming orders, with product pictures, ready for RapidXpk to confirm.
Odoo 18
Odoo's built-in webhook action cannot send headers, so it uses an address with your key in the path:
Odoo sends only the sale order number. Save an Odoo API user in the portal (Connect orders → Odoo) and we read the full order from your Odoo and create the delivery. Without it the call is accepted with 202 and the answer explains the next step. Your RapidXpk contact has the full Odoo setup guide.
Errors
Every error has the same shape, and message is written to be shown to a person:
{ "error": "missing_field", "field": "customer.name", "message": "customer.name is required." }| HTTP | error | What to do |
|---|---|---|
| 400 | missing_field | Add the field named in field. |
| 400 | invalid_phone | Send an 11-digit Pakistani mobile, 03XXXXXXXXX. |
| 400 | invalid_field | qty must be a whole number; amounts and coordinates plain numbers. |
| 400 | invalid_url | Webhook address must be public HTTPS on port 443. |
| 400 | too_many | Bulk takes at most 100 orders per request. |
| 400 | invalid_payload | The body is not the expected JSON. Bulk needs { "orders": [ … ] }. |
| 400 | invalid_timestamp | updated_since must be ISO 8601, for example 2026-10-22T10:00:00Z. |
| 400 | missing_reference | Put an order number in the path or use ?external_id=. |
| 401 | missing_api_key, invalid_api_key | Check the X-API-Key header and that the key is not revoked. |
| 404 | not_found | No order with that reference on your account. |
| 405 | method_not_allowed | Use POST to create orders. |
| 409 | too_late | The rider has the parcel; it can no longer be cancelled here. |
| 429 | too_many_attempts | Too many badly formed keys from your address. Wait 10 minutes. |
| 501 | not_enabled | Stock levels are not switched on for your account. |
| 502 | upstream_error | Retry shortly. Creating is safe to retry with the same external_id. |
| 502 | odoo_unreachable | We could not read the order from your Odoo. Check the Odoo details saved in the portal. |
Limits
| What | Limit |
|---|---|
| Orders per bulk request | 100 |
| Results per sync page | 500 (default 200) |
| Webhook answer time | 12 seconds |
| Webhook retries | 12 attempts, backing off to every 6 hours |
| Badly formed keys | 30 from one address within 10 minutes blocks that address for 10 minutes |
| COD amount | Rs 0 to Rs 10,000,000 per order |
Stock levels (/v1/inventory) | Not switched on yet. Ask your RapidXpk contact. |