Capi Agent API — واجهة برمجة المتجر المخصص
يرسل نظام متجرك المنتجات والطلبات والسلات المتروكة والعملاء، فيعمل مساعد Capi Agent والحملات والأتمتة تمامًا كما تعمل على متجر سلة أو Shopify. وفي الاتجاه المعاكس يستقبل نظامك أحداثًا موقّعة: كوبونات الخصم، وتأكيد العميل لطلبه أو إلغاؤه، وإلغاء الاشتراك.
الصق الدليل في Claude أو ChatGPT ليربط متجرك، أو استورد ملف OpenAPI في Postman أو Insomnia.
https://capiagent.com/api/ingest/v1المحتويات
البدء السريع
- 1من لوحة Capi Agent افتح «التكاملات» ثم «متجر مخصص»، وأنشئ مفتاحًا تجريبيًا أولًا.
- 2تحقق من المفتاح بطلب GET /ping.
- 3أرسل المنتجات عبر POST /products، ثم كل طلب عبر POST /orders، وحدّث حالته بـPATCH /orders/{id}.
- 4عندما تنجح كل الطلبات بالمفتاح التجريبي، أنشئ مفتاحًا فعليًا وبدّله في نظامك.
- 5اختياري: أضف رابط الأحداث ليصلك الكوبون وتأكيد الطلبات وإلغاء الاشتراك.
- 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.
- 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 ينهي التخفيض ويمسح السعر قبل الخصم.
- 15.10.2026
- هوية العميل الموقّعة في AISA.identify (hash) لدمج محادثاته بين الأجهزة.
- 08.10.2026
- tracking في POST /orders لاحتساب مبيعات الإعلانات.
- items[].productId وcouponCode في الطلبات.
- الودجت وأحداث المتصفح في المتجر.
- عملة المتجر تُحدَّد من أول بيانات تصل.
- 06.10.2026
- المفاتيح التجريبية capi_test_.
- POST /customers وPATCH /products/:id.
- أحداث order.cod_confirmed وorder.cod_cancelled وcustomer.unsubscribed بإعادة المحاولة.
- تحقق الكوبون وصرفه وإلغاء صرفه.
- ملف OpenAPI وصفحة المطوّرين.
- 05.10.2026
- POST /carts للسلات المتروكة.
- حدث coupon.create.
- 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تحقق من المفتاح
https://capiagent.com/api/ingest/v1/pingيعيد اسم البراند وعملته ونوع المفتاح (فعلي أو تجريبي).
curl -X GET https://capiagent.com/api/ingest/v1/ping \
-H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx"{
"ok": true,
"mode": "live",
"store": {
"name": "Lumi Store",
"currency": "SAR"
}
}POSTأرسل المنتجات أو حدّثها
https://capiagent.com/api/ingest/v1/productsحتى 500 منتج في الطلب. المطلوب id وname فقط. أرسل complete: true فقط إن كان الطلب يحوي كتالوجك كاملًا.
| الحقل | النوع | مطلوب | ملاحظات |
|---|---|---|---|
products | object[] | نعم | |
products[].id | string | نعم | Your own product id. Every request is matched on it — re-sending the same id updates the product, never duplicates it. |
products[].name | string | نعم | Product name as the customer sees it. |
products[].description | string | null | Plain text or HTML. HTML is stripped for the assistant. | |
products[].price | number | string | null | Major 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[].salePrice | number | string | null | The current price when the product is discounted. | |
products[].compareAtPrice | number | string | null | An explicit "was" price, when you keep one separately. | |
products[].costPrice | number | string | null | What 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[].currency | string | null | ISO 4217 code. Defaults to the brand's currency. | |
products[].sku | string | null | Stock keeping unit. | |
products[].url | string (uri) | null | Public product page — the link the assistant sends. | |
products[].images | string (uri)[] | Image URLs, main image first. Up to 20. | |
products[].categories | string[] | Category names, broadest first. | |
products[].stock | integer | null | Units available. Decides availability; 0 means out of stock. | |
products[].availability | "in_stock" | "out_of_stock" | null | Used only when `stock` is not sent. Without either, availability is "unknown" and the assistant never claims it is sold out. | |
products[].published | boolean | `false` archives the product immediately (it stays in order history). | |
products[].variants | object[] | Sizes, colours and other options, each with its own id, price and stock. | |
products[].variants[].id | string | نعم | |
products[].variants[].name | string | null | ||
products[].variants[].sku | string | null | ||
products[].variants[].price | number | string | null | ||
products[].variants[].compareAtPrice | number | string | null | ||
products[].variants[].costPrice | number | string | null | ||
products[].variants[].stock | integer | null | ||
complete | boolean | `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
}
]
}
]
}'{
"ok": true,
"succeeded": 1,
"failed": 0,
"results": [
{
"id": "SKU-1001",
"ok": true
}
]
}PATCHpatchProduct
https://capiagent.com/api/ingest/v1/products/{id}| الحقل | النوع | مطلوب | ملاحظات |
|---|---|---|---|
price | number | string | null | ||
salePrice | number | string | null | ||
compareAtPrice | number | string | null | ||
costPrice | number | string | null | ||
stock | integer | null | ||
availability | "in_stock" | "out_of_stock" | null | ||
published | boolean |
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
}'{
"ok": true,
"succeeded": 1,
"failed": 0,
"results": [
{
"id": "SKU-1001",
"ok": true
}
]
}DELETEأرشف منتجًا
https://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"{
"ok": true,
"succeeded": 1,
"failed": 0,
"results": [
{
"id": "SKU-1001",
"ok": true
}
]
}POSTأرسل الطلبات أو حدّثها
https://capiagent.com/api/ingest/v1/ordersأرسل كل طلب جديد. backfill: true لاستيراد طلبات قديمة دون إطلاق رسائل أو أتمتة.
| الحقل | النوع | مطلوب | ملاحظات |
|---|---|---|---|
orders | object[] | نعم | |
orders[].id | string | نعم | Your order number. Re-sending the same id updates the order; its items are replaced in full. |
orders[].status | string | نعم | 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[].statusName | string | null | Optional. 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[].total | number | string | نعم | Order total in major units. |
orders[].currency | string | ISO 4217 code. Defaults to the brand's currency. | |
orders[].placedAt | string | ISO 8601. A date in the future is refused. | |
orders[].customer | object | Who 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.name | string | null | ||
orders[].customer.phone | string | null | ||
orders[].customer.email | string (email) | null | ||
orders[].items | object[] | 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[].productId | string | null | ||
orders[].items[].name | string | نعم | |
orders[].items[].quantity | integer | نعم | |
orders[].items[].price | number | string | نعم | |
orders[].subtotal | number | string | null | ||
orders[].discount | number | string | null | ||
orders[].tax | number | string | null | ||
orders[].shipping | number | string | null | ||
orders[].refunded | number | string | null | ||
orders[].shipment | object | Carrier and tracking — the assistant answers "where is my order?" from these fields. | |
orders[].shipment.carrier | string | null | ||
orders[].shipment.trackingNumber | string | null | ||
orders[].shipment.trackingUrl | string (uri) | null | ||
orders[].shipment.shippedAt | string | null | ||
orders[].shipment.deliveredAt | string | null | ||
orders[].shipment.estimatedDeliveryAt | string | null | ||
orders[].paymentMethod | string | null | `cod` (or `cash_on_delivery`) enables cash-on-delivery confirmation over WhatsApp for this order. | |
orders[].paymentUrl | string (uri) | null | An 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[].couponCode | string | null | ||
orders[].backfill | boolean | `true` stores the order without firing automations, notifications or messages — for importing history. | |
orders[].tracking | object | Optional. 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.landingUrl | string (uri) | null | ||
orders[].tracking.userAgent | string | null | ||
orders[].tracking.ip | string | null | ||
orders[].tracking.fbp | string | null | ||
orders[].tracking.fbc | string | null | ||
orders[].tracking.clickIds | object | ||
orders[].tracking.clickIds.fbclid | string | ||
orders[].tracking.clickIds.gclid | string | ||
orders[].tracking.clickIds.gbraid | string | ||
orders[].tracking.clickIds.wbraid | string | ||
orders[].tracking.clickIds.ttclid | string | ||
orders[].tracking.clickIds.ScCid | string |
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"
}
]
}'{
"ok": true,
"succeeded": 1,
"failed": 0,
"results": [
{
"id": "10045",
"ok": true
}
]
}PATCHحدّث حالة طلب أو شحنه
https://capiagent.com/api/ingest/v1/orders/{id}الحقول التي ترسلها فقط هي التي تتغير. تغيّر الحالة يطلق رسالة حالة الطلب للعميل حسب إعدادات التاجر.
| الحقل | النوع | مطلوب | ملاحظات |
|---|---|---|---|
status | string | New status — one of ours or your system's own, as on POST /orders. Fires order.status_changed like any platform. | |
statusName | string | null | Optional. The status's name in your store's admin. | |
shipment | object | Only the fields you send change; a status-only patch keeps the stored tracking number. | |
shipment.carrier | string | null | ||
shipment.trackingNumber | string | null | ||
shipment.trackingUrl | string (uri) | null | ||
shipment.shippedAt | string | null | ||
shipment.deliveredAt | string | null | ||
shipment.estimatedDeliveryAt | string | null | ||
refunded | number | string | null | Amount refunded so far, in major units. | |
paymentMethod | string | null | The customer paid online after all (e.g. `card`): the order is no longer cash on delivery. | |
paymentUrl | string (uri) | null | Replace 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"
}
}'{
"ok": true,
"succeeded": 1,
"failed": 0,
"results": [
{
"id": "10045",
"ok": true
}
]
}POSTأرسل السلات
https://capiagent.com/api/ingest/v1/cartsأرسل السلة كاملة في كل مرة تتغير. السلة الفارغة تعني أنها أُغلقت.
| الحقل | النوع | مطلوب | ملاحظات |
|---|---|---|---|
carts | object[] | نعم | |
carts[].id | string | نعم | Your cart or session id. Re-sending the same id updates the cart. |
carts[].currency | string | null | ||
carts[].total | number | string | null | ||
carts[].items | object[] | The whole cart every time, never a delta. An empty array closes the cart (bought or cleared). | |
carts[].items[].productId | string | null | The same id you send on POST /products — how the line links to the catalogue. | |
carts[].items[].name | string | null | ||
carts[].items[].quantity | integer | null | ||
carts[].items[].price | number | string | null | ||
carts[].customer | object | Contact for the recovery message. A phone is not marketing consent; sending is governed by the customer's subscription. | |
carts[].customer.name | string | null | ||
carts[].customer.phone | string | null | ||
carts[].customer.email | string (email) | null | ||
carts[].checkoutUrl | string (uri) | null | Your "resume this checkout" link, sent as-is in the recovery message. https only. | |
carts[].updatedAt | string | null | When 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"
}
]
}'{
"ok": true,
"succeeded": 1,
"failed": 0,
"results": [
{
"id": "cart-7f3a",
"ok": true
}
]
}POSTأرسل العملاء
https://capiagent.com/api/ingest/v1/customersللعملاء المسجّلين الذين قد لا يكونون طلبوا بعد، ولمزامنة إلغاء الاشتراك من نظامك إلينا.
| الحقل | النوع | مطلوب | ملاحظات |
|---|---|---|---|
customers | object[] | نعم | |
customers[].id | string | نعم | Your own customer id — echoed back in the results. Customers are matched on phone, then email. |
customers[].name | string | null | ||
customers[].phone | string | null | International, digits only (966501234567). A local number is refused with a warning. | |
customers[].email | string (email) | null | Used as the identity when there is no phone. | |
customers[].country | string | null | ||
customers[].city | string | null | ||
customers[].acceptsMarketing | boolean | null | The 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
}
]
}'{
"ok": true,
"succeeded": 2,
"failed": 0,
"results": [
{
"id": "U-501",
"ok": true
},
{
"id": "U-502",
"ok": true
}
]
}GETcheckCoupon
https://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"{
"ok": true,
"valid": true,
"coupon": {
"code": "CAPI-7K3M9",
"discountType": "percent",
"percent": 10,
"expiresAt": "2026-10-12T18:00:00.000Z",
"usageLimit": 1
}
}POSTredeemCoupon
https://capiagent.com/api/ingest/v1/coupons/{id}/redeem| الحقل | النوع | مطلوب | ملاحظات |
|---|---|---|---|
orderId | string | Your 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"
}'{
"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
https://capiagent.com/api/ingest/v1/coupons/{id}/redeemcurl -X DELETE https://capiagent.com/api/ingest/v1/coupons/CAPI-7K3M9/redeem \
-H "Authorization: Bearer capi_live_xxxxxxxxxxxxxxxxxxxxxxxx"{
"ok": true,
"released": true
}الأحداث (Webhooks)
رابط واحد في نظامك يستقبل كل الأحداث. كل حدث طلب POST موقّع، والجواب المطلوب 2xx.
الإعداد
- من «متجر مخصص» في اللوحة احفظ رابط النقطة (https فقط).
- انسخ سر التوقيع الذي يظهر مرة واحدة (capi_whsec_…) واحفظه في نظامك.
- اضغط «أرسل اختبار ping» وتأكد أن نقطتك تتحقق من التوقيع وتجيب 2xx.
الأحداث
coupon.create | يطلب المساعد كود خصم لعميل. أنشئه وأجب خلال 10 ثوانٍ. لا يُعاد. |
order.cod_confirmed | العميل أكّد طلب الدفع عند الاستلام على واتساب. |
order.cod_cancelled | العميل ألغى طلب الدفع عند الاستلام. إذا كان cancelRequested صحيحًا فألغِ الطلب وأرجع المخزون. |
customer.unsubscribed | العميل ألغى اشتراكه بالتسويق عندنا. ألغِه في قائمتك أيضًا. |
ping | حدث اختبار من زر اللوحة. |
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)),
);
}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 بمعرّف متجرك من اللوحة. يظهر الودجت للزوار بعد «نشر الآن».
<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).
// 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 ينفّذ الربط ويعطيك تقريرًا بندًا بندًا.
// 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 ?>" });الدعم
سؤال لم تجد جوابه هنا، أو طلب يُرفض ولا تعرف السبب؟ راسلنا ومعك رقم الطلب والرد كاملًا.
تواصل مع الدعم