للمطوّرين

Capi Agent API — واجهة برمجة المتجر المخصص

يرسل نظام متجرك المنتجات والطلبات والسلات المتروكة والعملاء، فيعمل مساعد Capi Agent والحملات والأتمتة تمامًا كما تعمل على متجر سلة أو Shopify. وفي الاتجاه المعاكس يستقبل نظامك أحداثًا موقّعة: كوبونات الخصم، وتأكيد العميل لطلبه أو إلغاؤه، وإلغاء الاشتراك.

الصق الدليل في Claude أو ChatGPT ليربط متجرك، أو استورد ملف OpenAPI في Postman أو Insomnia.

https://capiagent.com/api/ingest/v1
لغة الأمثلة
المحتويات

البدء السريع

  1. 1من لوحة Capi Agent افتح «التكاملات» ثم «متجر مخصص»، وأنشئ مفتاحًا تجريبيًا أولًا.
  2. 2تحقق من المفتاح بطلب GET /ping.
  3. 3أرسل المنتجات عبر POST /products، ثم كل طلب عبر POST /orders، وحدّث حالته بـPATCH /orders/{id}.
  4. 4عندما تنجح كل الطلبات بالمفتاح التجريبي، أنشئ مفتاحًا فعليًا وبدّله في نظامك.
  5. 5اختياري: أضف رابط الأحداث ليصلك الكوبون وتأكيد الطلبات وإلغاء الاشتراك.
  6. 6أضف سطر الودجت لكل الصفحات، واستدعِ AISA.track للأحداث الثلاثة وAISA.identify للعميل المسجّل (قسم «الودجت وأحداث المتصفح»).
الطلب
curl -X GET https://capiagent.com/api/ingest/v1/ping \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx"

المصادقة والمفاتيح

أرسل المفتاح في الترويسة Authorization: Bearer <key> مع كل طلب، أو في X-API-Key إن لم يسمح عميلك بترويسة Authorization. يظهر المفتاح مرة واحدة عند إنشائه، ولا نحفظ منه إلا بصمة SHA-256.

المفتاح يفتح براندًا واحدًا، ولا يستطيع أي طلب تسمية براند آخر.

المفاتيح التجريبية

المفتاح الذي يبدأ بـcapi_test_ يتحقق من كل طلب ويرد بالنتائج نفسها تمامًا، لكنه لا يكتب شيئًا: لا منتجات ولا طلبات ولا رسائل. كل رد عليه يحمل "test": true.

قواعد عامة

المبالغ
بالوحدة الرئيسية (199.99) وليس بالهللات، رقم JSON لا نصًا: أي مبلغ نصي (حتى "199.99") يُقرأ بتخمين حسب العملة ويصلك عليه تحذير. تُحوَّل حسب عملة البراند، وللدينار الكويتي والبحريني والريال العماني ثلاث خانات.
رقم الجوال
دولي وأرقام فقط (966501234567). هو هوية العميل على واتساب، وبه يجد المساعد طلباته. الرقم المحلي يُرفض مع تحذير.
التكرار الآمن
كل كتابة تُطابَق على معرّفك أنت، فإعادة إرسال الطلب نفسه تحدّث ولا تكرّر. أعد المحاولة بلا قلق.
التواريخ
بصيغة ISO 8601 مع المنطقة الزمنية (2026-10-06T09:00:00Z). تاريخ بلا منطقة زمنية يُقرأ بتوقيت الخادم ويصلك عليه تحذير.
النتائج
الرد 200 يحمل نتيجة لكل عنصر؛ عنصر معطوب لا يُسقط بقية الطلب. العنصر الفاشل فيه errorCode، وأي حقل باسم غير معروف يصلك تحذيرًا في warnings.
الحقل الناقص وnull
POST /products وPOST /orders يستبدلان السجل: أرسل كل الحقول في كل مرة. POST /customers يدمج ولا يمسح شيئًا. PATCH يغيّر ما ترسله فقط، وnull يمسح الحقل، إلا costPrice وshipment وrefunded فيُتجاهل فيها null (أرسل 0 لتصفير refunded)، وnull داخل shipment يمسح ذلك الحقل وحده. statusName يُقرأ مع status فقط.
النصوص والرموز
الرسائل وتحذيرات warnings نص للقراءة وقد تتغيّر صياغته. اعتمد في كودك على error.code وerrorCode وwarningCodes فقط.
ترتيب الأحداث
الأحداث لا تصل بالضرورة بالترتيب، فإعادة المحاولة قد تصل بعد حدث أحدث. امنع التكرار بـid، ورتّب بوقت الحدث نفسه (respondedAt أو unsubscribedAt أو createdAt).
التوافق
داخل v1 لا نحذف ولا نعيد تسمية أي شيء، بل نضيف فقط: حقول وأحداث وحالات ورموز أخطاء. تجاهل في كودك أي شيء لا تعرفه، فيبقى يعمل مع كل إضافة.

