TelDel
TelDel

TelDel API للتجار

اربط متجرك الإلكتروني بتل دل: كل طلب على موقعك يتحول تلقائيًا لشحنة، تتابع حالتها من نظامك، وتوصلك التحديثات أول بأول على الـ webhook.

نظرة عامة

الـ API بيعمل نفس اللي بتعمله من التطبيق، بس من سيرفر موقعك:

  • إنشاء طلب شحن من بيانات طلب العميل.
  • تتبع حالة الطلب ورقم التتبع عند شركة الشحن.
  • إلغاء الطلب قبل الاستلام.
  • استقبال تحديثات الحالة تلقائيًا على رابط عندك (Webhook).

إزاي تبدأ

  • لازم يكون عندك حساب تاجر مفعّل على تطبيق تل دل.
  • تواصل مع فريق تل دل وهيبعتلك مفتاح API للتيست (بيبدأ بـ tdk_) ويحدد لحسابك شركة الشحن الافتراضية، وبعد التجربة مفتاح الإنتاج.
  • لو عايز تحديثات تلقائية ابعت لنا رابط الـ webhook، وهتستلم مفتاح التوقيع (بيبدأ بـ whsec_).
المفتاح سري زي الباسورد: استخدمه من السيرفر بس، وما تحطهوش في كود الموقع اللي بيفتح في المتصفح أو في تطبيق موبايل. لو اتسرب اطلب مفتاح جديد فورًا.

الرابط والمصادقة

عندنا بيئتين منفصلين تمامًا، ولكل واحدة مفتاحها:

البيئةالرابط الأساسيالاستخدام
تجريبي (Test)https://api.teldel.app/api/merchantللتطوير والتجربة — الطلبات هنا ما بتتوصلش فعليًا.
إنتاج (Production)https://api.teldel.om/api/merchantالطلبات الحقيقية بعد ما تخلص التجربة.
مفتاح التيست ما بيشتغلش على الإنتاج والعكس. ابدأ بمفتاح التيست، ولما الربط يشتغل اطلب مفتاح الإنتاج وغيّر الرابط والمفتاح بس. الأمثلة في الصفحة دي على بيئة التيست.
الهيدرالقيمة
X-Api-Keyمفتاحك tdk_... (أو Authorization: Bearer tdk_...). إلزامي.
Content-Typeapplication/json في طلبات POST.
Accept-Languagear أو en — لغة رسائل الأخطاء. اختياري (الافتراضي إنجليزي).

الرد الناجح دايمًا جوه غلاف ثابت: code (كود HTTP)، وresponse (البيانات)، وtimestamp. الحد الأقصى 120 طلب في الدقيقة.

إنشاء طلب

POST /orders
curl -X POST "https://api.teldel.app/api/merchant/orders" \
  -H "X-Api-Key: tdk_your_key" \
  -H "Content-Type: application/json" \
  -H "Accept-Language: en" \
  -d '{
    "reference": "WEB-100234",
    "description": "2 x T-shirt",
    "recipient": { "name": "Ahmed Al Balushi", "phone": "91234567" },
    "deliveryAddress": {
      "wilayaCode": "5000",
      "street": "Sohar, Al Hambar, Way 1203, House 7",
      "landmark": "Behind Sohar Hospital"
    },
    "orderAmount": 12.5,
    "cashOnDelivery": true,
    "weight": 1.2,
    "notes": "Call before arriving"
  }'
الحقلالنوعالوصف
referencestring · إلزاميرقم الطلب عندك (فريد في متجرك، حتى 100 حرف). لو بعته تاني بيرجعلك نفس الطلب بدل ما يتعمل طلب جديد.
recipient.namestring · إلزامياسم العميل.
recipient.phonestring · إلزاميرقم عُماني 8 أرقام (مقبول بـ +968 أو 00968).
recipient.whatsappstringرقم الواتساب للتحديثات (الافتراضي = phone).
deliveryAddressobject · إلزاميعنوان التسليم: wilayaCode أو cityId من قائمة الولايات، وstreet (إلزامي)، وlandmark (اختياري).
pickupAddressobjectعنوان الاستلام بنفس الشكل. الافتراضي: عنوان فرعك المسجّل في التطبيق.
orderAmountnumber · إلزاميقيمة البضاعة بالريال العُماني (حتى 3 أرقام عشرية).
cashOnDeliveryboolean · إلزاميtrue: العميل يدفع البضاعة + التوصيل كاش عند الاستلام. false: البضاعة مدفوعة.
deliveryFeePaidByMERCHANT | CUSTOMERلما cashOnDelivery = false: مين يدفع التوصيل. الافتراضي MERCHANT.
descriptionstringوصف الشحنة (الافتراضي: Order <reference>).
categoryIduuidالتصنيف من GET /categories (الافتراضي: تصنيف حسابك).
weightnumberالوزن بالكيلو — بيأثر على سعر شركة الشحن.
shippingCompanyIduuidشركة شحن غير الافتراضية، من GET /shipping-companies.
notesstringملاحظات للسائق (حتى 1000 حرف).
expressbooleanشحن سريع بمناديب تل دل: رسوم إضافية مقابل توصيل أسرع. بين ولايتين ما بيحتاجش شركة شحن. شوف الشحن السريع.

