Developers

Connect your own store to Shiplayer.

Your store does not run on Shopify, WooCommerce or Magento? Our connectors use nothing you cannot use yourself: a REST API, one key, five calls. This guide is the shortest path, with the requests ready to copy.

1The orderYour system sends it as soon as it is placed.POST /orders
2The ratesEvery carrier, its price and its expected landed cost.POST /orders/{id}/quotes
3The shipmentA carrier, or “auto”. The parcel exists.POST /orders/{id}/ship
4The labelA PDF to print. The driver comes.GET /shipments/{ref}/label.pdf
5TrackingPushed to you by webhook, or read whenever you like.GET /shipments/{ref}
Authentication

One key, one header.

The merchant creates the key himself, in his dashboard: Settings → API keys → Create a key. It is shown once. A “shp_live_…” key creates real parcels; a “shp_test_…” key is for development. Send it in the Authorization header of every request.

Base URL     https://shiplayer.online
Content-Type application/json
Authorization: Bearer shp_live_xxxxxxxxxxxxxxxxxxxxxxxx

# Vérifiez la clé : votre compte marchand
curl https://shiplayer.online/merchants/me \
  -H "Authorization: Bearer $SHIPLAYER_KEY"

Every response is JSON. Amounts are strings in dirhams (“38.08”), never floats. Dates are UTC, ISO 8601. The limit is about 10 requests per second per key: beyond it you get 429, retry after a second.

The full reference of every route, generated from the code: shiplayer.online/docs.

Step 1

Create the order.

Send the order as soon as it is placed in your store. It lands in the merchant's “Orders” inbox with its risk score and the WhatsApp link to confirm it with the customer — shipping is decided afterwards, by you or by him.

curl -X POST https://shiplayer.online/orders \
  -H "Authorization: Bearer $SHIPLAYER_KEY" -H "Content-Type: application/json" \
  -d '{
    "order_id": "CMD-10482",
    "origin": {
      "raw": "12 rue Ibnou Mounir, Maarif", "city": "Casablanca",
      "phone": "0661000000", "recipient_name": "Boutique Atlas"
    },
    "destination": {
      "raw": "Résidence Yasmine, imm. B, apt 4, Guéliz", "city": "Marrakech",
      "phone": "0662000000", "recipient_name": "Salma Bennani"
    },
    "parcel": { "weight_kg": "0.8" },
    "payment_mode": "cod",
    "cod_amount_mad": "450",
    "declared_value_mad": "450",
    "ready_at": "2026-10-14T09:00:00",
    "contents": "Caftan",
    "notes": "Appeler avant livraison"
  }'

201 Created
{
  "order_id": "CMD-10482",
  "recipient": "Salma Bennani", "city": "Marrakech", "zone": "metro",
  "cod_mad": "450", "needs_address_review": false,
  "risk": { "score": 34, "band": "medium", "action": "confirm_first", ... },
  "whatsapp_url": "https://wa.me/212662000000?text=...",
  ...
}
FieldRequiredNotes
order_idyesYour order number, 64 characters max. It must be unique: sending it again answers 409, which makes your retries safe.
origin, destinationyesraw (the free-text address), city, phone, recipient_name. No zone to supply: we infer it from the city. Spell the city the way carriers know it (“Missour”, not “Mayssour”).
parcel.weight_kgyesGreater than 0. Some carriers do not take parcels that are too light or too heavy for some cities: they then simply do not appear in the rates.
payment_modeyescod (cash on delivery, in which case cod_amount_mad is required) or prepaid.
declared_value_madyesThe value of the goods, the basis for compensation and insurance.
ready_atyesWhen the parcel will be ready for pickup.
contents, notesnoThe contents are printed on the label; the note is passed to the carrier.
exchange_contentsnoAn exchange: what the driver takes back from the customer during the same visit.
Step 2

Compare rates.

Optional if you let Shiplayer choose. Otherwise this call lists every carrier serving the city, with its price and its expected landed cost — the price plus the failure risk and the cash tied up, the method explained on the home page. Carriers that do not serve the city are listed in not_offered, with the reason.

curl -X POST https://shiplayer.online/orders/CMD-10482/quotes \
  -H "Authorization: Bearer $SHIPLAYER_KEY"

