Developers

Available now

GHAYA API

Get rates, create shipments, print labels and receive tracking updates — straight from your store or system.

Getting started

1

Create a test key

In the merchant portal → Developers & API. Test keys work right away.

2

Build & test

Test shipments are practice ones — a practice label and statuses that move by themselves; nothing is charged and no courier comes.

3

Go live

Verify your business, create a live key and swap the key — same URL, same code.

Base URL

https://os.ghayaex.com/v1/api

The same URL for test and live — the key decides.

Authentication

Send your key in the Authorization header on every request. Each key only has the access you chose when creating it (for example: shipments read & write, cities read).

HTTP
Authorization: Bearer gx_test_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Idempotency-Key: order-10045   # POST only — safe retries
Content-Type: application/json
  • Keep keys on your server only — never in a website, app or public repository.
  • Use a separate key per system so you can revoke one without affecting the others.
  • Optionally lock a key to your server IPs and give it an expiry date.
  • Roll a key at any time from the portal; the old one stops immediately.

Endpoints

GET/ping—

Check that a key works and see its mode (test or live).

Response
{ "ok": true, "mode": "test", "merchant": { "id": "…", "name": "My Store" } }
POST/ratesshipments:read

Your prices from every carrier (cheapest first) before booking. Nothing is booked or charged.

Request
{
  "parcel": { "weightKg": 2 },
  "cod": { "amount": 249 },
  "receiverCity": "Jeddah"
}
Response
{
  "object": "list",
  "data": [
    { "carrier": "smsa", "name": { "ar": "سمسا", "en": "SMSA" }, "price": { "net": 22, "vat": 3.3, "total": 25.3, "currency": "SAR" } },
    { "carrier": "aramex", "name": { "ar": "أرامكس", "en": "Aramex" }, "price": { "net": 24, "vat": 3.6, "total": 27.6, "currency": "SAR" } }
  ]
}
POST/shipmentsshipments:write

Book a shipment and get the AWB, the price and the label link. The sender is your pickup address. Send an Idempotency-Key so a retry never books twice.

Request
{
  "carrier": "aramex",
  "reference": "order-10045",
  "receiver": { "name": "Noura", "phone": "0551234567", "city": "Jeddah", "line1": "Al Andalus, Tahlia St 12" },
  "parcel": { "weightKg": 2, "pieces": 1, "description": "Perfume" },
  "cod": { "amount": 249 }
}
Response
{
  "id": "6f0c2a1e-…",
  "object": "shipment",
  "mode": "live",
  "awb": "44012345678",
  "carrier": "aramex",
  "status": "created",
  "reference": "order-10045",
  "cod": { "amount": 249, "currency": "SAR" },
  "price": { "net": 24, "vat": 3.6, "total": 27.6, "currency": "SAR" },
  "labelUrl": "https://os.ghayaex.com/v1/api/shipments/6f0c2a1e-…/label",
  "createdAt": "2026-10-27T09:41:12.000Z",
  "events": [ { "status": "created", "description": "Booked with carrier", "at": "2026-10-27T09:41:14.000Z" } ]
}
GET/shipments/{id | awb}shipments:read

Current status and full tracking history, by our id or the AWB.

Response
{
  "awb": "44012345678",
  "status": "out_for_delivery",
  "events": [
    { "status": "out_for_delivery", "location": "Jeddah", "at": "2026-10-28T08:30:00.000Z" },
    { "status": "in_transit", "location": "Jeddah hub", "at": "2026-10-28T06:15:00.000Z" },
    { "status": "picked_up", "location": "Riyadh", "at": "2026-10-27T13:02:00.000Z" }
  ],
  …
}
GET/shipments?status=delivered&page=1&pageSize=25shipments:read

Your shipments, newest first, filtered by status, reference or date.

Response
{ "object": "list", "data": [ … ], "page": 1, "pageSize": 25, "total": 1, "hasMore": false }
GET/shipments/{id}/labelshipments:read

Download the label as a print-ready PDF.

Response
%PDF-1.7 … (application/pdf)
POST/shipments/{id}/cancelshipments:write

Cancel before pickup; the price returns to your wallet.

Response
{ "id": "6f0c2a1e-…", "status": "canceled", … }
GET/carriersshipping-companies:read

Carriers your account can book.

Response
{ "object": "list", "data": [ { "code": "aramex", "name": { "ar": "أرامكس", "en": "Aramex" }, "cod": true, "cancelable": false } ] }
GET/cities?q=jedcities:read

Cities in Arabic and English, optionally only those a carrier serves.

Response
{ "object": "list", "data": [ { "name": { "en": "Jeddah", "ar": "جدة" } } ], "total": 1 }
GET/accountwallet:read

Wallet and cash-on-delivery balances.

Response
{ "object": "account", "wallet": { "balance": 512.4, "currency": "SAR" }, "cod": { "balance": 1890, "currency": "SAR" } }

Webhooks

Add an HTTPS endpoint in the portal and choose the events you want. We send a POST with a JSON body, signed with your endpoint’s secret.

shipment.createdshipment.status_changedshipment.deliveredshipment.returnedshipment.canceledcod.settledwallet.low_balance
Example delivery
POST https://your-store.com/webhooks/ghaya
X-GHAYA-Event: shipment.delivered
X-GHAYA-Delivery: evt_3f9c…
X-GHAYA-Signature: t=1761559200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
  "id": "evt_3f9c…",
  "object": "event",
  "type": "shipment.delivered",
  "mode": "live",
  "created": "2026-10-28T10:04:00.000Z",
  "data": { "id": "6f0c2a1e-…", "awb": "44012345678", "reference": "order-10045", "status": "delivered", "previousStatus": "out_for_delivery", … }
}
Verify the signature
import crypto from 'node:crypto'

// rawBody = the exact request body as received (before JSON.parse)
export function verifyGhaya(rawBody, signatureHeader, secret) {
  const p = Object.fromEntries(signatureHeader.split(',').map((x) => x.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false // older than 5 min
  const expected = crypto.createHmac('sha256', secret).update(`${p.t}.${rawBody}`).digest('hex')
  return expected.length === p.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1))
}
  • Always verify X-GHAYA-Signature and reject anything older than 5 minutes.
  • Answer with any 2xx within 10 seconds; do slow work afterwards.
  • Failed deliveries are retried 6 times over about 21 hours with the same event id (X-GHAYA-Delivery) — use it to de-duplicate.
  • Test endpoints receive test-key events only; live endpoints receive live events only.

Errors

Errors use standard HTTP status codes and a JSON body: { "error": { "code": "invalid", "message": "…", "fields": ["receiver.phone"] }, "requestId": "req_…" }. Every answer carries X-Request-Id — quote it to support.

400invalid · invalid_weight · bad_json
401unauthorized · key_revoked · key_expired
402insufficient_balance · outstanding_balance
403forbidden_scope · ip_not_allowed
404not_found · shipment_not_found
409no_rate · not_cancelable · cancel_not_supported
422carrier_rejected
429rate_limited
500internal

Limits & best practice

  • Test: 60 requests per minute · Live: 600 per minute per key (a 429 includes Retry-After; every answer has X-RateLimit-Remaining).
  • Optional hourly cap per resource, set on the key.
  • Lists are paginated with page and pageSize (max 100).
  • All times are UTC (ISO 8601); amounts are in SAR.

Need a hand with your integration? Contact