مين بيدفع إيه

الحالةالحقولالنتيجة
دفع عند الاستلامcashOnDelivery: trueالعميل بيدفع البضاعة + التوصيل: أونلاين من رابط الواتساب، أو كاش للسائق (amountToCollect).
البضاعة مدفوعة، والتوصيل عليكcashOnDelivery: false, deliveryFeePaidBy: "MERCHANT"رسوم التوصيل بتتخصم من محفظتك في تل دل وقت إنشاء الطلب.
البضاعة مدفوعة، والتوصيل على العميلcashOnDelivery: false, deliveryFeePaidBy: "CUSTOMER"العميل بيدفع رسوم التوصيل بس: أونلاين أو كاش للسائق.

إيه اللي بيحصل بعد الإنشاء

  • لو العميل هو اللي بيدفع (أي حالة غير إن التوصيل عليك): الطلب بيخرج كاش عند الاستلام خلال ثواني من غير انتظار (awaitingRecipient: true في الثواني دي بس). بتوصل للعميل رسالة واتساب فيها المبلغ والرابط، ولو الطلب مع سائق تل دل يقدر يدفع أونلاين قبل الاستلام، وساعتها amountToCollect بيبقى 0. طلبات شركات الشحن كاش بس. وتقدر تبعتله الرابط بنفسك من recipientPaymentUrl.
  • داخل نفس الولاية: الطلب بيروح لسائقين تل دل.
  • بين ولايتين: الطلب بيتعيّن لشركة الشحن (shippingCompanyId أو الافتراضية لحسابك) ويتبعت لها تلقائيًا. لو مفيش شركة محددة الطلب بيترفض.
  • الطلب بيتعمل كامل أو ما بيتعملش خالص: لو رصيد المحفظة مش كفاية أو الشركة مش بتغطي الولاية، بيرجعلك خطأ وما فيش طلب بيتسجّل — صلّح وابعت تاني بنفس reference.

الرد

201 لطلب جديد، أو 200 مع existing: true لو الـ reference ده اتعمل قبل كده (آمن تعيد المحاولة بعد timeout).

201 Created
{
  "code": 201,
  "response": {
    "existing": false,
    "order": {
      "id": "6f1c2a7e-0b8d-4f3e-9a51-2c7d8e9f0a1b",
      "orderNumber": "ORD-2026-0815",
      "reference": "WEB-100234",
      "status": "READY_FOR_DELIVERY",
      "deliveryType": "NATIONAL",
      "isExpress": false,
      "expressFee": null,
      "shippingCompany": { "id": "b2d4…", "name": "Dalilee" },
      "trackingCode": null,
      "trackingUrl": "https://teldel.om/track/ORD-2026-0815",
      "deliveryFee": 2.5,
      "orderAmount": 12.5,
      "amountToCollect": 15,
      "isPaid": false,
      "awaitingRecipient": true,
      "recipientPaymentUrl": "https://teldel.om/order-details/6f1c2a7e-0b8d-4f3e-9a51-2c7d8e9f0a1b?t=Qm9x…",
      "cancelReason": null,
      "createdAt": "2026-10-28T09:12:44.120Z",
      "updatedAt": "2026-10-28T09:12:45.003Z"
    }
  },
  "timestamp": "2026-10-28T09:12:45.010Z"
}
الحقلالوصف
orderNumberرقم الطلب عندنا (ORD-...)، احفظه مع طلبك.
statusحالة الطلب — شوف الحالات.
trackingCodeرقم الشحنة عند شركة الشحن (بيظهر بعد ما الطلب يوصلها).
trackingUrlصفحة تتبع ممكن تبعتها للعميل.
isExpress / expressFeeطلب سريع ورسوم السريع (جزء من deliveryFee).
deliveryFeeرسوم التوصيل.
amountToCollectالمبلغ اللي السائق هيحصّله كاش من العميل (بيبقى 0 لو العميل دفع أونلاين).
awaitingRecipienttrue لثواني بعد إنشاء الطلب، قبل ما يخرج كاش عند الاستلام.
recipientPaymentUrlرابط العميل: يدفع أونلاين أو يختار كاش، ويتابع الشحنة. null لو التوصيل عليك.

