cashod
للمطوّرين · API v1

اربط أنظمتك مباشرة مع كاشود.

تسمح لك واجهة كاشود البرمجية بإرسال طلبات الدفع عند الاستلام من أنظمتك، ومعرفة حالة توصيلها، وإبقاء منتجاتك ومخزونك متزامنين. واجهة HTTP بسيطة بصيغة JSON، ولا تحتاج أي مكتبة إضافية.

الرابط الأساسيhttps://api.cashod.ma/api/v1
إنشاء طلب
$ 
الرد
المصادقة

مفتاح لكل متجر، وبالصلاحيات التي تختارها فقط.

أنشئ المفتاح من الإعدادات ← المطوّرون داخل كاشود. يظهر المفتاح السري مرة واحدة فقط عند إنشائه ولا يمكن استرجاعه بعدها، فإذا ضاع ألغِه وأنشئ مفتاحاً جديداً.

أرسله في كل طلب كـ bearer token:

Authorization
Authorization: Bearer csk_live_…
كل مفتاح خاص بمتجر واحد.

يرى ويعدّل بيانات ذلك المتجر فقط، لذلك لا تحتاج أن ترسل معرّف المتجر أبداً؛ المفتاح نفسه يحدّده. وإذا كان عندك عدة متاجر، أنشئ مفتاحاً لكل واحد.

الصلاحيات

لكل مفتاح صلاحيات تحدّدها عند إنشائه. صلاحية الكتابة تشمل القراءة المقابلة لها، فالمفتاح الذي ينشئ الطلبات يستطيع قراءتها أيضاً. أعطِ سكربت التقارير مفتاحاً للقراءة فقط، فلن يستطيع إنشاء أو إلغاء أي شيء حتى لو تسرّب.

  • orders:readقراءة الطلبات وحالة توصيلها
  • orders:writeإنشاء الطلبات — وتشمل orders:read
  • products:readقراءة كتالوج المنتجات
  • products:writeإنشاء المنتجات وتعديلها — وتشمل products:read
  • stock:writeتحديد كميات المخزون لكل خيار
البداية السريعة

أول طلب لك في ثلاث خطوات.

  1. 01
    أنشئ مفتاحاً

    افتح الإعدادات ← المطوّرون في كاشود، اختر الصلاحيات التي تحتاجها وانسخ المفتاح السري؛ لن يظهر مرة أخرى.

  2. 02
    أرسل طلباً

    أرسله بـ POST إلى /v1/orders مع external_id خاص بك، حتى لا يتحوّل أي طلب مُعاد إلى طرد ثانٍ.

  3. 03
    تابع توصيله

    اقرأ status وconfirmation_status وtracking_number من /v1/orders/{id} مع كل تقدّم لشركة الشحن.

POST /v1/orders
curl -X POST https://api.cashod.ma/api/v1/orders \
  -H "Authorization: Bearer csk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "order-77",
    "customer": { "name": "Amine Berrada", "phone": "+212600000000" },
    "shipping": { "address": "12 Rue Tarik Ibn Ziad", "city": "Casablanca" },
    "items": [ { "sku": "MON-001-M-NOIR", "quantity": 2 } ]
  }'
نقاط الاتصال

واجهة صغيرة تتقنها في ظهيرة واحدة.

كل شيء تحت https://api.cashod.ma/api/v1.

POST/v1/orders

أنشئ طلباً من صفحة الدفع عندك. يمكنك إعادة المحاولة بأمان مع external_id.

الصلاحيةorders:write
GET/v1/orders/{id}

اقرأ طلباً واحداً: حالة الشحن وحالة التأكيد ورقم التتبع.

الصلاحيةorders:read
GET/v1/orders

اعرض الطلبات من الأحدث، صفِّها بـ created_after، وتنقّل بينها بمؤشّر (cursor).

الصلاحيةorders:read
POST/v1/products

أنشئ منتجاً أو عدّله حسب الـ SKU، مع خياراته وصوره.

الصلاحيةproducts:write
PATCH/v1/products/{productId}/variants/{variantId}/stock

حدّد مخزون خيار معيّن بقيمة نهائية.

الصلاحيةstock:write
GET/v1/openapi.json

ملف OpenAPI الذي بُني منه المرجع، بصيغة تقرأها الآلات.

