01How it works
- 1
Your platform sends each new order to
POST /orders. - 2
Yudoo sends the WhatsApp confirmation from your own WhatsApp Business number, with Confirm and Cancel buttons, plus every automation you turned on for the store (follow-up, shipping update, review request…).
- 3
The customer taps Confirm or Cancel. Yudoo answers them on WhatsApp and sends
order.confirmedororder.cancelledto your webhook URL. - 4
Your platform moves the order (ship it, or cancel it). When it ships, PATCH the order with the tracking number and the Shipping update goes out.
The messages, buttons and timings are the store's automations in the Yudoo app, exactly as for a Shopify or Salla store: the API only brings the orders in and the answers out.
02Getting started
- 1
Sign in to the Yudoo app (app.yudoo.io) and connect your WhatsApp Business number.
- 2
Settings → Store platforms → Other platform (API): enter the store's name and URL, then Connect.
- 3
Copy the API key and the signing secret. The API key is shown only this once; keep both on your server, never in a browser or an app.
- 4
Turn on the automations you want (Cash on Delivery Confirmation first) under Automations.
- 5
Add your webhook URL on the store's card, and press Send a test event.
GET/api/v1/me
Check that your key works: it answers with the store it belongs to.
curl https://app.yudoo.io/api/v1/me -H "Authorization: Bearer wp_live_your_api_key"
{
"store": { "name": "My Store", "url": "https://mystore.com", "connected_at": "2026-11-04T09:00:00.000Z" },
"webhook": { "url": "https://mystore.com/yudoo/webhook", "events": ["order.confirmed", "order.cancelled"] }
} 03Authentication
Every request carries the store's API key in the Authorization header. X-API-Key: wp_live_… is accepted too.
Authorization: Bearer wp_live_your_api_key - One key per store. Make a new key from the store's card any time: the old one stops working at once.
- Requests go over HTTPS only. Answers are JSON.
- At most 120 requests a minute per store (429
rate_limitedafter that).
Never put the key in a storefront, a mobile app or client-side JavaScript: anyone who has it can send orders as your store.
04Create an order
POST/api/v1/orders
Send each order as soon as it is placed. Yudoo stores it and fires the store's order automations: the confirmation for its payment method first. Sending the same id again updates the order instead of creating a second one, so a retry is always safe.
curl -X POST https://app.yudoo.io/api/v1/orders \
-H "Authorization: Bearer wp_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"id": "1042",
"number": "1042",
"status": "pending",
"customer": { "name": "Mohamed Ali", "phone": "201001234567", "email": "[email protected]" },
"currency": "EGP",
"total": 540,
"shipping_total": 40,
"payment_method": "cod",
"shipping_address": { "address1": "12 Tahrir St", "city": "Cairo", "province": "Cairo", "country": "EG" },
"items": [
{ "name": "Cotton T-shirt", "variant": "White / L", "quantity": 1, "price": 250, "sku": "TS-WH-L" },
{ "name": "Sport socks", "quantity": 2, "price": 125 }
],
"note": "Please deliver after 5 pm"
}'const res = await fetch("https://app.yudoo.io/api/v1/orders", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WHATPRO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: order.id, // your platform's order id
number: order.number,
customer: { name: order.customerName, phone: order.phone }, // 201001234567
currency: "EGP",
total: order.total,
payment_method: "cod",
items: order.lines.map((l) => ({ name: l.title, quantity: l.qty, price: l.price })),
}),
});
const { order: saved, error } = await res.json();$ch = curl_init("https://app.yudoo.io/api/v1/orders");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("WHATPRO_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"id" => (string) $order->id,
"number" => $order->number,
"customer" => ["name" => $order->name, "phone" => $order->phone],
"currency" => "EGP",
"total" => $order->total,
"payment_method" => "cod",
"items" => array_map(fn($l) => ["name" => $l->title, "quantity" => $l->qty, "price" => $l->price], $order->lines),
]),
]);
$response = json_decode(curl_exec($ch), true);import os, requests
res = requests.post(
"https://app.yudoo.io/api/v1/orders",
headers={"Authorization": f"Bearer {os.environ['WHATPRO_API_KEY']}"},
json={
"id": str(order.id),
"number": order.number,
"customer": {"name": order.name, "phone": order.phone},
"currency": "EGP",
"total": order.total,
"payment_method": "cod",
"items": [{"name": l.title, "quantity": l.qty, "price": l.price} for l in order.lines],
},
timeout=10,
)
saved = res.json()["order"] | Field | Type | Description |
|---|---|---|
id | string | Required. Your platform's own order id. Sending the same id again updates that order. |
customer.phone | string | Required. International format with the country code, digits only (201001234567). The WhatsApp messages go here. |
customer.name | string | The customer's name ({{customer_first_name}} in the messages). |
customer.email | string | Optional. |
number | string | The number the customer knows ("1042" → #1042). Defaults to #id. |
status | string | pending (default), processing, shipped, delivered, cancelled… See Order statuses. |
payment_method | string | cod, bank_transfer, instapay, vodafone_cash, card… It picks the confirmation the order gets. See Payment methods. |
currency | string | 3-letter ISO code: EGP, SAR, AED, USD… |
total | number | The order's total, shipping included. |
shipping_total | number | The shipping fee. |
items[] | array | Up to 200 lines: name (required), quantity, price, variant, sku, product_id, image_url (https). |
shipping_address | object | address1, address2, city, province, postal_code, country (ISO-2 or name). |
note | string | The customer's order note. |
tracking | object | number, url, carrier — when the order already shipped. |
placed_at | string | ISO 8601 date. Defaults to the moment Yudoo received the order. |
The answer is 201 Created with the order (200 when the id was sent before):
{
"order": {
"id": "1042",
"number": "#1042",
"status": "pending",
"confirmation": { "status": "pending", "at": null, "cancel_reason": null },
"customer": { "name": "Mohamed Ali", "phone": "201001234567", "email": "[email protected]" },
"currency": "EGP",
"total": 540,
"payment_method": "cod",
"items": [
{ "name": "Cotton T-shirt", "variant": "White / L", "quantity": 1, "price": 250, "sku": "TS-WH-L", "product_id": null },
{ "name": "Sport socks", "variant": null, "quantity": 2, "price": 125, "sku": null, "product_id": null }
],
"tracking": null,
"created_at": "2026-11-04T10:00:00.000Z",
"updated_at": "2026-11-04T10:00:00.000Z"
}
} 05Get an order
GET/api/v1/orders/{id}
Read an order by your own id, with the customer's answer in confirmation.
curl https://app.yudoo.io/api/v1/orders/1042 \
-H "Authorization: Bearer wp_live_your_api_key" | confirmation.status | Meaning |
|---|---|
pending | No answer yet. |
confirmed | The customer (or you, in Yudoo) confirmed the order. |
cancelled | The customer (or you) cancelled it; cancel_reason holds the reason they chose, when they gave one. |
06Update an order
PATCH/api/v1/orders/{id}
Send only what changed: the status, the tracking, the items or the total. A new status fires the matching automation (Shipping update, Ready for pickup, Order cancelled…); a tracking number gives the Shipping update its link.
curl -X PATCH https://app.yudoo.io/api/v1/orders/1042 \
-H "Authorization: Bearer wp_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"status": "shipped",
"tracking": { "number": "7845120963", "url": "https://track.example.com/7845120963", "carrier": "Bosta" }
}' 07Order statuses and payment methods
Send your platform's own words: Yudoo reads them in English and Arabic. These are the ones it understands:
| Status | Examples Yudoo understands |
|---|---|
New | pending, new, placed, awaiting, unpaid |
Processing | processing, confirmed, preparing, packed, on hold |
Ready for pickup | ready for pickup, ready to collect |
Shipped | shipped, out for delivery, in transit, dispatched |
Delivered | delivered, completed, fulfilled |
Failed delivery | failed delivery, undelivered, customer unreachable |
Cancelled | cancelled, canceled, rejected, refunded |
Returned | returned, rto |
| payment_method | Confirmation sent |
|---|---|
cod, cash on delivery | Cash on Delivery Confirmation (Confirm / Cancel) |
bank_transfer, instapay, deposit | Bank Deposit Confirmation (send the payment proof) |
vodafone_cash, wallet, manual, pay later | Manual Payment Confirmation |
card, paid, anything else | Paid Order Confirmation |
08Webhooks
Add your webhook URL on the store's card in Yudoo (https only). Yudoo POSTs a JSON event to it whenever an order gets an answer:
| Event | When |
|---|---|
order.confirmed | The customer tapped Confirm, or you confirmed the order in Yudoo (data.by = customer / merchant). |
order.cancelled | The customer tapped Cancel, or you cancelled it in Yudoo. The reason, when given, is in data.order.confirmation.cancel_reason. |
webhook.test | You pressed Send a test event, or called POST /webhooks/test. |
POST https://mystore.com/yudoo/webhook
Content-Type: application/json
Yudoo-Event: order.cancelled
Yudoo-Delivery: evt_k3j9x2m1q8w7e6r5t4y3u2i1
Yudoo-Signature: t=1793782800,v1=5f2b8c…e91a
{
"id": "evt_k3j9x2m1q8w7e6r5t4y3u2i1",
"type": "order.cancelled",
"created_at": "2026-11-04T10:20:00.000Z",
"data": {
"by": "customer",
"order": {
"id": "1042",
"number": "#1042",
"status": "pending",
"confirmation": { "status": "cancelled", "at": "2026-11-04T10:19:58.000Z", "cancel_reason": "Changed my mind" },
"customer": { "name": "Mohamed Ali", "phone": "201001234567", "email": "[email protected]" },
"…": "the same order object as GET /orders/{id}"
}
}
} - Answer with any 2xx within 10 seconds. Do the slow work after answering.
- A delivery that fails is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h, then marked failed. The store's card shows the latest deliveries.
- The same event can arrive twice (a retry after a timeout): skip an id you already handled.
- Always check the signature before trusting an event.
POST/api/v1/webhooks/test
curl -X POST https://app.yudoo.io/api/v1/webhooks/test -H "Authorization: Bearer wp_live_your_api_key"
{ "delivery": { "id": "evt_…", "delivered": true, "error": null } } 09Signature verification
Every webhook carries a Yudoo-Signature header: t is the time it was sent (Unix seconds) and v1 the HMAC-SHA256 of "t.raw_body" with your signing secret, in hex.
Yudoo-Signature: t=1793782800,v1=5f2b8c0d…e91a - 1
Read the raw request body, before parsing the JSON.
- 2
Compute HMAC-SHA256(
signing_secret, t + "." +raw_body) in hex. - 3
Compare it with v1 in constant time, and refuse a t older than 5 minutes.
import crypto from "node:crypto";
import express from "express";
const app = express();
// Read the RAW body: the signature is over the exact bytes Yudoo sent.
app.post("/yudoo/webhook", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Yudoo-Signature") ?? "";
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", process.env.WHATPRO_SIGNING_SECRET)
.update(`${t}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!fresh || !v1 || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))) {
return res.status(400).send("bad signature");
}
const event = JSON.parse(req.body);
// event.type: "order.confirmed" | "order.cancelled" | "webhook.test"
res.sendStatus(200);
});$raw = file_get_contents("php://input");
$header = $_SERVER["HTTP_WHATPRO_SIGNATURE"] ?? "";
parse_str(str_replace(",", "&", $header), $sig); // ["t" => …, "v1" => …]
$expected = hash_hmac("sha256", $sig["t"] . "." . $raw, getenv("WHATPRO_SIGNING_SECRET"));
$fresh = abs(time() - (int) $sig["t"]) < 300;
if (!$fresh || !hash_equals($expected, $sig["v1"] ?? "")) {
http_response_code(400);
exit("bad signature");
}
$event = json_decode($raw, true);
http_response_code(200);import hmac, hashlib, os, time
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/yudoo/webhook")
def yudoo_webhook():
raw = request.get_data() # the raw bytes, before any JSON parsing
parts = dict(p.split("=", 1) for p in request.headers.get("Yudoo-Signature", "").split(","))
expected = hmac.new(
os.environ["WHATPRO_SIGNING_SECRET"].encode(),
f"{parts.get('t')}.".encode() + raw,
hashlib.sha256,
).hexdigest()
fresh = abs(time.time() - int(parts.get("t", 0))) < 300
if not fresh or not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = request.get_json()
return "", 200 10Errors
A failed request answers with an HTTP status and one error object:
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "missing_field",
"message": "`customer.phone` is required, in international format with the country code (e.g. 201001234567).",
"field": "customer.phone"
}
} | Status | code | Meaning |
|---|---|---|
400 | invalid_json | The body is not a JSON object. |
401 | missing_api_key / invalid_api_key | No key, a wrong or rotated key, or the store was disconnected. |
404 | order_not_found | No order with this id was sent to this store. |
405 | method_not_allowed | Wrong method for this endpoint. |
409 | no_webhook_url | The test event needs a webhook URL first. |
413 | body_too_large | The body is larger than 64 KB. |
422 | missing_field / invalid_field / invalid_order | A field is missing or wrong; `field` names it. |
429 | rate_limited | More than 120 requests a minute for the store. |
500 | server_error | The order was not saved; send it again. |
Stuck, or need a field we don't have yet? Our team answers on WhatsApp.