حالة الطلب

بالـ reference بتاعك، أو رقم الطلب عندنا ORD-...، أو الـ id. الرد هو نفس كائن order اللي فوق.

GET /orders/{ref}
curl "https://api.teldel.app/api/merchant/orders/WEB-100234" \
  -H "X-Api-Key: tdk_your_key"
لو عندك webhook مش محتاج تسأل كل شوية. من غيره، اسأل كل 15–30 دقيقة كحد أدنى، ووقّف بعد DELIVERED أو CANCELED.

إلغاء طلب

POST /orders/{ref}/cancel
curl -X POST "https://api.teldel.app/api/merchant/orders/WEB-100234/cancel" \
  -H "X-Api-Key: tdk_your_key" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Customer cancelled" }'

مسموح بس قبل الاستلام (READY_FOR_DELIVERY أو PENDING_PICKUP أو ORDER_RECEIVED). رسوم التوصيل المخصومة من محفظتك بترجع لها تلقائيًا، والإلغاء بيتبعت لشركة الشحن. بعد الاستلام تواصل مع تل دل.

الولايات والتصنيفات وشركات الشحن

بيانات ثابتة تقريبًا — خزّنها عندك وحدّثها كل فترة.

الطلبالاستخدام
GET /wilayasالولايات ومناطقها: استخدم code كـ wilayaCode، أو id (ولاية أو منطقة) كـ cityId.
GET /categoriesتصنيفات الطلبات لـ categoryId.
GET /shipping-companiesالشركات المتاحة لـ shippingCompanyId (isDefault = الافتراضية لحسابك).
GET /expressالشحن السريع: متاح دلوقتي ولا لأ، رسومه، وساعاته ووعد التوصيل.
GET /wilayas
{
  "code": 200,
  "response": [
    {
      "id": "0b9a6c2e-4c1f-4b8e-9d0a-1f2e3d4c5b6a",
      "code": "5000",
      "nameAr": "صحار",
      "nameEn": "Sohar",
      "areas": [
        { "id": "4e8f…", "nameAr": "الهمبار", "nameEn": "Al Hambar" }
      ]
    }
  ],
  "timestamp": "2026-10-28T09:00:00.000Z"
}

الشحن السريع

ابعت express: true مع الطلب علشان يتوصل أسرع بمناديب تل دل: العميل بيدفع رسوم إضافية فوق سعر التوصيل، والطلب بيظهر الأول للمناديب. السريع دايمًا بمناديب تل دل حتى بين ولايتين، والعروض ما بتخصمش من رسومه.

GET /express
{
  "code": 200,
  "response": {
    "enabled": true,
    "availableNow": true,
    "startHour": 8,
    "endHour": 22,
    "fee": 1,
    "promise": "Within 3 hours",
    "national": { "enabled": true, "fee": 3, "promise": "Same day" }
  },
  "timestamp": "2026-10-29T09:00:00.000Z"
}
  • availableNow: false يعني السريع مقفول أو بره ساعاته (توقيت عُمان) — الطلب السريع وقتها بيترفض بـ errors.express_outside_hours أو errors.express_unavailable.
  • fee بتتضاف لـ deliveryFee وبترجع في expressFee.
  • بين ولايتين: national.enabled لازم يكون true، والرسوم national.fee. الطلب بيروح لمندوب تل دل، فما تبعتش shippingCompanyId. لو مقفول بيترفض بـ errors.express_domestic_only.
  • promise هو وعد التوصيل اللي ممكن تعرضه للعميل في صفحة الدفع.

حالات الطلب

statusالمعنى
READY_FOR_DELIVERYالطلب اتعمل وجاهز للاستلام.
PENDING_PICKUPاتعيّن لسائق/شركة وفي انتظار الاستلام.
ORDER_RECEIVEDالسائق أو الشركة قبلوا الطلب.
PICKED_UPاتستلم من عندك.
IN_TRANSITفي الطريق للعميل.
DELIVEREDاتسلّم للعميل. (نهائية)
CANCELEDاتلغى. (نهائية)
ممكن نضيف حالات جديدة مستقبلًا — اتعامل مع أي قيمة مش معروفة من غير ما الكود يقع.

Webhooks

مع كل تغيير في حالة طلب اتعمل من الـ API، ولما الطلب يخرج (isPaid بتبقى true)، بنبعت POST بـ JSON للرابط اللي سجلته. الحدث حاليًا: order.status_changed (وفيه كمان ping لما فريق تل دل يجرب الرابط).