المساعد الذكي

دع مساعد البرمجة يكتب الربط عنك

تستعمل مساعداً ذكياً للبرمجة؟ انسخ هذه التعليمات بدل أن تشرح له الصفحة بنفسك. لا تحتوي على أي مفتاح؛ الكود الناتج يقرأ مفتاحك من متغيّر البيئة CASHOD_API_KEY.

الصقها في ChatGPT أو Claude أو Cursor أو Lovable. فيها قواعد هذه الصفحة ورابط ملف OpenAPI، فيكتب المساعد الربط بلغة البرمجة التي تستعملها.

cashod-api-prompt.md
You are helping me integrate my system (online store, landing page or ERP) with the Cashod API, a cash-on-delivery order platform. Write the integration code following the specification below exactly. Ask me which language/framework I use if you cannot tell from context, and which of the tasks below I need.

The full machine-readable specification is at https://api.cashod.ma/api/v1/openapi.json. Fetch it if you can, and trust it over this summary for field details.

## Rules
- Call the API from my SERVER only, never from browser JavaScript: the key must not reach visitors.
- Read the key from an environment variable named CASHOD_API_KEY (it looks like csk_live_…). Never hard-code it. I create it in Cashod under Settings → Developers; one key belongs to one store.
- Every request: header "Authorization: Bearer <CASHOD_API_KEY>", JSON bodies with "Content-Type: application/json".
- Send only the fields listed here or in the OpenAPI document: unknown fields are rejected with 400 invalid_request.
- Rate limit: 120 requests per minute per key.
- Never let a Cashod failure break my customer's checkout: log it and retry in the background.

## Task 1: send an order
POST https://api.cashod.ma/api/v1/orders  (scope orders:write)
{
  "external_id": "order-77",
  "customer": { "name": "Amine Berrada", "phone": "+212600000000" },
  "shipping": { "address": "12 Rue Tarik Ibn Ziad", "city": "Casablanca" },
  "items": [ { "sku": "MON-001-M-NOIR", "quantity": 2, "unit_price": 149 } ]
}
- Always send external_id, set to my own order id. It makes the call idempotent: a retry returns the existing order (200) instead of creating a second one (201). Without it a retried timeout becomes a second real parcel.
- customer.name, customer.phone, shipping.address, shipping.city are required. shipping.phone is optional (defaults to the customer's).
- items: at least one. Each needs quantity (integer ≥ 1) and one of: sku (variant or product SKU, the usual way), product_id (Cashod product id), or name (free text for an item not in the catalogue; unit_price is then required).
- unit_price: what the customer actually paid per unit. Omit it to use the catalogue price.
- Optional: total (final agreed total; the difference is recorded as a discount), discount, notes, scheduled_for (ISO 8601), source_name (e.g. "Landing page Ramadan"), confirmed (true ONLY if I already confirmed the order with the customer; it skips the confirmation call).

## Task 2: read delivery status
GET https://api.cashod.ma/api/v1/orders/{id}  or  GET https://api.cashod.ma/api/v1/orders?created_after=<ISO 8601>  (scope orders:read)
- An order has status (shipping, e.g. NOT_SHIPPED, SHIPPED, DELIVERED, RETURNED), confirmation_status (e.g. PENDING, CONFIRMED, CANCELLED) and tracking_number once shipped.
- Lists are cursor-paginated, newest first: read data, has_more and next_cursor, and pass ?cursor=<next_cursor> for the next page. limit defaults to 50, max 100.
- Poll at most once a minute.

## Task 3: sync products
POST https://api.cashod.ma/api/v1/products  (scope products:write)
- Keyed on sku: an unknown sku creates the product (201), a known one updates it (200). Safe to send the whole catalogue on a schedule.
- Required: sku, name, category, price, cost_price. Optional: description, sale_price, is_active, track_stock, min_stock, images (URLs), variants.
- variants: each needs sku; optional size, color, price, sale_price, cost_price, stock. Variants are matched by sku; one I do not list is left untouched, never deleted.
- images: omit to keep the existing ones, send a list to replace them, send [] to clear them.
- Setting stock here also needs the stock:write scope.
- 409 conflict: another product already uses this name, or a variant sku belongs to another product. Report it to me; do not retry.

## Task 4: keep stock in sync
PATCH https://api.cashod.ma/api/v1/products/{productId}/variants/{variantId}/stock  (scope stock:write)
{ "stock": 12 }
- Stock is set to an absolute level, never a delta, so a retried call cannot double-apply.

## Errors
Every failure looks like {"error": {"type": "...", "message": "..."}}. Branch on error.type, never on the message text.
- invalid_request (400): fix the payload, do not retry.
- authentication_error (401): missing, unknown or revoked key. Do not retry.
- permission_error (403): the key lacks the scope, or the subscription is not active. Do not retry.
- not_found (404): no such order or product in this key's store.
- rate_limit_exceeded (429) and server_error (500), plus timeouts and network errors: retry with backoff (for example 1 min, 5 min, 30 min). Orders are safe to retry because of external_id.

New optional fields may appear in responses: ignore what you do not recognise.

## When you are done
Show me where the calls happen in my code, the environment variable to set, and a curl command to send one test order.
أدلة

ما لا تخبرك به قائمة الحقول.

إنشاء طلب

حدّد كل منتج بـ SKU الخاص به، سواء كان SKU الخيار أو SKU المنتج حسب طريقة بيعك. إذا لم ترسل unit_price يُستعمل سعر الكتالوج، وإذا أرسلته يُعتمد سعرك، وهذا ما تحتاجه عندما يكون في صفحة البيع عرض خاص.

POST /v1/orders
curl -X POST https://api.cashod.ma/api/v1/orders \
  -H "Authorization: Bearer csk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "order-77",
    "customer": { "name": "Amine Berrada", "phone": "+212600000000" },
    "shipping": { "address": "12 Rue Tarik Ibn Ziad", "city": "Casablanca" },
    "items": [ { "sku": "MON-001-M-NOIR", "quantity": 2 } ]
  }'

