> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oneclickdz.com/llms.txt
> Use this file to discover all available pages before exploring further.

# الإيصالات والمطابقة

> خزّن إثبات الدفع وطابق دفاترك يومياً

<div dir="ltr">
  ## نظرة عامة

  الدفعة التي وصلت إلى `SUCCESS` تترك دليلين: `operationId`، وهو مرجع الجهة المُصدِرة للفاتورة الخاص بهذه المعاملة، وملف الإيصال. وهما معاً يحسمان أي نزاع يثيره العميل بعد أشهر.

  تتناول هذه الخطوة تنزيل الاثنين وتخزينهما، إضافةً إلى روتين يومي يثبت أن دفتر حساباتك ودفترنا متطابقان.

  ## ما الذي تحصل عليه عند النجاح

  ```json theme={null}
  {
    "transactionId": "68b2f4c1a7d3e9f204c81a55",
    "ref": "disc-inv-2026-0042",
    "type": "payment",
    "status": "SUCCESS",
    "partner": "ADE",
    "account": { "reference": "0123456789012345678901234" },
    "selectedBill": {
      "billId": "sbx_bill_68b2f4c1a7d3e9f204c81a55_0",
      "amount": 443.39,
      "fee": 30.00,
      "label": "Facture ADE"
    },
    "total": 473.39,
    "currency": "DZD",
    "receiptUrl": "https://billapi.oneclickdz.com/v3/bills/transactions/68b2f4c1a7d3e9f204c81a55/receipt",
    "operationId": "op_7726351904",
    "createdAt": "2026-08-31T10:15:32.194Z",
    "updatedAt": "2026-08-31T10:15:41.902Z",
    "completedAt": "2026-08-31T10:15:41.902Z"
  }
  ```

  | الحقل          | لماذا تحتفظ به                                                         |
  | -------------- | ---------------------------------------------------------------------- |
  | `operationId`  | هو مرجع الجهة المُصدِرة للفاتورة لهذه الدفعة — وهو ما يُحسم به أي نزاع |
  | `receiptUrl`   | يشير إلى ملف الإيصال. نزّل الملف ولا تخزّن الرابط                      |
  | `total`        | المبلغ الدقيق المخصوم من رصيدك                                         |
  | `selectedBill` | الفاتورة التي دُفعت فعلاً، بمبلغها `amount` ورسومها `fee`              |
  | `completedAt`  | وقت تسوية الدفعة                                                       |

  <Warning>
    يتطلب `receiptUrl` ترويسة `X-Access-Token` الخاصة بك. وهو **ليس** رابطاً يمكنك إرساله بالبريد إلى عميل أو تضمينه في صفحة. نزّل البايتات وقدّمها من نظامك الخاص.
  </Warning>

  ## تنزيل الإيصال

  نزّله في الخطوة نفسها التي تسجّل فيها النجاح، وخذ امتداد الملف من ترويسة `Content-Type` — فالإيصال ملف PDF أو صورة بحسب الجهة المُصدِرة للفاتورة.

  <CodeGroup>
    ```bash cURL theme={null}
    # -J -O writes the file under the name the server suggests
    curl https://billapi.oneclickdz.com/v3/bills/transactions/68b2f4c1a7d3e9f204c81a55/receipt \
      -H "X-Access-Token: YOUR_API_KEY" \
      --fail \
      --remote-header-name --remote-name
    ```

    ```javascript Node.js theme={null}
    import { writeFile } from "node:fs/promises";

    const BASE = "https://billapi.oneclickdz.com";
    const KEY = process.env.ONECLICKDZ_API_KEY;

    function extensionFor(contentType = "") {
      if (contentType.includes("pdf")) return "pdf";
      if (contentType.includes("png")) return "png";
      if (contentType.includes("jpeg")) return "jpg";
      return "bin";
    }

    async function downloadReceipt(transactionId) {
      const response = await fetch(
        `${BASE}/v3/bills/transactions/${transactionId}/receipt`,
        { headers: { "X-Access-Token": KEY } },
      );

      if (!response.ok) {
        const error = await response.json();
        throw new Error(`${error.error.code}: ${error.error.message}`);
      }

      const extension = extensionFor(response.headers.get("content-type"));
      const bytes = Buffer.from(await response.arrayBuffer());
      const path = `receipts/${transactionId}.${extension}`;

      await writeFile(path, bytes);

      return { path, bytes: bytes.length };
    }
    ```

    ```python Python theme={null}
    import os
    import requests

    BASE = 'https://billapi.oneclickdz.com'
    KEY = os.getenv('ONECLICKDZ_API_KEY')


    def extension_for(content_type=''):
        if 'pdf' in content_type:
            return 'pdf'
        if 'png' in content_type:
            return 'png'
        if 'jpeg' in content_type:
            return 'jpg'
        return 'bin'


    def download_receipt(transaction_id):
        response = requests.get(
            f'{BASE}/v3/bills/transactions/{transaction_id}/receipt',
            headers={'X-Access-Token': KEY}
        )

        if response.status_code != 200:
            error = response.json()
            raise RuntimeError(f"{error['error']['code']}: {error['error']['message']}")

        extension = extension_for(response.headers.get('Content-Type', ''))
        path = f'receipts/{transaction_id}.{extension}'

        with open(path, 'wb') as handle:
            handle.write(response.content)

        return {'path': path, 'bytes': len(response.content)}
    ```

    ```php PHP theme={null}
    <?php
    const BASE = 'https://billapi.oneclickdz.com';

    function extensionFor(string $contentType): string
    {
        if (str_contains($contentType, 'pdf'))  return 'pdf';
        if (str_contains($contentType, 'png'))  return 'png';
        if (str_contains($contentType, 'jpeg')) return 'jpg';
        return 'bin';
    }

    function downloadReceipt(string $transactionId): array
    {
        $ch = curl_init(BASE . "/v3/bills/transactions/$transactionId/receipt");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'X-Access-Token: ' . getenv('ONECLICKDZ_API_KEY')
        ]);

        $bytes  = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $type   = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
        curl_close($ch);

        if ($status !== 200) {
            $error = json_decode($bytes, true);
            throw new Exception($error['error']['code'] . ': ' . $error['error']['message']);
        }

        $path = "receipts/$transactionId." . extensionFor($type);
        file_put_contents($path, $bytes);

        return ['path' => $path, 'bytes' => strlen($bytes)];
    }
    ?>
    ```
  </CodeGroup>

  لا يوجد إيصال إلا لمعاملة في حالة `SUCCESS`. وأي حالة أخرى — لا تزال جارية، أو `FAILED`، أو `REFUNDED`، أو تخص شريكاً آخر — تُجيب بـ `404 NOT_FOUND` داخل غلاف JSON المعتاد.

  ## تخزينه

  <Steps>
    <Step title="نزّله مرة واحدة، عند التسوية">
      نزّل الإيصال في الخطوة نفسها التي تضع فيها علامة "مدفوع" على طلبك. إعادة المحاولة لاحقاً أمر مقبول، لكن لا تؤجّله حتى يسأل عنه عميل.
    </Step>

    <Step title="خزّن البايتات لا الرابط">
      ضع الملف في تخزين الكائنات الخاص بك، مفهرساً بمعرّف طلبك. فرابط API يحتاج مفتاحك ولا فائدة منه لأي شخص آخر.
    </Step>

    <Step title="خزّن operationId إلى جانبه">
      الإيصال هو المستند؛ و`operationId` هو المرجع. احتفظ بكليهما في سجل الطلب.
    </Step>

    <Step title="قدّمه خلف مصادقتك الخاصة">
      عميلك ينزّله منك، لا منّا.
    </Step>
  </Steps>

  ```javascript theme={null}
  async function settle(order, transaction) {
    const receipt = await downloadReceipt(transaction.transactionId);

    await db.orders.update(order.id, {
      state: "PAID",
      operationId: transaction.operationId,
      total: transaction.total,
      amount: transaction.selectedBill.amount,
      fee: transaction.selectedBill.fee,
      completedAt: transaction.completedAt,
      receiptPath: receipt.path,
    });
  }
  ```

  ## مطابقة يوم واحد

  يمنحك `GET /v3/bills/transactions` مع `from` و`to` كل ما حدث خلال نافذة زمنية. حُدّ النافذة — فالقائمة غير المحدودة تكبر مع نمو نشاطك، أما النافذة المحدودة فلا يمكن أن تتغيّر من تحتك أثناء تصفّحك للصفحات.

  <CodeGroup>
    ```bash cURL theme={null}
    curl -G https://billapi.oneclickdz.com/v3/bills/transactions \
      --data-urlencode "from=2026-08-30T00:00:00Z" \
      --data-urlencode "to=2026-08-30T23:59:59Z" \
      --data-urlencode "status=SUCCESS" \
      --data-urlencode "limit=100" \
      -H "X-Access-Token: YOUR_API_KEY"
    ```

    ```javascript Node.js theme={null}
    async function* eachTransaction(filters) {
      let offset = 0;
      const limit = 100;

      for (;;) {
        const url = new URL(`${BASE}/v3/bills/transactions`);
        for (const [key, value] of Object.entries(filters)) {
          url.searchParams.set(key, value);
        }
        url.searchParams.set("limit", String(limit));
        url.searchParams.set("offset", String(offset));

        const response = await fetch(url, { headers: { "X-Access-Token": KEY } });
        const body = await response.json();
        if (!body.success) throw new Error(body.error.code);

        for (const transaction of body.data) yield transaction;

        offset += body.data.length;
        if (offset >= body.meta.total || body.data.length === 0) return;
      }
    }

    async function reconcile(day) {
      const filters = {
        from: `${day}T00:00:00Z`,
        to: `${day}T23:59:59Z`,
      };

      let charged = 0;
      let returned = 0;
      const mismatches = [];

      for await (const transaction of eachTransaction(filters)) {
        const order = await db.orders.findByRef(transaction.ref);

        if (!order) {
          mismatches.push({ reason: "unknownToUs", transaction });
          continue;
        }

        if (transaction.status === "SUCCESS") {
          charged += transaction.total;
          if (order.state !== "PAID") {
            mismatches.push({ reason: "missedSuccess", transaction });
          }
        }

        if (transaction.status === "REFUNDED") {
          returned += transaction.total;
          if (order.state === "PAID") {
            mismatches.push({ reason: "refundMarkedPaid", transaction });
          }
        }

        if (["PENDING", "PROCESSING", "UNKNOWN"].includes(transaction.status)) {
          mismatches.push({ reason: "stillOpen", transaction });
        }
      }

      return { day, charged, returned, net: charged - returned, mismatches };
    }
    ```

    ```python Python theme={null}
    def each_transaction(filters):
        offset, limit = 0, 100

        while True:
            response = requests.get(
                f'{BASE}/v3/bills/transactions',
                headers={'X-Access-Token': KEY},
                params={**filters, 'limit': limit, 'offset': offset}
            )

            body = response.json()
            if not body['success']:
                raise RuntimeError(body['error']['code'])

            for transaction in body['data']:
                yield transaction

            offset += len(body['data'])
            if offset >= body['meta']['total'] or not body['data']:
                return


    def reconcile(day):
        filters = {'from': f'{day}T00:00:00Z', 'to': f'{day}T23:59:59Z'}

        charged = returned = 0.0
        mismatches = []

        for transaction in each_transaction(filters):
            order = db.orders.find_by_ref(transaction['ref'])

            if not order:
                mismatches.append(('unknownToUs', transaction))
                continue

            if transaction['status'] == 'SUCCESS':
                charged += transaction['total']
                if order['state'] != 'PAID':
                    mismatches.append(('missedSuccess', transaction))

            if transaction['status'] == 'REFUNDED':
                returned += transaction['total']
                if order['state'] == 'PAID':
                    mismatches.append(('refundMarkedPaid', transaction))

            if transaction['status'] in ('PENDING', 'PROCESSING', 'UNKNOWN'):
                mismatches.append(('stillOpen', transaction))

        return {
            'day': day,
            'charged': charged,
            'returned': returned,
            'net': charged - returned,
            'mismatches': mismatches
        }
    ```

    ```php PHP theme={null}
    <?php
    function eachTransaction(array $filters): Generator
    {
        $offset = 0;
        $limit  = 100;

        while (true) {
            $query = http_build_query($filters + ['limit' => $limit, 'offset' => $offset]);

            $ch = curl_init(BASE . "/v3/bills/transactions?$query");
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_HTTPHEADER, [
                'X-Access-Token: ' . getenv('ONECLICKDZ_API_KEY')
            ]);

            $body = json_decode(curl_exec($ch), true);
            curl_close($ch);

            if (!$body['success']) {
                throw new Exception($body['error']['code']);
            }

            foreach ($body['data'] as $transaction) {
                yield $transaction;
            }

            $offset += count($body['data']);
            if ($offset >= $body['meta']['total'] || count($body['data']) === 0) {
                return;
            }
        }
    }

    function reconcile(string $day): array
    {
        $filters = ['from' => "{$day}T00:00:00Z", 'to' => "{$day}T23:59:59Z"];

        $charged = 0.0;
        $returned = 0.0;
        $mismatches = [];

        foreach (eachTransaction($filters) as $transaction) {
            $order = $db->orders->findByRef($transaction['ref']);

            if (!$order) {
                $mismatches[] = ['unknownToUs', $transaction];
                continue;
            }

            if ($transaction['status'] === 'SUCCESS') {
                $charged += $transaction['total'];
                if ($order['state'] !== 'PAID') {
                    $mismatches[] = ['missedSuccess', $transaction];
                }
            }

            if ($transaction['status'] === 'REFUNDED') {
                $returned += $transaction['total'];
                if ($order['state'] === 'PAID') {
                    $mismatches[] = ['refundMarkedPaid', $transaction];
                }
            }

            if (in_array($transaction['status'], ['PENDING', 'PROCESSING', 'UNKNOWN'], true)) {
                $mismatches[] = ['stillOpen', $transaction];
            }
        }

        return [
            'day'        => $day,
            'charged'    => $charged,
            'returned'   => $returned,
            'net'        => $charged - $returned,
            'mismatches' => $mismatches
        ];
    }
    ?>
    ```
  </CodeGroup>

  ## ماذا يعني كل تعارض

  | النتيجة            | المعنى                                      | الإجراء                                                           |
  | ------------------ | ------------------------------------------- | ----------------------------------------------------------------- |
  | `missedSuccess`    | خصمنا منك المبلغ؛ وطلبك غير مُعلَّم كمدفوع  | سوِّه الآن، ونزّل الإيصال، وأشعر العميل                           |
  | `refundMarkedPaid` | عاد المال؛ وطلبك لا يزال يقول إنه مدفوع     | اعكس ذلك لديك وحرّر أموال العميل                                  |
  | `stillOpen`        | معاملة من ذلك اليوم لم تصل إلى حالة نهائية  | أبقِها في المطابقة حتى تصل إليها. ولا تغلقها أبداً على أنها فاشلة |
  | `unknownToUs`      | معاملة لا يطابق `ref` الخاص بها أي طلب لديك | تحقّق منها — عادةً استكشاف لم يُدفع قط، أو عملية كتابة ضائعة      |

  <Note>
    طابق على `SUCCESS` و`REFUNDED` فقط. فالمعاملة `FAILED` لم تُحرّك أي مال، والاستكشاف في حالة `PENDING` أو `READY` لم يخصم شيئاً.
  </Note>

  ## روتين يومي

  <Steps>
    <Step title="شغّله مرة واحدة يومياً، عن يوم أمس">
      حُدّ النافذة بـ `from` و`to`. الأمس مكتمل؛ أما اليوم فلا يزال يتحرك.
    </Step>

    <Step title="طابق على ref">
      `ref` الخاص بك هو مفتاح الربط بين معاملاتنا وطلباتك. وهذا هو الغرض منه.
    </Step>

    <Step title="اجمع SUCCESS وREFUNDED كلاً على حدة">
      صافي الحركة هو مجاميع `SUCCESS` ناقص مجاميع `REFUNDED`. وكلاهما ينتمي إلى دفتر حساباتك.
    </Step>

    <Step title="رحّل المعاملات المفتوحة">
      كل ما لا يزال `PENDING` أو `PROCESSING` أو `UNKNOWN` يبقى في القائمة حتى يُحسم.
    </Step>

    <Step title="أطلق تنبيهاً عند أي تعارض">
      المطابقة التي لا تجد شيئاً ينبغي أن تكون صامتة. أما التي تجد شيئاً فينبغي أن تستدعي شخصاً.
    </Step>
  </Steps>

  ## أفضل الممارسات

  <CardGroup cols={2}>
    <Card title="خزّن كلا الدليلين" icon="receipt">
      `operationId` وملف الإيصال. أحدهما دون الآخر نصف إجابة في أي نزاع.
    </Card>

    <Card title="طابق يومياً لا شهرياً" icon="calendar">
      نافذة يوم واحد صغيرة بما يكفي للتحقيق فيها يدوياً. أما الشهر فلا.
    </Card>

    <Card title="لا تغلق معاملة مفتوحة أبداً" icon="clock-rotate-left">
      `UNKNOWN` و`PROCESSING` تُحسمان من تلقاء نفسيهما. رحّلهما بدلاً من شطبهما.
    </Card>

    <Card title="قدّم الإيصالات بنفسك" icon="shield-halved">
      خلف مصادقتك الخاصة، ومن تخزينك الخاص. ولا تشارك رابط API أبداً.
    </Card>
  </CardGroup>

  ## الخطوة التالية

  <Card title="الخطوة 6: اختبار Sandbox" icon="flask" href="/ar/bill-payment-guides/6-sandbox-testing">
    أعد إنتاج كل نتيجة عند الطلب، ثم انتقل إلى الإنتاج بثقة
  </Card>

  ## صفحات ذات الصلة

  <CardGroup cols={2}>
    <Card title="تنزيل الإيصال" icon="file-arrow-down" href="/ar/api-reference/bill-payment/get-receipt">
      مرجع الـ endpoint
    </Card>

    <Card title="سرد المعاملات" icon="list" href="/ar/api-reference/bill-payment/list-transactions">
      الفلاتر والتصفّح و`meta.total`
    </Card>

    <Card title="استطلاع الحالة" icon="arrows-rotate" href="/ar/bill-payment-guides/4-status-polling">
      الوصول إلى حالة نهائية
    </Card>

    <Card title="الحصول على معاملة بالمعرّف" icon="id-card" href="/ar/api-reference/bill-payment/check-by-id">
      أين يظهر `operationId`
    </Card>
  </CardGroup>
</div>