سجل التغييرات

كل ما أُضيف إلى الواجهة، الأحدث أولًا. لم يُحذف أي شيء من v1.

  1. 16.10.2026
    • رمز errorCode لكل عنصر فاشل في الدفعة.
    • تحذيرات في warnings: حقل باسم غير معروف، مبلغ مُرسل نصًا، تاريخ بلا منطقة زمنية، عملة غير مدعومة.
    • رقم النسخة apiVersion في كل حدث (Webhook) وفي الترويسة X-Capi-Api-Version.
    • إعادة صرف الكوبون لنفس orderId ترد بنجاح (replayed) بدل already_used.
    • warningCodes بجانب warnings، وreconciled مع complete، ونتيجة كل عنصر في 422 all_failed.
    • ترويسة التوقيع قد تحمل أكثر من v1 (لتبديل السر)، وهي صحيحة إن طابق أي منها.
    • discountType في الكوبونات، وusageLimit رقم.
    • قواعد منشورة: الحقل الناقص وnull، النصوص والرموز، ترتيب الأحداث.
    • PATCH /products: إرسال price مع salePrice: null ينهي التخفيض ويمسح السعر قبل الخصم.
  2. 15.10.2026
    • هوية العميل الموقّعة في AISA.identify (hash) لدمج محادثاته بين الأجهزة.
  3. 08.10.2026
    • tracking في POST /orders لاحتساب مبيعات الإعلانات.
    • items[].productId وcouponCode في الطلبات.
    • الودجت وأحداث المتصفح في المتجر.
    • عملة المتجر تُحدَّد من أول بيانات تصل.
  4. 06.10.2026
    • المفاتيح التجريبية capi_test_.
    • POST /customers وPATCH /products/:id.
    • أحداث order.cod_confirmed وorder.cod_cancelled وcustomer.unsubscribed بإعادة المحاولة.
    • تحقق الكوبون وصرفه وإلغاء صرفه.
    • ملف OpenAPI وصفحة المطوّرين.
  5. 05.10.2026
    • POST /carts للسلات المتروكة.
    • حدث coupon.create.
  6. 21.09.2026
    • الإصدار الأول: GET /ping وPOST /products وPOST /orders وPATCH /orders/:id وDELETE /products/:id.

الأخطاء

اعتمد على error.code في برمجتك؛ أما message فنص عربي للقراءة فقط.

unauthorizedالمفتاح مفقود أو غير معروف أو أُلغي.
rate_limitedتجاوزت الحد. انتظر المدة في Retry-After.
invalid_jsonالمحتوى ليس JSON صالحًا.
invalid_bodyشكل الطلب نفسه غير صالح (مثل غياب مصفوفة products).
not_foundلا يوجد عنصر بهذا المعرّف.
all_failedكل العناصر فشلت؛ الرسالة تحمل أول سبب.
payload_too_largeالطلب أكبر من 4 ميغابايت؛ قسّمه.
already_usedالكوبون صُرف من قبل (409).
expiredالكوبون منتهي الصلاحية (410).
not_redeemedلا يوجد صرف لإلغائه (409).
internal_errorخطأ لدينا. أعد المحاولة بعد قليل.

النجاح الجزئي

إذا نجح بعض العناصر وفشل بعضها فالرد 200، وفي results سبب كل عنصر فاشل. لا تعتبر 200 نجاحًا كاملًا قبل أن تقرأ failed.

الحدود

  • 300 طلب في الدقيقة لكل مفتاح. عند التجاوز يصل 429 مع Retry-After.
  • 4 ميغابايت لكل طلب.
  • 500 منتج، أو 100 طلب، أو 100 سلة، أو 500 عميل في الطلب الواحد.

المرجع

كل نقطة مع شكل الطلب وأمثلة جاهزة بأربع لغات.

