البداية السريعة#
في خمس دقائق ستُنشئ جلسة لسلّة بقيمة 45,000 دينار، وتدفعها على الصفحة المستضافة عبر البوابة التجريبية، وتتحقّق منها من خادمك، وتستقبل webhook موقّعاً. كل ما هنا يعمل في وضع الاختبار؛ استبدل المفتاح بمفتاح إنتاج وستأخذ الاستدعاءات نفسها مالاً حقيقياً.
قبل أن تبدأ#
تحتاج من Povito:
- مفتاح اختبار سرّي —
povito_ck_test_…— يُعرض مرّة واحدة عند إصداره؛ أبقِه على خادمك فقط؛ - تسجيل نطاقات
success_urlوcancel_urlعلى حساب التاجر (وإلاreturn_host_not_allowed)؛ - تفعيل طريقة الدفع
testعلى تاجرك في وضع الاختبار. تأكّد عبرGET /v1/payment_methods: يجب أن يكون الصفّ الذي فيه"code": "test"يحمل"enabled": true.
curl https://api.checkout.povito.com/v1/payment_methods \
-H "Authorization: Bearer $POVITO_SECRET_KEY"المبالغ
المبالغ أعداد صحيحة بأصغر وحدة للعملة. الدينار العراقي ليست له وحدة أصغر، فـ 45000 تعني 45,000 دينار. أما الدولار فوحدته السنت. تُرفض الكسور العشرية والنصوص بالخطأ validation_error.
1. أنشئ جلسة#
يحتاج POST /v1/checkout/sessions إلى ترويسة Idempotency-Key (المتعارف عليه أن تكون UUID) حتى لا يُنشئ طلب مُعاد جلستين. ويجب أن تتطابق line_items مع amount: بنود charge وshipping وfee تُجمع، وdiscount يُطرح — 42,000 + 5,000 − 2,000 = 45,000.
curl -X POST https://api.checkout.povito.com/v1/checkout/sessions \
-H "Authorization: Bearer $POVITO_SECRET_KEY" \
-H "Idempotency-Key: 7d9d2a1e-3b0f-4c7e-9a1d-2f6b8c1e5a90" \
-H "Content-Type: application/json" \
-d '{
"reference_id": "POV-1041",
"amount": 45000,
"currency": "IQD",
"line_items": [
{ "label": "السلّة", "amount": 42000, "type": "charge" },
{ "label": "التوصيل — Hi-Express", "amount": 5000, "type": "shipping" },
{ "label": "قسيمة RAMADAN", "amount": 2000, "type": "discount" }
],
"payment_method_types": ["test"],
"customer": { "phone": "+9647500000000", "name": "علي محمد إسماعيل" },
"locale": "ar",
"success_url": "https://your-store.example/checkout/return",
"cancel_url": "https://your-store.example/cart",
"metadata": { "cart_id": "cart_01J8Z3K9Q4M2XA7B" }
}'import { randomUUID } from "node:crypto"
const API = "https://api.checkout.povito.com/v1"
const headers = {
Authorization: `Bearer ${process.env.POVITO_SECRET_KEY}`,
"Content-Type": "application/json",
}
const res = await fetch(`${API}/checkout/sessions`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": randomUUID() },
body: JSON.stringify({
reference_id: "POV-1041",
amount: 45000, // 45,000 دينار — الدينار ليست له وحدة أصغر
currency: "IQD",
line_items: [
{ label: "السلّة", amount: 42000, type: "charge" },
{ label: "التوصيل — Hi-Express", amount: 5000, type: "shipping" },
{ label: "قسيمة RAMADAN", amount: 2000, type: "discount" },
],
payment_method_types: ["test"],
customer: { phone: "+9647500000000", name: "علي محمد إسماعيل" },
locale: "ar",
success_url: "https://your-store.example/checkout/return",
cancel_url: "https://your-store.example/cart",
metadata: { cart_id: "cart_01J8Z3K9Q4M2XA7B" },
}),
})
if (res.status !== 201 && res.status !== 200) throw new Error(await res.text())
const session = await res.json()
// خزّن session.id مع طلبك قبل التوجيه؛ هكذا يمكن استرجاع أي webhook ضائع.
await orders.attachCheckoutSession("POV-1041", session.id)الاستجابة هي الجلسة كاملةً. 201 تعني أنّها أُنشئت؛ و200 تعني أنّ جلسة مفتوحة بهذا reference_id وبالسعر نفسه كانت موجودة في هذا الوضع فأُعيدت كما هي. (وإن تغيّر السعر — عُدّلت السلّة — تُنهى الجلسة المفتوحة وتُنشأ جلسة جديدة بـ 201.)
{
"id": "cs_test_7KQ4M2XA9BC3DEFGH1JK",
"object": "checkout.session",
"code": "7KQ4M2XA9BC3DEFGH1JKMNPQRS",
"url": "https://checkout.povito.com/c/7KQ4M2XA9BC3DEFGH1JKMNPQRS",
"livemode": false,
"status": "open",
"payment_status": "unpaid",
"reference_id": "POV-1041",
"amount": 45000,
"currency": "IQD",
"line_items": [
{ "label": "السلّة", "amount": 42000, "quantity": 1, "type": "charge" },
{ "label": "التوصيل — Hi-Express", "amount": 5000, "quantity": 1, "type": "shipping" },
{ "label": "قسيمة RAMADAN", "amount": 2000, "quantity": 1, "type": "discount" }
],
"payment_method_types": ["test"],
"customer": { "id": null, "phone": "+9647500000000", "name": "علي محمد إسماعيل", "address": null },
"collect": { "phone": "required", "name": "required", "shipping_address": "none" },
"presentment": null,
"payment": null,
"refunded_amount": 0,
"locale": "ar",
"success_url": "https://your-store.example/checkout/return",
"cancel_url": "https://your-store.example/cart",
"expires_at": "2026-09-10T13:00:00.000Z",
"completed_at": null,
"metadata": { "cart_id": "cart_01J8Z3K9Q4M2XA7B" },
"created_at": "2026-09-10T12:00:00.000Z",
"updated_at": "2026-09-10T12:00:00.000Z"
}تنتهي الجلسات افتراضياً بعد 60 دقيقة من إنشائها (يقبل expires_at من 5 دقائق إلى 24 ساعة). خزّن id مع طلبك قبل التوجيه.
2. وجّه المتسوّق إلى url#
أعد توجيه المتصفّح (302) أو افتح الرابط في متصفّح داخل التطبيق. على الصفحة يُدخل المتسوّق رقم هاتفه ورمز الرسالة النصية، ويؤكّد الاسم والعنوان بحسب إعدادات collect، ويختار طريقة الدفع ويدفع.
في وضع الاختبار استخدم رقم الهاتف التجريبي +9647500000000 مع الرمز 000000 — لا تُرسل أي رسالة. تعرض الطريقة test صفحة مزوّد وهمية فيها الأزرار نجاح / فشل / إلغاء / ياخذ 90 ثانية بدل مصرف حقيقي. اضغط نجاح.
عند اكتمال الجلسة تُرسل الصفحة المتسوّق إلى:
GET https://your-store.example/checkout/return?session_id=cs_test_7KQ4M2XA9BC3DEFGH1JK&reference_id=POV-1041لا تُوضع أي حالة في هذا الرابط أبداً، فلا شيء يمكن تزويره. وإن ألغى المتسوّق، يُرسل إلى cancel_url دون أي إضافة.
3. تحقّق عبر GET#
في صفحة العودة اقرأ session_id من الاستعلام، ثم اسأل Povito عمّا حدث فعلاً. هذا هو استدعاء التحقّق، وجوابه هو المرجع.
curl https://api.checkout.povito.com/v1/checkout/sessions/cs_test_7KQ4M2XA9BC3DEFGH1JK \
-H "Authorization: Bearer $POVITO_SECRET_KEY"const res = await fetch(`${API}/checkout/sessions/${encodeURIComponent(sessionId)}`, { headers })
const session = await res.json()
if (session.reference_id !== order.reference_id) throw new Error("الجلسة لا تخصّ هذا الطلب")
if (session.status === "completed" && session.payment_status === "paid") {
await fulfil(order, session.payment) // captured_amount, captured_currency, method, gateway_reference
} else if (session.payment_status === "requires_offline_collection") {
await scheduleCashCollection(order) // دفع عند الاستلام — لم يُحوَّل أي مبلغ
} else {
// open / processing: ما زال يدفع. expired / canceled: اعرض المحاولة مجدداً.
}{
"id": "cs_test_7KQ4M2XA9BC3DEFGH1JK",
"object": "checkout.session",
"code": "7KQ4M2XA9BC3DEFGH1JKMNPQRS",
"url": "https://checkout.povito.com/c/7KQ4M2XA9BC3DEFGH1JKMNPQRS",
"livemode": false,
"status": "completed",
"payment_status": "paid",
"reference_id": "POV-1041",
"amount": 45000,
"currency": "IQD",
"line_items": [
{ "label": "السلّة", "amount": 42000, "quantity": 1, "type": "charge" },
{ "label": "التوصيل — Hi-Express", "amount": 5000, "quantity": 1, "type": "shipping" },
{ "label": "قسيمة RAMADAN", "amount": 2000, "quantity": 1, "type": "discount" }
],
"payment_method_types": ["test"],
"customer": { "id": null, "phone": "+9647500000000", "name": "علي محمد إسماعيل", "address": null },
"collect": { "phone": "required", "name": "required", "shipping_address": "none" },
"presentment": null,
"payment": {
"attempt_id": "pa_test_3M8XQ2K7A9BC4DEFGH1J",
"method": "test",
"gateway": "test",
"gateway_reference": "test_pa_test_3M8XQ2K7A9BC4DEFGH1J",
"captured_amount": 45000,
"captured_currency": "IQD",
"presented_amount": 45000,
"presented_currency": "IQD",
"fx_rate": null,
"captured_at": "2026-09-10T12:04:31.000Z",
"instrument": null
},
"refunded_amount": 0,
"locale": "ar",
"success_url": "https://your-store.example/checkout/return",
"cancel_url": "https://your-store.example/cart",
"expires_at": "2026-09-10T13:00:00.000Z",
"completed_at": "2026-09-10T12:04:31.000Z",
"metadata": { "cart_id": "cart_01J8Z3K9Q4M2XA7B" },
"created_at": "2026-09-10T12:00:00.000Z",
"updated_at": "2026-09-10T12:04:31.000Z"
}يبقى payment بقيمة null حتى تنجح محاولة دفع. وcaptured_amount هو ما أبلغ المزوّد أنّه حصّله، بالعملة التي حصّله بها — ولم تَعدّ Povito المحاولة ناجحة إلا لأنّه ساوى ما عُرض على المتسوّق.
4. عالج الـ webhook#
المتسوّقون يغلقون الصفحات. سجّل نقطة HTTPS مرّة واحدة وسترسل إليها Povito الحدث checkout.session.completed (وبقية أنواع الأحداث). يُعاد سرّ التوقيع secret في هذه الاستجابة فقط.
curl -X POST https://api.checkout.povito.com/v1/webhook_endpoints \
-H "Authorization: Bearer $POVITO_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-store.example/webhooks/povito", "events": ["*"], "mode": "test" }'{
"id": "we_2K7A9BC4DEFGH1J3M8XQ",
"object": "webhook_endpoint",
"url": "https://your-store.example/webhooks/povito",
"events": ["*"],
"mode": "test",
"status": "enabled",
"disabled_reason": null,
"description": null,
"created_at": "2026-09-10T11:58:00.000Z",
"secret": "whsec_9Xp2fQ7Lw4vB1nR8sT6uY3zA0cD5eF2gH7jK4mN1pQ8"
}يحمل كل إرسال الترويسة Povito-Signature: t=<unix>,v1=<hex> حيث v1 = HMAC-SHA256(secret, "<t>.<raw body>"). تحقّق من البايتات الخام قبل تحليل JSON، وارفض ما يزيد عمره على خمس دقائق، ثم تعامل مع الحدث كتنبيه لتنفيذ الخطوة 3.
import { createHmac, timingSafeEqual } from "node:crypto"
import express from "express"
const app = express()
function verify(rawBody, header, secret, now = Math.floor(Date.now() / 1000)) {
const parts = (header ?? "").split(",").map((p) => p.trim().split("="))
const t = Number(parts.find((p) => p[0] === "t")?.[1])
if (!Number.isFinite(t) || Math.abs(now - t) > 300) return false
const expected = Buffer.from(createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"), "hex")
return parts
.filter((p) => p[0] === "v1")
.some((p) => {
const given = Buffer.from(p[1] ?? "", "hex")
return given.length === expected.length && timingSafeEqual(given, expected)
})
}
app.post("/webhooks/povito", express.raw({ type: "application/json" }), async (req, res) => {
if (!verify(req.body, req.header("povito-signature"), process.env.POVITO_WEBHOOK_SECRET)) return res.status(401).end()
const event = JSON.parse(req.body.toString("utf8"))
if (await events.alreadySeen(event.id)) return res.status(200).end() // الإرسال يتكرّر؛ كن idempotent
if (event.type === "checkout.session.completed") {
const r = await fetch(`${API}/checkout/sessions/${event.data.object.id}`, { headers })
const session = await r.json() // الحقيقة، لا الحمولة
if (session.payment_status === "paid") await fulfil(session.reference_id, session.payment)
if (session.payment_status === "requires_offline_collection") await scheduleCashCollection(session.reference_id)
}
await events.markSeen(event.id)
res.status(200).end() // أي شيء خارج 2xx يُعاد إرساله: 1 د، 5 د، 30 د، 2 س، 6 س، 24 س، 24 س
}){
"id": "evt_test_4DEFGH1J3M8XQ2K7A9BC",
"object": "event",
"type": "checkout.session.completed",
"livemode": false,
"created_at": "2026-09-10T12:04:31.000Z",
"data": {
"object": {
"id": "cs_test_7KQ4M2XA9BC3DEFGH1JK",
"object": "checkout.session",
"status": "completed",
"payment_status": "paid",
"reference_id": "POV-1041",
"amount": 45000,
"currency": "IQD",
"payment": { "attempt_id": "pa_test_3M8XQ2K7A9BC4DEFGH1J", "method": "test", "captured_amount": 45000, "captured_currency": "IQD", "…": "…" },
"…": "الجلسة كاملةً كما يعيدها GET"
}
}
}دوال التحقّق الكاملة بلغات PHP وPython وDart، وجدول إعادة المحاولة، وإعادة الإرسال — كلها في دليل الـ webhooks.
الانتقال إلى الإنتاج#
- اطلب من Povito مفاتيح الإنتاج وفعّل طرق الدفع الحقيقية (
zaincash،fib،fib_card،card،cashondelivery، …) على تاجر الإنتاج — راجع طرق الدفع. - سجّل نقطة webhook ثانية بـ
"mode": "live"؛ أحداث الإنتاج لا تذهب إلا إلى نقاط الإنتاج. - استبدل
payment_method_types: ["test"]بطرق الإنتاج، أو احذف الحقل لعرض كل الطرق المفعّلة. - أبقِ كل شيء آخر كما هو. شكل الجلسة، وعقد رابط العودة، واستدعاء التحقّق — كلها متطابقة.
تفضّل عميلاً مكتوباً بالأنواع؟
@povito/checkout-node يغلّف هذه الاستدعاءات، ويولّد مفاتيح idempotency، ويعيد المحاولة بأمان، ويتحقّق من تواقيع الـ webhook.