التوثيق
الدمجالأخطاء

أخطاء API

مغلّف الخطأ اللي بيرجعه كل استدعاء API فاشل. أنواع الأخطاء الثلاثة، والحقول اللي بيحملوها، ونمط المعالِج.

الصفحة دي بتغطّي الأخطاء المتزامنة اللي XPay بيرجّعها: لما السيرفر بتاعك يعمل استدعاء API ويرجعله رد غير 2xx، إيه اللي في الجسم، وإزاي تتعامل معاه.

لحالات فشل الدفع غير المتزامنة (رفض بطاقة بيقع على lastPaymentError بتاع Payment Intent)، ده مكان مختلف تمامًا. شوف أخطاء الدفع.

المغلّف

كل رد خطأ API ليه نفس الشكل:

{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "message": "Missing required parameter: lineItems",
    "param": "lineItems",
    "doc_url": "https://docs.xpay.app/integrate/errors/api-error-codes#parameter_missing"
  },
  "request_id": "req_3STkwmFGhGHoO0IX13BRo5iU"
}
الحقلالنوعالمعنى
error.typestringواحد من الفئات الثلاثة تحت. تفرّع على ده الأول.
error.codestring | nullمعرّف الخطأ المحدّد. ثابت عبر الرسائل والترجمات. استخدمه هو، مش الرسالة، للتحكم في المسار.
error.messagestringإنجليزي مقروء للبني آدمين. لسجلاتك وواجهة فريقك. ما تعرضوش حرفيًا لعملائك.
error.paramstring | nullحقل جسم الطلب اللي فشل (زي lineItems[0].price، customerDetails.email). موجود في أخطاء التحقّق.
error.doc_urlstring | nullرابط مباشر لصف الكود ده في مرجع أكواد أخطاء API.
request_idstringالـ req_* ID للطلب الفاشل. سجّله دايمًا. استخدمه علشان تلاقي الطلب في Workbench ← السجلات.

code، وparam، وdoc_url ممكن يكونوا غايبين. التلاتة التانيين موجودين دايمًا.

لو تقدر تتحمّل تفرّع واحد بس في المعالِج بتاعك، تفرّع على type، مش على code أو حالة الـ HTTP. قائمة الأنواع ثابتة عبر الإصدارات؛ وقائمة الأكواد بتكبر على مدار الوقت وبتتضاف أسباب أخطاء جديدة.

الأنواع الثلاثة

النوعحالة HTTPمعناه إيهالمعالِج المعتاد
invalid_request_error400، 404، 409الطلب اترفض بسبب حاجة من ناحيتك: حقل ناقص أو غلط، أو مورد مش موجود، أو حالة متعارضة.ما تعيدش المحاولة. صلّح الطلب. اعرض الرسالة لمين تسبّب فيها (فريقك، الفورم بتاعك).
authentication_error401مفتاح الـ API أو توقيع الـ HMAC ناقص، أو غير صالح، أو معطّل، أو بصيغة غلط.ما تعيدش المحاولة. اتأكد من XPAY_SECRET_KEY في بيئتك، ومن حالة تفعيل المفتاح في لوحة التحكم، ومن إنك بتبعت Authorization: Bearer <key>.
api_error500مشكلة غير متوقعة من ناحية السيرفر عند XPay.أعِد المحاولة بتراجع أسّي. لو استمرت، اذكر request_id للدعم.

لما تعيد محاولة كتابة (POST أو PATCH)، ابعت Idempotency-Key علشان إعادة المحاولة ترجّع النتيجة الأصلية بدل ما تكرّر العملية. شوف عدم التكرار (idempotency).

نمط المعالجة

المعالِج هو try / catch واحد. جوّه الـ catch، افصل الرد، وتفرّع على error.type، وضيّق على error.code للعدد القليل من الأكواد اللي بتهمّك، وسجّل كل حاجة، واعرض اللي مناسب.

