مكتبة 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، وتتيح لك الأصناف الفرعية التفريق بين الأنواع:

الصنفالحالة
TeyraPayInvalidRequestError400 و422: validation_error وrefund_exceeds_amount وpayment_not_refundable
TeyraPayAuthenticationError401
TeyraPayPermissionError403: merchant_not_active وforbidden
TeyraPayNotFoundError404
TeyraPayConflictError وTeyraPayIdempotencyError409
TeyraPayRateLimitError429 (retryAfter بالثواني)
TeyraPayAPIError5xx
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.