شرح الحقول بالإنجليزية لأنه مولَّد مباشرة من مواصفات الواجهة نفسها، فيطابق ما تقبله دائمًا.

GETتحقق من المفتاح

GEThttps://capiagent.com/api/ingest/v1/ping

يعيد اسم البراند وعملته ونوع المفتاح (فعلي أو تجريبي).

الطلب
curl -X GET https://capiagent.com/api/ingest/v1/ping \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx"
الرد
JSON
{
  "ok": true,
  "mode": "live",
  "store": {
    "name": "Lumi Store",
    "currency": "SAR"
  }
}

POSTأرسل المنتجات أو حدّثها

POSThttps://capiagent.com/api/ingest/v1/products

حتى 500 منتج في الطلب. المطلوب id وname فقط. أرسل complete: true فقط إن كان الطلب يحوي كتالوجك كاملًا.

الحقلالنوعمطلوبملاحظات
productsobject[]نعم
products[].idstringنعمYour own product id. Every request is matched on it — re-sending the same id updates the product, never duplicates it.
products[].namestringنعمProduct name as the customer sees it.
products[].descriptionstring | nullPlain text or HTML. HTML is stripped for the assistant.
products[].pricenumber | string | nullMajor units (199.99), as a JSON number (a string is accepted but read by guesswork and warned, `string_amount`). When `salePrice` is lower, this reads as the price before discount.
products[].salePricenumber | string | nullThe current price when the product is discounted.
products[].compareAtPricenumber | string | nullAn explicit "was" price, when you keep one separately.
products[].costPricenumber | string | nullWhat the item costs you (cost of goods). Send it — profit, margin and the ads stock/margin rules need it; never shown to customers. Per variant too, when sizes cost differently.
products[].currencystring | nullISO 4217 code. Defaults to the brand's currency.
products[].skustring | nullStock keeping unit.
products[].urlstring (uri) | nullPublic product page — the link the assistant sends.
products[].imagesstring (uri)[]Image URLs, main image first. Up to 20.
products[].categoriesstring[]Category names, broadest first.
products[].stockinteger | nullUnits available. Decides availability; 0 means out of stock.
products[].availability"in_stock" | "out_of_stock" | nullUsed only when `stock` is not sent. Without either, availability is "unknown" and the assistant never claims it is sold out.
products[].publishedboolean`false` archives the product immediately (it stays in order history).
products[].variantsobject[]Sizes, colours and other options, each with its own id, price and stock.
products[].variants[].idstringنعم
products[].variants[].namestring | null
products[].variants[].skustring | null
products[].variants[].pricenumber | string | null
products[].variants[].compareAtPricenumber | string | null
products[].variants[].costPricenumber | string | null
products[].variants[].stockinteger | null
completeboolean`true` says this request is your whole catalogue: products missing from it are archived. Ignored if any item fails. Use it only when the catalogue fits in one request, and never together with a catalog feed link.
الطلب
curl -X POST https://capiagent.com/api/ingest/v1/products \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "products": [
    {
      "id": "SKU-1001",
      "name": "Cotton shirt",
      "price": 199.99,
      "salePrice": 149.99,
      "costPrice": 80,
      "stock": 12,
      "url": "https://mystore.com/p/1001",
      "images": [
        "https://mystore.com/img/1001.jpg"
      ],
      "categories": [
        "Clothing"
      ],
      "variants": [
        {
          "id": "SKU-1001-M",
          "name": "M",
          "price": 149.99,
          "costPrice": 80,
          "stock": 5
        }
      ]
    }
  ]
}'
الرد
JSON
{
  "ok": true,
  "succeeded": 1,
  "failed": 0,
  "results": [
    {
      "id": "SKU-1001",
      "ok": true
    }
  ]
}

PATCHpatchProduct

PATCHhttps://capiagent.com/api/ingest/v1/products/{id}
الحقلالنوعمطلوبملاحظات
pricenumber | string | null
salePricenumber | string | null
compareAtPricenumber | string | null
costPricenumber | string | null
stockinteger | null
availability"in_stock" | "out_of_stock" | null
publishedboolean
الطلب
curl -X PATCH https://capiagent.com/api/ingest/v1/products/SKU-1001 \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "price": 199.99,
  "salePrice": 149.99,
  "costPrice": 80,
  "stock": 3
}'
الرد
JSON
{
  "ok": true,
  "succeeded": 1,
  "failed": 0,
  "results": [
    {
      "id": "SKU-1001",
      "ok": true
    }
  ]
}

