Integration guide
Connect your shipping system to OneShip: receive shipments and order events in real time, and push delivery status back.
1 How it works
A OneShip integration works in two directions, both scoped to one shipping app:
2 Set up your app
- Create a shipping app (Shipping Apps โ Create application).
- Open the app โ Coverage, and link the countries and cities you deliver to.
- Rates: add a price for each route and weight band.
- Events & Webhooks: subscribe your listener URL to the events you need โ at least shipment.create.
- Keep the Client ID from the app overview for status updates. OneShip activates the app after review.
3 Receive webhooks
Deliveries are HTTP POST requests with a JSON body and these headers:
| Header | Meaning |
|---|---|
| X-Webhook-Signature | HMAC-SHA256 hex digest of the raw body, keyed with the subscription's signing secret. |
| X-Webhook-Event | The event code, e.g. shipment.tracked โ use it to route the payload and pick the right secret. |
| X-Webhook-Timestamp | Unix time of the delivery (sent on queued deliveries). |
| X-Webhook-Id | Unique delivery id โ use it to ignore duplicates (sent on queued deliveries). |
Example body of a shipment status change:
{
"shipment_id": 18452,
"one_ship_number": "OS-2026-000123",
"status": "transferred",
"old_status": "prepared",
"history": {
"status": "transferred",
"notes": "Status updated by carrier",
"event_at": "2026-11-08T10:15:00+03:00"
}
}- Answer with any 2xx status within 10 seconds. Do heavy work asynchronously after acknowledging.
- Use an https:// listener; payloads contain shipment and customer details.
- The same event can be delivered more than once (retries). Make your handler idempotent, e.g. by one_ship_number + status.
- Always read the event type from the X-Webhook-Event header, not from the body.
4 Verify signatures
Never trust a payload before checking its signature. Each event subscription has its own secret (app โ Events & Webhooks โ ๐). Compute the HMAC of the exact raw body and compare in constant time:
const express = require('express');
const crypto = require('crypto');
const app = express();
// one secret per subscribed event ( copy them from the app โ Events & webhooks tab )
const SECRETS = {
'shipment.create': process.env.ONESHIP_SECRET_SHIPMENT_CREATE,
'shipment.tracked': process.env.ONESHIP_SECRET_SHIPMENT_TRACKED,
};
// keep the raw body : the signature is computed over the exact bytes received
app.post('/oneship/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const event = req.get('X-Webhook-Event');
const secret = SECRETS[event];
if (!secret) return res.sendStatus(400);
const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
const received = req.get('X-Webhook-Signature') || '';
const valid = received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
if (!valid) return res.sendStatus(401);
res.sendStatus(200); // acknowledge fast โฆ
handleEvent(event, JSON.parse(req.body)); // โฆ then process asynchronously
});5 Events catalog
Subscribe per app and per event. Subscriptions can be paused without losing their URL and secret.
| Event | Sent when |
|---|---|
6 Send status updates
Push every status change of a OneShip shipment from your system. OneShip validates the transition, updates tracking for the merchant and customer, and notifies subscribers.
POST https://stg.api.oneshipapp.com/api/webhooks/shipment-status-updateAuthorization: Bearer the app token issued by OneShip.
status required โ the new status (see the mapping below).
external_policy_number optional โ your waybill / policy number.
tracking_url optional โ public tracking link on your side.
curl -X POST "https://stg.api.oneshipapp.com/api/webhooks/shipment-status-update" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-Client-Id: ca_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Authorization: Bearer <APP_TOKEN>" \
-d '{
"one_ship_number": "OS-2026-000123",
"status": "delivered",
"external_policy_number": "AWB-998877",
"tracking_url": "https://track.your-company.com/AWB-998877"
}'Status mapping
| You send | OneShip status |
|---|---|
| pending | New (new) |
| confirmed | Processing (processing) |
| picked_up | In preparation (in_preparation) |
| in_transit | With the carrier (transferred) |
| out_for_delivery | With the carrier (transferred) |
| delivered | Delivered (success) |
| returned | Returned (returned) |
| failed | Returned (returned) |
| cancelled | Cancelled (cancelled) |
Statuses must move forward through the lifecycle; an invalid jump is rejected with 400. Sending returned for a delivered shipment opens a return shipment (RE-โฆ) automatically.
// 200 โ applied
{ "status": true, "message": "Shipment status updated successfully" }
// 400 โ rejected ( unknown shipment , shipment of another app , unknown status , invalid transition )
{ "status": false, "message": "Invalid status transition" }
// 401 โ missing / wrong X-Client-Id or token
{ "status": false, "message": "Invalid or missing client credentials" }
// 422 โ validation ( one_ship_number / status missing )
{ "message": "The status field is required.", "errors": { "status": ["The status field is required."] } }7 Troubleshooting
- Webhook Logs shows every delivery with the payload, your response and the duration โ start there.
- Signature mismatch: hash the raw body (not re-serialised JSON) and use the secret of that event's subscription.
- Timeouts: acknowledge first, then process; deliveries are cut after 10 seconds.
- No deliveries: check the subscription is active and the app is activated by OneShip.
- 401 on status updates: check the X-Client-Id header and the Bearer token belong to the same app.