Headers
POST /teldel/webhook HTTP/1.1
Content-Type: application/json
User-Agent: TelDel-Webhooks/1
X-TelDel-Event: order.status_changed
X-TelDel-Delivery: 9a0e5c1b-…
X-TelDel-Timestamp: 1761643965
X-TelDel-Signature: sha256=5d41402abc4b2a76b9719d911017c592…
Body
{
  "id": "9a0e5c1b-…",
  "event": "order.status_changed",
  "createdAt": "2026-10-28T10:32:45.000Z",
  "data": {
    "id": "6f1c2a7e-0b8d-4f3e-9a51-2c7d8e9f0a1b",
    "orderNumber": "ORD-2026-0815",
    "reference": "WEB-100234",
    "status": "IN_TRANSIT",
    "trackingCode": "A9658145",
    "…": "same fields as GET /orders/{ref}"
  }
}

التحقق من التوقيع

X-TelDel-Signature = sha256= + HMAC-SHA256 بمفتاح التوقيع whsec_... للنص <X-TelDel-Timestamp>.<body كما وصل>. اتأكد منه قبل ما تثق في الرسالة، وارفض الرسائل اللي عدّى عليها أكتر من 5 دقايق.

Node.js
// Node.js (Express) - use the raw body, not a re-serialized object
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.TELDEL_WEBHOOK_SECRET; // whsec_...

app.post("/teldel/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.header("X-TelDel-Timestamp");
  const signature = req.header("X-TelDel-Signature") ?? "";
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", SECRET).update(`${timestamp}.${req.body}`).digest("hex");

  const valid =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  // Reject old deliveries (replays)
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
  if (!valid || !fresh) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString("utf8"));
  // event.id is unique per delivery - ignore ids you already handled
  // update your order event.data.reference with event.data.status ...
  res.sendStatus(200);
});
PHP
<?php
// PHP (Laravel, WooCommerce, plain PHP...)
$secret    = getenv('TELDEL_WEBHOOK_SECRET');
$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TELDEL_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_TELDEL_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (!hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
// update your order $event['data']['reference'] with $event['data']['status'] ...
http_response_code(200);

الرد وإعادة المحاولة

  • رد بأي كود 2xx خلال 10 ثواني — اعمل المعالجة التقيلة بعد الرد.
  • لو الرد مش 2xx أو حصل timeout بنعيد بعد 1، 5، 15، 60 دقيقة، وبعدين 6 ساعات (6 محاولات في المجموع).
  • ممكن نفس الرسالة توصل مرتين — استخدم id عشان تتجاهل المكرر.
  • الترتيب مش مضمون 100% — اعتمد على data.status وdata.updatedAt، أو اسأل GET /orders/{ref}.

الأخطاء

الأخطاء فيها message مترجمة (حسب Accept-Language) وcode ثابت تقدر تعتمد عليه في الكود. أخطاء الحقول فيها كمان details بكل حقل غلط.

400 Bad Request
{
  "statusCode": 400,
  "timestamp": "2026-10-28T09:12:45.010Z",
  "path": "/api/merchant/orders",
  "method": "POST",
  "requestId": "c1d2e3…",
  "message": "Unknown wilaya: send a cityId or wilayaCode from the wilayas list.",
  "code": "errors.merchant_api_unknown_wilaya"
}
HTTPcodeالسبب
401errors.merchant_api_key_missing / errors.merchant_api_key_invalidمفيش مفتاح، أو المفتاح غلط/اتلغى، أو الحساب موقوف.
400errors.validation_failedحقل ناقص أو غلط — شوف details.
400errors.merchant_api_invalid_phoneرقم الهاتف مش رقم عُماني 8 أرقام.
400errors.merchant_api_unknown_wilayawilayaCode / cityId مش موجود.
400errors.merchant_api_pickup_requiredمفيش عنوان فرع لحسابك — ابعت pickupAddress.
400errors.merchant_api_no_shipping_companyطلب بين ولايتين من غير شركة شحن.
400errors.insufficient_balanceرصيد المحفظة مش كفاية لرسوم التوصيل.
409—شركة الشحن مش بتغطي الولاية، أو حالة الطلب ما تسمحش بالعملية.
404errors.order_not_foundالطلب مش موجود أو مش تبعك.
429errors.too_many_requestsأكتر من 120 طلب في الدقيقة — استنى وجرّب تاني.
5xx—مشكلة عندنا — جرّب تاني بعد شوية (نفس reference آمن).
للدعم ابعت لنا: reference ورقم الطلب ORD-...، والـ requestId من رد الخطأ، ووقت الطلب.