DELETEأرشف منتجًا

DELETEhttps://capiagent.com/api/ingest/v1/products/{id}

يُخرج المنتج من الكتالوج ويبقى في تاريخ الطلبات.

الطلب
curl -X DELETE https://capiagent.com/api/ingest/v1/products/SKU-1001 \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx"
الرد
JSON
{
  "ok": true,
  "succeeded": 1,
  "failed": 0,
  "results": [
    {
      "id": "SKU-1001",
      "ok": true
    }
  ]
}

POSTأرسل الطلبات أو حدّثها

POSThttps://capiagent.com/api/ingest/v1/orders

أرسل كل طلب جديد. backfill: true لاستيراد طلبات قديمة دون إطلاق رسائل أو أتمتة.

الحقلالنوعمطلوبملاحظات
ordersobject[]نعم
orders[].idstringنعمYour order number. Re-sending the same id updates the order; its items are replaced in full.
orders[].statusstringنعمOrder status: pending, processing, shipped, delivered, cancelled or refunded — or your system's own status as is (e.g. awaiting_pickup). An unknown one is kept and the merchant decides once in the dashboard what it means; until then it sends nothing. A change fires order automations and the customer's status message, as on any platform.
orders[].statusNamestring | nullOptional. The status's name as your store's admin shows it (e.g. "Awaiting pickup") — what the merchant sees when deciding. Defaults to the code.
orders[].totalnumber | stringنعمOrder total in major units.
orders[].currencystringISO 4217 code. Defaults to the brand's currency.
orders[].placedAtstringISO 8601. A date in the future is refused.
orders[].customerobjectWho ordered. The phone must be international, digits only (966501234567) — it is the customer's WhatsApp identity and how the assistant finds their orders.
orders[].customer.namestring | null
orders[].customer.phonestring | null
orders[].customer.emailstring (email) | null
orders[].itemsobject[]Order lines. Send each line's `productId` (the id you used on /products) to link it to the catalogue; without it the name is matched exactly.
orders[].items[].productIdstring | null
orders[].items[].namestringنعم
orders[].items[].quantityintegerنعم
orders[].items[].pricenumber | stringنعم
orders[].subtotalnumber | string | null
orders[].discountnumber | string | null
orders[].taxnumber | string | null
orders[].shippingnumber | string | null
orders[].refundednumber | string | null
orders[].shipmentobjectCarrier and tracking — the assistant answers "where is my order?" from these fields.
orders[].shipment.carrierstring | null
orders[].shipment.trackingNumberstring | null
orders[].shipment.trackingUrlstring (uri) | null
orders[].shipment.shippedAtstring | null
orders[].shipment.deliveredAtstring | null
orders[].shipment.estimatedDeliveryAtstring | null
orders[].paymentMethodstring | null`cod` (or `cash_on_delivery`) enables cash-on-delivery confirmation over WhatsApp for this order.
orders[].paymentUrlstring (uri) | nullAn https link where the customer pays this same order online with your prepaid discount. Sent after they confirm a COD order, when that offer is on.
orders[].couponCodestring | null
orders[].backfillboolean`true` stores the order without firing automations, notifications or messages — for importing history.
orders[].trackingobjectOptional. The buyer's browser at checkout — landingUrl (with the ad's fbclid/gclid/ttclid/ScCid), clickIds, the pixel cookies fbp/fbc, userAgent and ip — so the ad platforms can count a sale from an ad that never went through the chat. The IP is shared only when the merchant turned on enhanced matching. If the store has a cookie consent banner, send it only when the buyer accepted marketing cookies — the condition is the consent, not the pixel loading (a blocked pixel still sends, without fbp/fbc).
orders[].tracking.landingUrlstring (uri) | null
orders[].tracking.userAgentstring | null
orders[].tracking.ipstring | null
orders[].tracking.fbpstring | null
orders[].tracking.fbcstring | null
orders[].tracking.clickIdsobject
orders[].tracking.clickIds.fbclidstring
orders[].tracking.clickIds.gclidstring
orders[].tracking.clickIds.gbraidstring
orders[].tracking.clickIds.wbraidstring
orders[].tracking.clickIds.ttclidstring
orders[].tracking.clickIds.ScCidstring
الطلب
curl -X POST https://capiagent.com/api/ingest/v1/orders \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "orders": [
    {
      "id": "10045",
      "status": "processing",
      "total": 349.98,
      "currency": "SAR",
      "placedAt": "2026-10-06T09:00:00Z",
      "customer": {
        "name": "Sara",
        "phone": "966501234567",
        "email": "sara@example.com"
      },
      "items": [
        {
          "productId": "SKU-1001",
          "name": "Cotton shirt",
          "quantity": 2,
          "price": 174.99
        }
      ],
      "couponCode": "CAPI-7K3M9",
      "paymentMethod": "cod",
      "paymentUrl": "https://mystore.com/pay/10045"
    }
  ]
}'
الرد
JSON
{
  "ok": true,
  "succeeded": 1,
  "failed": 0,
  "results": [
    {
      "id": "10045",
      "ok": true
    }
  ]
}