تفادي التكرار — أرسل external_id

أرسل معرّف الطلب الخاص بك في external_id، فتصبح إعادة المحاولة آمنة. إذا وصل نفس external_id مرة ثانية، يعيد كاشود الطلب الموجود بدل أن ينشئ طلباً جديداً:

  • 201 Created — تم إنشاء طلب جديد.
  • 200 OK — هذا external_id له طلب سابق، وأُعيد كما هو دون تغيير.

هذه النقطة أهم مما تبدو. الطلب الذي تنتهي مهلته يكون غالباً قد عولج فعلاً؛ وبدون external_id تتحوّل إعادة المحاولة إلى طلب حقيقي ثانٍ يُرسل لشركة شحن حقيقية، ويستلم الزبون طردين.

معرفة حالة التوصيل

كل طلب يحمل status (الشحن) وconfirmation_status (مكالمة التأكيد) وtracking_number بعد الشحن.

GET /v1/orders/{id}
curl https://api.cashod.ma/api/v1/orders/ORDER_ID \
  -H "Authorization: Bearer csk_live_…"

لمتابعة طلبات كثيرة، اعرضها بـ GET /v1/orders وصفِّها بـ created_after. لا تستعلم أكثر من مرة في الدقيقة؛ الحالات تتغيّر بإيقاع شركة الشحن، فلا فائدة من الاستعلام المتكرر.

إنشاء المنتجات وتعديلها

نقطة اتصال واحدة تعتمد على sku الخاص بك: SKU جديد يُنشئ المنتج، وSKU موجود يعدّله. هكذا ترسل كتالوجك كاملاً بشكل دوري دون أن تتحقق مما هو موجود.

  • 201 Created — هذا الـ SKU جديد.
  • 200 OK — يوجد منتج بهذا الـ SKU وتم تعديله.
POST /v1/products
curl -X POST https://api.cashod.ma/api/v1/products \
  -H "Authorization: Bearer csk_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "sku": "MON-001",
    "name": "Montre Or",
    "category": "Accessoires",
    "price": 199,
    "cost_price": 60,
    "images": [ "https://cdn.example/montre.jpg" ],
    "variants": [
      { "sku": "MON-001-M-NOIR", "size": "M", "color": "Noir", "stock": 7 }
    ]
  }'

الخيارات تُطابق حسب SKU، والخيار الذي لا ترسله يبقى كما هو ولا يُحذف أبداً. غالباً ما يكون للخيار طلبات سابقة، فلا يجب أن تمحوه مزامنة نسيته. احذف الخيار من لوحة تحكم كاشود.