200 OK
{
  "recommended": { "carrier_code": "relaisexpress", "price_mad": "30.00", ... },
  "candidates": [
    { "carrier_code": "relaisexpress", "price_mad": "30.00",
      "expected_landed_cost_mad": "48.10", "promised_transit_hours": 48,
      "rate_basis": "contract", "insurance_offer_fee_mad": "6.75", ... },
    { "carrier_code": "sendit", "price_mad": "45.00", ... }
  ],
  "not_offered": [
    { "carrier_code": "digylog", "reason": "Digylog n'accepte pas les colis de moins de 1.2 kg." }
  ]
}
Step 3

Ship.

Name a carrier, or leave “auto”: Shiplayer picks the lowest landed cost according to the merchant's setting. The answer is the full parcel — keep shiplayer_ref, the reference for everything else, and carrier_tracking_ref, the carrier's own number. The tracking link to give the customer is shiplayer.online/t/{shiplayer_ref}.

curl -X POST https://shiplayer.online/orders/CMD-10482/ship \
  -H "Authorization: Bearer $SHIPLAYER_KEY" -H "Content-Type: application/json" \
  -d '{ "carrier_code": "auto", "insured": false }'

200 OK
{
  "shiplayer_ref": "SHP8B064DE84C28",
  "order_id": "CMD-10482",
  "status": "created",
  "total_price_mad": "30.00",
  "legs": [ { "carrier_code": "relaisexpress", "carrier_tracking_ref": "RE123456789",
              "from_city": "Casablanca", "to_city": "Marrakech", "price_mad": "30.00",
              "promised_transit_hours": 48, ... } ],
  "events": [ { "at": "2026-10-13T14:18:44Z", "status": "created",
                "description": "Expédition créée, étiquette prête" } ],
  "pickup": { "stated": true, "date": "2026-10-14", "when": "demain avant 12 h", ... },
  "cancellable": true,
  ...
}
In a single call. If your system decides on its own, POST /shipments takes the order (order) and the carrier (carrier_code) in the same body and creates the parcel directly. Send a unique Idempotency-Key header per order: a retry after a timeout returns the parcel already created instead of buying a second one.
Validated account. Creating an order and asking for rates work from signup. Shipping needs an account validated by Shiplayer (file and contract): before that, the answer is 409 and its message says what is missing.
Step 4

The label and the pickup.

The label is a PDF in the carrier's format, with the logo and the contents. Print it and stick it on the parcel. Pickups are managed in the merchant's dashboard (Shipments → Pickups): he sees the driver's time slot there and confirms the handover. Cancelling is possible as long as the parcel has not been picked up.

# L'étiquette (PDF)
curl https://shiplayer.online/shipments/SHP8B064DE84C28/label.pdf \
  -H "Authorization: Bearer $SHIPLAYER_KEY" -o etiquette.pdf

# Plusieurs étiquettes en un PDF
curl "https://shiplayer.online/labels.pdf?ref=SHP8B064DE84C28&ref=SHP2F19C0A7E415" \
  -H "Authorization: Bearer $SHIPLAYER_KEY" -o etiquettes.pdf

# Annuler avant ramassage
curl -X POST https://shiplayer.online/shipments/SHP8B064DE84C28/cancel \
  -H "Authorization: Bearer $SHIPLAYER_KEY"
Step 5

Track the parcel.

The best way is the webhook below: every change reaches you without asking. Otherwise read the parcel whenever you like; the answer is the same as at shipping time, with the full history in events. The public page /t/{ref} and its JSON /track/{ref} need no key and show nothing confidential: you can give them to the customer.

curl https://shiplayer.online/shipments/SHP8B064DE84C28 \
  -H "Authorization: Bearer $SHIPLAYER_KEY"

200 OK
{
  "shiplayer_ref": "SHP8B064DE84C28", "status": "delivered",
  "delivered_at": "2026-10-16T11:02:00Z",
  "events": [
    { "at": "2026-10-13T14:18:44Z", "status": "created",          "description": "Expédition créée, étiquette prête" },
    { "at": "2026-10-14T10:40:12Z", "status": "carrier_received", "description": "Pris en charge", "location": "Casablanca" },
    { "at": "2026-10-15T07:12:30Z", "status": "in_transit",       "location": "Marrakech" },
    { "at": "2026-10-16T08:05:00Z", "status": "out_for_delivery" },
    { "at": "2026-10-16T11:02:00Z", "status": "delivered" }
  ],
  "pod": { "recipient_name": "Salma Bennani", "recorded_at": "2026-10-16T11:02:00Z", ... },
  ...
}

# Sans clé, pour le client
curl https://shiplayer.online/track/SHP8B064DE84C28
Webhooks

Be told, don't ask.