PATCHحدّث حالة طلب أو شحنه

PATCHhttps://capiagent.com/api/ingest/v1/orders/{id}

الحقول التي ترسلها فقط هي التي تتغير. تغيّر الحالة يطلق رسالة حالة الطلب للعميل حسب إعدادات التاجر.

الحقلالنوعمطلوبملاحظات
statusstringNew status — one of ours or your system's own, as on POST /orders. Fires order.status_changed like any platform.
statusNamestring | nullOptional. The status's name in your store's admin.
shipmentobjectOnly the fields you send change; a status-only patch keeps the stored tracking number.
shipment.carrierstring | null
shipment.trackingNumberstring | null
shipment.trackingUrlstring (uri) | null
shipment.shippedAtstring | null
shipment.deliveredAtstring | null
shipment.estimatedDeliveryAtstring | null
refundednumber | string | nullAmount refunded so far, in major units.
paymentMethodstring | nullThe customer paid online after all (e.g. `card`): the order is no longer cash on delivery.
paymentUrlstring (uri) | nullReplace the order's online payment link.
الطلب
curl -X PATCH https://capiagent.com/api/ingest/v1/orders/10045 \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "shipped",
  "shipment": {
    "carrier": "SMSA",
    "trackingNumber": "290012345678",
    "trackingUrl": "https://track.example.com/290012345678"
  }
}'
الرد
JSON
{
  "ok": true,
  "succeeded": 1,
  "failed": 0,
  "results": [
    {
      "id": "10045",
      "ok": true
    }
  ]
}

POSTأرسل السلات

POSThttps://capiagent.com/api/ingest/v1/carts

أرسل السلة كاملة في كل مرة تتغير. السلة الفارغة تعني أنها أُغلقت.

الحقلالنوعمطلوبملاحظات
cartsobject[]نعم
carts[].idstringنعمYour cart or session id. Re-sending the same id updates the cart.
carts[].currencystring | null
carts[].totalnumber | string | null
carts[].itemsobject[]The whole cart every time, never a delta. An empty array closes the cart (bought or cleared).
carts[].items[].productIdstring | nullThe same id you send on POST /products — how the line links to the catalogue.
carts[].items[].namestring | null
carts[].items[].quantityinteger | null
carts[].items[].pricenumber | string | null
carts[].customerobjectContact for the recovery message. A phone is not marketing consent; sending is governed by the customer's subscription.
carts[].customer.namestring | null
carts[].customer.phonestring | null
carts[].customer.emailstring (email) | null
carts[].checkoutUrlstring (uri) | nullYour "resume this checkout" link, sent as-is in the recovery message. https only.
carts[].updatedAtstring | nullWhen the shopper last touched the cart (ISO 8601). Reminder timing is computed from it; a future value is clamped to now.
الطلب
curl -X POST https://capiagent.com/api/ingest/v1/carts \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "carts": [
    {
      "id": "cart-7f3a",
      "currency": "SAR",
      "total": 199.99,
      "items": [
        {
          "productId": "SKU-1001",
          "name": "Cotton shirt",
          "quantity": 1,
          "price": 199.99
        }
      ],
      "customer": {
        "name": "Sara",
        "phone": "966501234567"
      },
      "checkoutUrl": "https://mystore.com/checkout/cart-7f3a",
      "updatedAt": "2026-10-06T08:40:00Z"
    }
  ]
}'
الرد
JSON
{
  "ok": true,
  "succeeded": 1,
  "failed": 0,
  "results": [
    {
      "id": "cart-7f3a",
      "ok": true
    }
  ]
}

