مكتبة Node.js
استخدم المكتبة الرسمية @teyra/pay لقبول المدفوعات والتنقل بين صفحات القوائم وإعادة المحاولة بأمان والتحقق من الـ Webhooks في Node.js وTypeScript.
لم تُنشر بعد
مكتبة @teyra/pay جاهزة ومُختبرة لكنها غير منشورة على npm بعد، لذلك لن يعمل الأمر npm install @teyra/pay حتى إصدارها. تواصل مع support@teyra.co للحصول على وصول مبكر. وإلى ذلك الحين استخدم واجهة REST مباشرةً كما في البدء السريع.
@teyra/pay هي مكتبة تيرا باي الرسمية لـ Node.js. مكتوبة بلغة TypeScript، وتأتي بصيغتي ESM وCommonJS، ولا تعتمد على أي حزم أخرى أثناء التشغيل.
- دوال بأنواع (types) واضحة للمدفوعات والاستردادات وروابط الدفع والإيداعات ونقاط نهاية الـ Webhooks
- تنقّل تلقائي بين الصفحات باستخدام
for await - ترويسة
Idempotency-Keyمع كل طلبPOST، تُستخدم نفسها في كل إعادة محاولة - إعادة محاولة تلقائية عند أخطاء الشبكة و
429و5xx - أخطاء بأنواع محددة تحمل
codeوstatusوdetails - دوال للمبالغ لا تستخدم الأعداد العشرية العائمة (floats)
- التحقق من تواقيع الـ Webhooks
التثبيت
تتطلب Node.js 18 أو أحدث.
npm install @teyra/payالبدء السريع
أنشئ عميلًا بمفتاحك السري، ثم أنشئ دفعة ووجّه العميل إلى checkout_url.
import { TeyraPay, toMinor } from '@teyra/pay';
const teyra = new TeyraPay(process.env.TEYRA_SECRET_KEY!); // sk_test_… or sk_live_…
const payment = await teyra.payments.create({
amount: toMinor('12.500', 'KWD'), // 12500
currency: 'KWD',
return_url: 'https://shop.example.com/thanks',
reference: 'order-1042',
customer: { name: 'Sara Ali', email: 'sara@example.com' },
metadata: { order_id: '1042' },
});
// Redirect the customer to the hosted checkout page
res.redirect(303, payment.checkout_url);عندما يعود العميل إلى return_url، تأكّد من النتيجة من خادمك ولا تعتمد على إعادة التوجيه وحدها:
const payment = await teyra.payments.retrieve(req.query.payment_id as string);
if (payment.status === 'succeeded') {
// mark the order as paid
}تعمل المكتبة مع CommonJS أيضًا: const { TeyraPay } = require('@teyra/pay');
أبقِ المفتاح السري على خادمك
لا تضع مفاتيح sk_test_… أو sk_live_… في المتصفح أو تطبيقات الجوال. حمّلها من متغير بيئة. راجع المصادقة.
خيارات العميل
const teyra = new TeyraPay(process.env.TEYRA_SECRET_KEY!, {
baseUrl: 'https://api.teyra.co', // default
timeoutMs: 30_000, // per attempt, default 30 seconds
maxRetries: 2, // default 2
});المفتاح هو ما يحدد الوضع: مفاتيح الاختبار تُنشئ كائنات اختبار (livemode: false) والمفاتيح الحية تُنشئ كائنات حية. راجع وضع الاختبار والوضع الحي.
الدوال
| الدالة | نقطة النهاية |
|---|---|
payments.create(params) | POST /v1/payments |
payments.retrieve(id) | GET /v1/payments/:id |
payments.list(params) | GET /v1/payments |
refunds.create(paymentId, params) | POST /v1/payments/:id/refunds |
refunds.retrieve(id) | GET /v1/refunds/:id |
refunds.list(params) | GET /v1/refunds |
paymentLinks.create(params) | POST /v1/payment-links |
paymentLinks.retrieve(id) | GET /v1/payment-links/:id |
paymentLinks.list(params) | GET /v1/payment-links |
paymentLinks.deactivate(id) | POST /v1/payment-links/:id/deactivate |
deposits.list(params) | GET /v1/deposits |
webhookEndpoints.create(params) | POST /v1/webhook-endpoints |
webhookEndpoints.list() | GET /v1/webhook-endpoints |
webhookEndpoints.delete(id) | DELETE /v1/webhook-endpoints/:id |
المعاملات والاستجابات مطابقة لما في مرجع الـ API. وتقبل كل دالة خيارات الطلب في آخر معامل: { idempotencyKey, timeoutMs, maxRetries, signal }.
// Partial refund
const refund = await teyra.refunds.create('pay_01JB…', { amount: 5000, reason: 'requested_by_customer' });
// Shareable payment link
const link = await teyra.paymentLinks.create({ amount: 5000, currency: 'KWD', description: 'Consultation' });
console.log(link.url);ترقيم الصفحات
تعيد list() قائمة صفحات. استخدم await للحصول على صفحة واحدة، أو مرّ عليها بـ for await لتمر على كل الصفحات. لا تطلب المكتبة الصفحة التالية باستخدام next_cursor إلا عند الوصول إليها، وتحتفظ بعوامل التصفية في كل طلب.
// One page
const page = await teyra.payments.list({ status: 'succeeded', limit: 50 });
console.log(page.data.length, page.has_more, page.next_cursor);
// Every item, across pages
for await (const payment of teyra.payments.list({ status: 'succeeded', created_gte: '2026-10-01T00:00:00Z' })) {
console.log(payment.id, payment.amount);
}
// Collect up to 200 items
const refunds = await teyra.refunds.list({ payment_id: 'pay_01JB…' }).toArray(200);
// Page by page
for await (const p of teyra.deposits.list({ limit: 100 }).pages()) {
console.log(p.data.length);
}راجع ترقيم الصفحات لمعرفة طريقة عمل المؤشرات.
منع التكرار
ترسل المكتبة ترويسة Idempotency-Key مع كل طلب POST. إذا لم تمرّر مفتاحًا، تولّد المكتبة معرّف UUID وتستخدمه نفسه في كل إعادة محاولة للاستدعاء، فلا يمكن أن يُنشئ الطلب المعاد دفعة أو استردادًا ثانيًا.
إذا كانت العملية نفسها قد تُنفَّذ مرة أخرى لاحقًا، مثل مهمة في طابور أو زر ضُغط مرتين، فمرّر مفتاحك أنت مشتقًا من رقم الطلب:
await teyra.payments.create(params, { idempotencyKey: 'order-1042-attempt-1' });
await teyra.refunds.create('pay_01JB…', { amount: 5000 }, { idempotencyKey: 'refund-order-1042-item-3' });استخدام المفتاح نفسه مع جسم مختلف يرمي TeyraPayIdempotencyError (409 idempotency_conflict). راجع منع التكرار.
الأخطاء وإعادة المحاولة
كل خطأ من الـ API هو TeyraPayError يحمل code وstatus وmessage وdetails، وتتيح لك الأصناف الفرعية التفريق بين الأنواع:
| الصنف | الحالة |
|---|---|
TeyraPayInvalidRequestError | 400 و422: validation_error وrefund_exceeds_amount وpayment_not_refundable |
TeyraPayAuthenticationError | 401 |
TeyraPayPermissionError | 403: merchant_not_active وforbidden |
TeyraPayNotFoundError | 404 |
TeyraPayConflictError وTeyraPayIdempotencyError | 409 |
TeyraPayRateLimitError | 429 (retryAfter بالثواني) |
TeyraPayAPIError | 5xx |
TeyraPayConnectionError | خطأ في الشبكة أو انتهاء المهلة (بلا status) |
import { TeyraPayError, TeyraPayInvalidRequestError } from '@teyra/pay';
try {
await teyra.refunds.create('pay_01JB…', { amount: 999999 });
} catch (err) {
if (err instanceof TeyraPayInvalidRequestError) {
console.log(err.code); // e.g. refund_exceeds_amount
console.log(err.details); // e.g. { fields: { amount: '…' } } on validation_error
} else if (err instanceof TeyraPayError) {
console.error(err.status, err.code, err.message);
} else {
throw err;
}
}تُعاد المحاولة تلقائيًا عند أخطاء الشبكة وانتهاء المهلة واستجابات 429 و5xx، حتى maxRetries مرة (الافتراضي 2)، مع تأخير متزايد: 0.5 ثانية ثم ثانية ثم ثانيتان وهكذا حتى 8 ثوانٍ. وإذا أرسلت الـ API ترويسة Retry-After تنتظر المكتبة المدة المذكورة فيها. لا تُعاد المحاولة لأخطاء 4xx الأخرى. راجع الأخطاء لكل الرموز.
دوال المبالغ
المبالغ أعداد صحيحة بأصغر وحدة للعملة. استخدم هذه الدوال للتحويل بدل ضرب أعداد عشرية:
import { toMinor, fromMinor, currencyExponent } from '@teyra/pay';
toMinor('1.500', 'KWD'); // 1500
toMinor('19.99', 'AED'); // 1999
fromMinor(1500, 'KWD'); // '1.500'
fromMinor(2500, 'SAR'); // '25.00'
currencyExponent('BHD'); // 3
toMinor('1.2345', 'KWD'); // throws: KWD amounts have at most 3 decimal placesراجع المبالغ والعملات.
التحقق من الـ Webhooks
تتحقق webhooks.constructEvent(rawBody, signatureHeader, secret, toleranceSec = 300) من ترويسة Teyra-Signature وتعيد الحدث بعد تحليله. وترمي TeyraPaySignatureVerificationError إذا كانت الترويسة مفقودة، أو كان الطابع الزمني بعيدًا بأكثر من toleranceSec ثانية، أو لم يطابق أي توقيع. مرّر الجسم الخام كما وصل، لا JSON بعد تحليله.
import express from 'express';
import { TeyraPay, TeyraPaySignatureVerificationError } from '@teyra/pay';
const teyra = new TeyraPay(process.env.TEYRA_SECRET_KEY!);
const app = express();
// express.raw keeps the body as a Buffer. Don't use express.json() on this route.
app.post('/hooks/teyra', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = teyra.webhooks.constructEvent(req.body, req.get('Teyra-Signature'), process.env.TEYRA_WEBHOOK_SECRET!);
} catch (err) {
if (err instanceof TeyraPaySignatureVerificationError) return res.status(400).send('Invalid signature');
throw err;
}
if (event.type === 'payment.succeeded') {
await markOrderPaid(event.data.object.id);
}
res.sendStatus(200);
});تبني generateTestHeader(payload, secret) ترويسة صالحة لاختباراتك. راجع التحقق من تواقيع الـ Webhooks والـ Webhooks.
جرّب الـ API في Postman
تفضّل تجربة الطلبات أولًا؟ استورد مجموعة Postman.