الصور تعمل بالعكس: إذا لم ترسل حقل images تبقى الصور الحالية، وإذا أرسلت قائمة تصبح هي الصور، وأرسل [] لحذفها كلها.

cost_price إلزامي. منه يحسب كاشود تكلفة البضاعة والربح، والمنتج الذي يُنشأ بدونه يظهر بهامش 100% في كل طلب. عند التعديل يُتجاهل هذا الحقل إذا اشتريت مخزوناً لهذا المنتج، لأن تغييره سيغيّر تكلفة البضاعة الموجودة عندك.

تحديد stock لخيار عبر هذه النقطة يحتاج أيضاً صلاحية stock:write. بدونها يُرفض الطلب كاملاً بدل أن يُحفظ كل شيء ما عدا المخزون بصمت، فيبقى مفتاح الكتالوج مفتاحاً للكتالوج فقط.

هناك تعارضان يجب التعامل معهما: منتج آخر في المتجر يحمل نفس الاسم (الأسماء فريدة داخل المتجر بشكل مستقل عن الـ SKU)، أو أحد SKU الخيارات تابع لمنتج آخر. في الحالتين يصلك 409 conflict مع توضيح سبب التعارض.

مزامنة المخزون

المخزون يُحدَّد بقيمة نهائية وليس بالزيادة أو النقص. أرسل الكمية التي تريدها للخيار؛ هكذا لا تُطبّق إعادة المحاولة مرتين، بينما -1 كانت ستفعل ذلك دون أن تنتبه.

PATCH /v1/products/{productId}/variants/{variantId}/stock
curl -X PATCH https://api.cashod.ma/api/v1/products/PRODUCT_ID/variants/VARIANT_ID/stock \
  -H "Authorization: Bearer csk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "stock": 12 }'

التصفّح بين الصفحات

نقاط العرض تستعمل مؤشّراً (cursor) ومرتّبة من الأحدث. كل رد يحتوي على data وhas_more وnext_cursor. أعد إرسال المؤشّر في ?cursor= للصفحة التالية. عدد العناصر الافتراضي 50 والحد الأقصى 100.

نستعمل المؤشّر بدل أرقام الصفحات لأن الطلبات تصل باستمرار: مع أرقام الصفحات، كل طلب جديد يُزيح ما بعده، فيفوّت من يتصفّح السجل بعض الطلبات ويقرأ أخرى مرتين دون أن يدري.

الأخطاء

كل خطأ يأتي بنفس الشكل:

application/json
{
  "error": {
    "type": "invalid_request",
    "message": "No product or variant with SKU \"GHOST-1\" in this store."
  }
}

اعتمد على type وليس على نص message؛ النص قد نحسّنه، أما النوع فلا يتغيّر.

400invalid_requestخطأ في البيانات المرسلة، والرسالة تحدّد مكانه.
401authentication_errorالمفتاح مفقود أو غير معروف أو مُلغى.
403permission_errorالمفتاح لا يملك الصلاحية، أو اشتراك الحساب غير نشط.
404not_foundلا يوجد طلب أو منتج بهذا المعرّف في متجر هذا المفتاح.
429rate_limit_exceededانتظر قليلاً ثم أعد المحاولة.
500server_errorالمشكلة عندنا. إعادة المحاولة آمنة، وآمنة حتى مرتين إذا أرسلت external_id.

حدود الاستخدام

0طلباً في الدقيقة لكل مفتاح

120 طلباً في الدقيقة لكل مفتاح. الحد مرتبط بالمفتاح وليس بعنوان IP، فلا يستهلك عميل آخر على نفس الاستضافة حصتك.

الإصدارات

رقم الإصدار في الرابط: /v1. قد نضيف حقولاً اختيارية جديدة للردود داخل نفس الإصدار، فاكتب كوداً يتجاهل ما لا يعرفه. وأي تغيير قد يكسر ربطاً موجوداً يأتي في إصدار جديد.

مرجع الـ API

كل نقطة اتصال، وكل حقل.

مولّد مباشرة من ملف OpenAPI الخاص بالخادم، فيتحدّث مع الكود في كل نشر ولا يصبح قديماً أبداً.

openapi.json
API كاشود للمطوّرين: الطلبات والمخزون والـwebhooks