POSTأرسل العملاء

POSThttps://capiagent.com/api/ingest/v1/customers

للعملاء المسجّلين الذين قد لا يكونون طلبوا بعد، ولمزامنة إلغاء الاشتراك من نظامك إلينا.

الحقلالنوعمطلوبملاحظات
customersobject[]نعم
customers[].idstringنعمYour own customer id — echoed back in the results. Customers are matched on phone, then email.
customers[].namestring | null
customers[].phonestring | nullInternational, digits only (966501234567). A local number is refused with a warning.
customers[].emailstring (email) | nullUsed as the identity when there is no phone.
customers[].countrystring | null
customers[].citystring | null
customers[].acceptsMarketingboolean | nullThe customer's marketing choice in your system. `false` unsubscribes them at once. `true` confirms a subscription but never re-subscribes a customer who opted out themselves — a "STOP" on WhatsApp stands.
الطلب
curl -X POST https://capiagent.com/api/ingest/v1/customers \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "customers": [
    {
      "id": "U-501",
      "name": "Sara",
      "phone": "966501234567",
      "email": "sara@example.com",
      "city": "Riyadh",
      "acceptsMarketing": true
    },
    {
      "id": "U-502",
      "email": "omar@example.com",
      "acceptsMarketing": false
    }
  ]
}'
الرد
JSON
{
  "ok": true,
  "succeeded": 2,
  "failed": 0,
  "results": [
    {
      "id": "U-501",
      "ok": true
    },
    {
      "id": "U-502",
      "ok": true
    }
  ]
}

GETcheckCoupon

GEThttps://capiagent.com/api/ingest/v1/coupons/{id}
الطلب
curl -X GET https://capiagent.com/api/ingest/v1/coupons/CAPI-7K3M9 \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx"
الرد
JSON
{
  "ok": true,
  "valid": true,
  "coupon": {
    "code": "CAPI-7K3M9",
    "discountType": "percent",
    "percent": 10,
    "expiresAt": "2026-10-12T18:00:00.000Z",
    "usageLimit": 1
  }
}

POSTredeemCoupon

POSThttps://capiagent.com/api/ingest/v1/coupons/{id}/redeem
الحقلالنوعمطلوبملاحظات
orderIdstringYour order id (a string; a number is accepted; longer than 120 characters it is cut to 120). Send it: a retry for the same order (the first answer was lost) is answered 200 with `replayed: true`, not `already_used`. The body is optional — a missing or malformed body is ignored, it never blocks the redeem.
الطلب
curl -X POST https://capiagent.com/api/ingest/v1/coupons/CAPI-7K3M9/redeem \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
  "orderId": "10045"
}'
الرد
JSON
{
  "ok": true,
  "redeemed": true,
  "coupon": {
    "code": "CAPI-7K3M9",
    "discountType": "percent",
    "percent": 10,
    "expiresAt": "2026-10-12T18:00:00.000Z",
    "usageLimit": 1,
    "redeemedAt": "2026-10-06T10:00:00.000Z"
  }
}

DELETEreleaseCoupon

DELETEhttps://capiagent.com/api/ingest/v1/coupons/{id}/redeem
الطلب
curl -X DELETE https://capiagent.com/api/ingest/v1/coupons/CAPI-7K3M9/redeem \
  -H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx"
الرد
JSON
{
  "ok": true,
  "released": true
}

الأحداث (Webhooks)

رابط واحد في نظامك يستقبل كل الأحداث. كل حدث طلب POST موقّع، والجواب المطلوب 2xx.

الإعداد

  1. من «متجر مخصص» في اللوحة احفظ رابط النقطة (https فقط).
  2. انسخ سر التوقيع الذي يظهر مرة واحدة (capi_whsec_…) واحفظه في نظامك.
  3. اضغط «أرسل اختبار ping» وتأكد أن نقطتك تتحقق من التوقيع وتجيب 2xx.

الأحداث

