WhatsApp order confirmation (v1)
Send each new order to Otpera. The customer gets a WhatsApp message with the order details and a one-tap choice: Confirm, Postpone delivery or Cancel. Otpera records the answer and notifies your system by webhook. Use it to cut failed cash-on-delivery (COD) deliveries and returns.
Using Shopify? You don't need code. See Shopify.
Flow
1. Store / backend ── POST /orders ───────────────▶ Otpera
2. Otpera ── WhatsApp order summary + poll ▶ Customer
3. Customer ── taps Confirm / Postpone / Cancel
4. Otpera ── order.status_changed webhook ─▶ Your server| Endpoint | Scope | Purpose |
|---|---|---|
POST /orders | orders:write | Create an order and send confirmation |
GET /orders/{id} | orders:read | Order with items, history and messages |
POST /orders/{id}/confirmation/resend | orders:write | Re-send the WhatsApp confirmation |
Create an order
POST /orders
Body
| Field | Type | Required | Notes |
|---|---|---|---|
externalId | string ≤ 120 | yes | Your order number. Unique per source. |
source | enum | no | api (default), shopify, easyorder, vondera, woocommerce, custom |
customer.phone | string | yes | E.164 (+201012345678) or Egyptian local 01XXXXXXXXX |
customer.name | string | no | |
customer.email | string | no | |
currency | ISO 4217 | yes | e.g. EGP |
items[] | 1–200 items | yes | { name, quantity, unitPrice, variant?, sku? } |
shipping | integer (minor units) | no | Default 0 |
discount | integer (minor units) | no | Default 0 |
total | integer (minor units) | no | Optional cross-check; must equal items + shipping − discount, otherwise 422 |
paymentMethod | string ≤ 40 | no | e.g. cod |
shippingAddress | object | no | { line1, line2, city, region, postalCode, country } (country is 2 letters) |
sourceStatus | string ≤ 60 | no | Your store's own status, informational |
Money is in minor units
unitPrice: 45000 with currency: "EGP" means EGP 450.00 (piastres).
Example
curl -X POST https://api.otpera.com/api/v1/orders \
-H "Authorization: Bearer $OTPERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"externalId": "1042",
"customer": { "phone": "+201012345678", "name": "Mona Adel" },
"currency": "EGP",
"items": [
{ "name": "Desk lamp", "quantity": 1, "unitPrice": 45000 },
{ "name": "Ceramic mug", "quantity": 2, "unitPrice": 12000 }
],
"shipping": 5000,
"paymentMethod": "cod",
"shippingAddress": { "line1": "12 Tahrir St", "city": "Cairo", "country": "EG" }
}'Response 201 Created
{
"data": {
"id": "3c9e…",
"external_id": "1042",
"source": "api",
"confirmation_status": "pending",
"total_minor": 74000,
"currency": "EGP",
"created_at": "2026-11-05T10:00:00.000Z",
"duplicate": false
}
}The response header Otpera-Confirmation tells you what happened to the WhatsApp message: sent, disabled, no_number, send_failed or error.
Sending the same (source, externalId) again returns 200 with duplicate: true and no second message.
Confirmation statuses
| Status | Meaning |
|---|---|
pending | Waiting for the customer |
confirmed | Customer confirmed |
cancelled | Customer cancelled |
needs_review | Customer asked to postpone, or the reply was unclear: a team member decides |
expired | No answer within the window you set |
Customers answer with a tappable WhatsApp poll, or by typing 1 (confirm), 2 (postpone) or 3 (cancel). Reading the message never counts as confirmation.
Resend the confirmation
POST /orders/{id}/confirmation/resend re-sends the WhatsApp message for the same order (no duplicate order).
- Only while
pendingorneeds_review;409once answered. - Max 5 per order per hour.
{ "data": { "sent": true, "messageId": "…" } }Get the answer
Subscribe to order.status_changed in Dashboard → Webhooks:
{
"id": "evt_…",
"type": "order.status_changed",
"created_at": "2026-11-05T10:04:12.000Z",
"workspace_id": "…",
"data": {
"order": { "id": "3c9e…", "external_id": "1042", "source": "api" },
"from": "pending",
"to": "confirmed",
"actor_type": "customer",
"reason": "whatsapp_choice"
}
}See Webhooks for signatures and retries. You can also poll GET /orders/{id}.