The merchant enters your system's address in Settings → Webhook and gets a secret, shown once. From then on Shiplayer POSTs JSON to that address on every parcel creation and every status change. The body is an envelope whose data is exactly the GET /shipments/{ref} object, plus tracking_url: there is only one format to learn.

EventWhen
shipment.createdThe parcel has just been created: it has its reference, its carrier and its label.
shipment.updatedThe parcel's status changed (received by the carrier, in transit, delivered, failed, returned, cancelled…). Once per change, not on every scan.
pingThe dashboard's “Test” button. Answer 200 and that is all.
POST https://ma-boutique.ma/webhooks/shiplayer
Content-Type: application/json
User-Agent: Shiplayer-Webhooks/1.0
Shiplayer-Event: shipment.updated
Shiplayer-Delivery: whd_3f9c2a7b1e4d5a6c8b0f
Shiplayer-Signature: t=1760364004,v1=5c1a0f…e2b9

{
  "id": "whd_3f9c2a7b1e4d5a6c8b0f",
  "event": "shipment.updated",
  "created_at": "2026-10-16T11:02:04Z",
  "merchant_id": "mrc_atlas",
  "data": {
    "shiplayer_ref": "SHP8B064DE84C28",
    "order_id": "CMD-10482",
    "status": "delivered",
    "tracking_url": "https://shiplayer.online/t/SHP8B064DE84C28",
    "legs": [ ... ], "events": [ ... ], "pod": { ... },
    ...
  }
}

Verifying the signature

Every delivery is signed with the merchant's secret: v1 = HMAC-SHA256(secret, "{t}.{body}"), in hex. Recompute it over the body exactly as received, byte for byte, before parsing it; compare in constant time; refuse a t more than five minutes from your clock. A delivery whose signature does not match did not come from Shiplayer.

import hmac, hashlib, time

def verify(secret: str, header: str, body: bytes) -> bool:
    fields = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = int(fields["t"]), fields["v1"]
    if abs(time.time() - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

# Flask
@app.post("/webhooks/shiplayer")
def shiplayer():
    if not verify(SECRET, request.headers["Shiplayer-Signature"], request.get_data()):
        abort(401)
    msg = request.get_json()
    if msg["event"] == "shipment.updated":
        update_order(msg["data"]["order_id"], msg["data"]["status"], msg["data"]["tracking_url"])
    return "", 200

Reliability

Answer 2xx within ten seconds, then process. Any other answer, or no answer, is retried: after 1, 5 and 15 minutes, then 1, 3, 6, 12 and 24 hours — eight attempts over a day. The same message can therefore arrive twice: keep its id and ignore a duplicate. Order is not guaranteed between two close messages: trust the status and the events in data, not the arrival order. The merchant sees every delivery, its answer and its next attempt in Settings → Webhook → Delivery log.

The address must be https, on a public server. During development a tunnel (ngrok, Cloudflare Tunnel) to your machine does the job; the “Test” button sends a ping right away and shows your server's answer.

Statuses

One vocabulary, for every carrier.

Every carrier has its own codes; Shiplayer maps them all onto this list. The original code stays in each event's raw_carrier_code if you need it.

statusMeaning
createdParcel created, label ready, waiting for pickup.
picked_upThe merchant confirmed the handover to the driver.
carrier_receivedThe carrier scanned the parcel: it is in its network.
in_transitOn its way to the destination city.
out_for_deliveryWith the driver, delivery today.
attempt_failedAttempt failed (absent, unreachable, refused). The merchant decides in his dashboard: new attempt or return.
deliveredDelivered. Final. The cash on delivery is now owed by the carrier; pod carries the proof when it provides one.
returnedReturned to the merchant. Final.
cancelledCancelled before pickup: nothing left the shop. Final.
lostLost, declared by the carrier. Final; compensation applies.
Errors

What the codes mean.

An error body is always an object with a detail key: a sentence in French, or for 422 the list of refused fields with their location.

401Key missing, unknown or revoked.
404The order or parcel does not exist — or belongs to another merchant, which is the same answer.
409Conflict: order_id already used, order already shipped, account not yet validated, Idempotency-Key reused with a different body. The message says which.
422Invalid body: missing field, too long, negative amount, city served by no carrier.
429Too many requests: wait a second and retry.
5xxShiplayer or the carrier did not answer. Nothing was created: retry, with the same Idempotency-Key if you use POST /shipments.
A question, a case that is not here? Write to us with the shiplayer_ref or order_id concerned: contact@shiplayer.online. The full reference: /docs.