coupon.createيطلب المساعد كود خصم لعميل. أنشئه وأجب خلال 10 ثوانٍ. لا يُعاد.
order.cod_confirmedالعميل أكّد طلب الدفع عند الاستلام على واتساب.
order.cod_cancelledالعميل ألغى طلب الدفع عند الاستلام. إذا كان cancelRequested صحيحًا فألغِ الطلب وأرجع المخزون.
customer.unsubscribedالعميل ألغى اشتراكه بالتسويق عندنا. ألغِه في قائمتك أيضًا.
pingحدث اختبار من زر اللوحة.
شكل الحدث
HTTP
POST https://your-store.com/capi/webhook
Content-Type: application/json
X-Capi-Event: order.cod_cancelled
X-Capi-Api-Version: v1
X-Capi-Event-Id: evt_cm9x2k1a80001
X-Capi-Delivery-Attempt: 1
X-Capi-Signature: t=1759740720,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
  "id": "evt_cm9x2k1a80001",
  "event": "order.cod_cancelled",
  "apiVersion": "v1",
  "createdAt": "2026-10-06T09:12:00.000Z",
  "data": {
    "orderId": "10045",
    "outcome": "cancelled",
    "cancelRequested": true,
    "note": "Capi Agent: the customer cancelled this cash-on-delivery order on WhatsApp.",
    "channel": "whatsapp",
    "respondedAt": "2026-10-06T09:12:00.000Z"
  }
}

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

الترويسة X-Capi-Signature تحمل t (الوقت) وv1 (HMAC-SHA256 للنص t.جسم الطلب الخام بسرّك). احسبه بنفسك وقارن بطريقة ثابتة الزمن، وارفض t الأقدم من 5 دقائق. قد تحمل الترويسة أكثر من v1 (عند تبديل السر) ومفاتيح لا تعرفها: التوقيع صحيح إن طابق أي v1، وتجاهل الباقي. استخدم جسم الطلب الخام كما وصل، لا بعد تحويله إلى كائن.

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

كل حدث عدا coupon.create وping يُعاد حتى 8 مرات خلال يوم ونصف (دقيقة، 5 دقائق، 30 دقيقة، ساعتان، 6، 12، 24 ساعة) بنفس id. خزّن المعرّفات التي عالجتها وتجاهل المكرر. إذا استنفدت المحاولات يصل التاجر إشعار، ويستطيع إعادة الإرسال من سجل الأحداث.

نقطة استقبال كاملة

تحقق، ثم تجاهل المكرر، ثم نفّذ، ثم أجب 2xx — بهذا الترتيب.

التحقق من التوقيع
import crypto from "node:crypto";

// Use the RAW request body — not a re-serialised object.
// The header may carry several v1= (while a secret is being rotated) and keys
// you do not know yet: it is valid when ANY v1 matches. Ignore the rest.
export function verifyCapiSignature(rawBody, header, secret) {
  const parts = header.split(",").map((p) => p.trim().split("="));
  const t = Number(parts.find(([key]) => key === "t")?.[1]);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // older than 5 minutes
  const expected = Buffer.from(crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"));
  return parts.some(
    ([key, value]) => key === "v1" && Buffer.byteLength(value ?? "") === expected.length && crypto.timingSafeEqual(expected, Buffer.from(value)),
  );
}
نقطة استقبال كاملة
Node.js (Express)
app.post("/capi/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyCapiSignature(raw, req.get("X-Capi-Signature") ?? "", process.env.CAPI_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  const event = JSON.parse(raw);
  if (await alreadyHandled(event.id)) return res.status(200).end(); // retries keep the same id

  switch (event.event) {
    case "coupon.create": {
      const coupon = await createCoupon(event.data);         // answer within 10 seconds
      await markHandled(event.id);
      return res.json({ code: coupon.code, id: coupon.id });
    }
    case "order.cod_cancelled":
      if (event.data.cancelRequested) await cancelOrder(event.data.orderId);
      else await addOrderNote(event.data.orderId, event.data.note);
      break;
    case "order.cod_confirmed":
      await markConfirmed(event.data.orderId);
      break;
    case "customer.unsubscribed":
      await unsubscribe(event.data.phone, event.data.email);
      break;
  }
  await markHandled(event.id);
  res.status(200).end();
});

الودجت وأحداث المتصفح

هذا الجزء يعمل في صفحات المتجر نفسها (JavaScript في المتصفح)، لا على السيرفر. به يظهر مساعد الدردشة، وبه تُربط تصرفات الزائر — مشاهدة منتج، إضافة للسلة، بدء الدفع — بالزائر نفسه، وعليها تقوم حملات اللايف شات ونسب المبيعات لها.

تثبيت الودجت