async function callXPay<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(`https://api.xpay.app${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (res.ok) return res.json() as Promise<T>;

  // Read the canonical envelope. Every XPay error response has `error.type`.
  const payload = await res.json().catch(() => null);
  const xpayError = payload?.error;
  const requestId = payload?.request_id;

  if (!xpayError?.type) {
    // Transport-level failure: not XPay-shaped. Treat as a client-side bug.
    throw new Error(`Non-XPay response: ${res.status}`);
  }

  // Always log first, branch second.
  log.error("xpay api error", {
    requestId,
    type: xpayError.type,
    code: xpayError.code,
    param: xpayError.param,
    message: xpayError.message,
    docUrl: xpayError.doc_url,
  });

  switch (xpayError.type) {
    case "invalid_request_error":
      // Your request is wrong. Don't retry. Bubble up to whoever sent it.
      throw new BadRequest(xpayError);

    case "authentication_error":
      // Your secret key is wrong. Page yourself; don't retry.
      throw new ConfigError(xpayError);

    case "api_error":
      // XPay is having a moment. Retry with backoff.
      throw new RetryableError(xpayError);

    default:
      // Unknown type. Treat as retryable to be safe.
      throw new RetryableError(xpayError);
  }
}
import os, requests

def call_xpay(path: str, body: dict) -> dict:
    res = requests.post(
        f"https://api.xpay.app{path}",
        headers={
            "Authorization": f"Bearer {os.environ['XPAY_SECRET_KEY']}",
            "Content-Type": "application/json",
        },
        json=body,
        timeout=30,
    )

    if res.ok:
        return res.json()

    payload = res.json() if res.content else {}
    xpay_error = payload.get("error") or {}
    request_id = payload.get("request_id")

    if not xpay_error.get("type"):
        # Transport-level failure: not XPay-shaped.
        raise RuntimeError(f"Non-XPay response: {res.status_code}")

    log.error(
        "xpay api error",
        extra={
            "request_id": request_id,
            "type": xpay_error.get("type"),
            "code": xpay_error.get("code"),
            "param": xpay_error.get("param"),
            "message": xpay_error.get("message"),
            "doc_url": xpay_error.get("doc_url"),
        },
    )

    error_type = xpay_error["type"]
    if error_type == "invalid_request_error":
        raise BadRequest(xpay_error)
    if error_type == "authentication_error":
        raise ConfigError(xpay_error)
    if error_type == "api_error":
        raise RetryableError(xpay_error)
    raise RetryableError(xpay_error)  # unknown type, treat as retryable

التضييق على code

type بيقرّر التفرّع العريض. code بيقرّر الرسالة المحدّدة اللي بتعرضها. شوية أكواد تستاهل معاملة خاصة في واجهتك؛ والباقي يقدر يستخدم error.message زائد رابط "اعرف أكتر" لـ error.doc_url.

if (xpayError.type === "invalid_request_error") {
  switch (xpayError.code) {
    case "resource_missing":
      // The ID you sent doesn't exist. Likely your code, not the user's.
      return show("That record was deleted or never existed.");

    case "checkout_session_expired":
      // The session is past expiresAt. Create a new one.
      return await createNewSession();

    case "price_sold_out":
      // Stock ran out between session creation and payment.
      return show("Sorry, that item is sold out.");

    case "merchant_not_activated":
      // Your account isn't approved for live mode yet.
      return show("Your account needs activation. Contact support.");

    default:
      // Generic invalid_request_error. Surface the message.
      return show(xpayError.message);
  }
}

للقائمة الكاملة بالأكواد اللي ممكن تشوفها، شوف أكواد أخطاء API.

اللي ما تعملوش

  • ما تعيدش محاولة invalid_request_error. نفس الطلب هيفشل بنفس الطريقة. صلّح المدخلات وحاول تاني، بـ Idempotency-Key جديد لو بتستخدم واحد.
  • ما تعيدش محاولة authentication_error كمان. المفتاح الغلط مفتاح غلط. إعادة المحاولة عشر مرات بتنتج بس عشر سطور في السجل بتقول كده.
  • ما تعرضش error.message لعملائك النهائيين حرفيًا. هو إنجليزي وتقني وممكن يحتوي معرّفات داخلية. استخدمه لسجلاتك وواجهة فريق الدعم بتاعك. ابنِ النص الموجّه للعميل بناءً على error.code.
  • ما تتجاهلش error.code وتفصل error.message بدلًا منه. نص الرسالة بيتغير؛ والكود ثابت. التفرّع على الرسالة هش.
  • ما تبلعش الأخطاء بصمت. سجّل request_id جنب كل خطأ بتمسكه دايمًا. من غيره، تصحيح الأخطاء في Workbench ← السجلات بيبقى تمرير لانهائي.
  • ما تفترضش إن الرد بشكل XPay. الرد من غير error.type هو فشل على مستوى النقل (بروكسي معطّل، أو نوع محتوى غلط، أو حمولة كبيرة أوي). اعتبره خطأ من ناحية العميل لازم يتصلّح، مش تفرّعًا وقت التشغيل.

رايح فين بعد كده

في الصفحة دي