الصق السطر قبل نهاية وسم body في القالب المشترك لكل الصفحات، بما فيها الدفع وصفحة الشكر. استبدل STORE_ID بمعرّف متجرك من اللوحة. يظهر الودجت للزوار بعد «نشر الآن».

تثبيت الودجت
HTML
<script src="https://capiagent.com/widget.js" data-store-id="STORE_ID" async></script>

أحداث مسار الشراء — إلزامية

PRODUCT_VIEWعند تحميل صفحة كل منتج، وعند كل تنقّل لمنتج آخر في متجر SPA.
ADD_TO_CARTبعد نجاح كل إضافة للسلة، من أي زر: صفحة المنتج، القوائم، النافذة السريعة، «اشترِ الآن».
CHECKOUT_STARTEDعند فتح صفحة الدفع، ثم مرة ثانية فور إنشاء الطلب مع orderExternalId.

قواعد لا تُكسر

  • productExternalId هو نفس id المنتج في POST /products حرفيًا — لا sku ولا رقم المتغيّر.
  • orderExternalId هو نفس id الطلب في POST /orders — لا رقم السلة. به يُنسب الطلب لحملة اللايف شات.
  • إن كان كودك قد يعمل قبل تحميل الودجت فاستعمل AISA.q.push كما في المثال؛ الحدث يُحفظ ويُرسل عند التحميل.
  • بديل بلا JavaScript: data-capi-product-id على وسم الودجت في صفحة المنتج، وdata-capi-checkout="1" في صفحة الدفع.
  • حدث الشراء لا يُقبل من المتصفح: الطلبات والمبالغ تأتي من POST /orders فقط.
  • إن حدّد التاجر نطاقات مسموحة للودجت فأضف كل نطاقات المتجر، وإلا رُفضت الأحداث (403).
أحداث مسار الشراء — إلزامية
JavaScript
// Product page — on load (and on every SPA navigation to a product)
AISA.track("PRODUCT_VIEW", { productExternalId: "SKU-1001" });

// After every successful add to cart, from any button
AISA.track("ADD_TO_CART", { productExternalId: "SKU-1001", quantity: 1 });

// Checkout page opened
AISA.track("CHECKOUT_STARTED", {});

// Thank-you page — the order id you send to POST /orders
AISA.track("CHECKOUT_STARTED", { orderExternalId: "10045" });

// Safe before widget.js has loaded (queued, sent on load):
(window.AISA = window.AISA || {}).q = window.AISA.q || [];
window.AISA.q.push(["ADD_TO_CART", { productExternalId: "SKU-1001", quantity: 1 }]);

العميل المسجّل — AISA.identify

استدعه في كل صفحة ما دام العميل مسجّل الدخول، بجواله الدولي أو إيميله (والاسم اختياري). الودجت يرسله مرة واحدة ثم عند تغيّر البيانات فقط. هكذا لا يُسأل العميل عن بياناته في نموذج ما قبل المحادثة، وتجتمع محادثاته وطلباته ونسب الحملات على عميل واحد. لا يشترك أحد في التسويق بهذا النداء: الموافقة تصل عبر acceptsMarketing في POST /customers.

دليل جاهز لمساعد البرمجة

في لوحة Capi ← متجر مخصص، زر «انسخ دليل المطوّر» ينسخ دليلًا كاملًا بمعرّف متجرك: ماذا تحتاج كل ميزة، قائمة تنفيذ إلزامية، كل الحقول، وطلب جاهز لـ Claude أو ChatGPT ينفّذ الربط ويعطيك تقريرًا بندًا بندًا.

العميل المسجّل — AISA.identify
JavaScript
// Every page, while the customer is logged in (phone or email required)
AISA.identify({ name: "Sara", phone: "966501234567", email: "sara@example.com" });

// Or, before widget.js has loaded:
(window.AISA = window.AISA || {}).q = window.AISA.q || [];
window.AISA.q.push(["identify", { name: "Sara", phone: "966501234567", email: "sara@example.com" }]);

// Signed (recommended): add "hash", computed on YOUR server — see SIGNED_IDENTIFY_SAMPLE.
// Then the same customer keeps one chat history in every browser.
AISA.identify({ name: "Sara", email: "sara@example.com", hash: "<?= $capiHash ?>" });

الدعم

سؤال لم تجد جوابه هنا، أو طلب يُرفض ولا تعرف السبب؟ راسلنا ومعك رقم الطلب والرد كاملًا.

تواصل مع الدعم