> ## 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.

# Receipts and Reconciliation

> Store the proof of payment and balance your books daily

## Overview

A payment that reached `SUCCESS` leaves two pieces of evidence: `operationId`, the biller's own reference for the transaction, and the receipt file. Together they settle any dispute a customer raises months later.

This step covers downloading and storing both, and a daily routine that proves your ledger and ours agree.

## What a success gives you

```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"
}
```

| Field          | Keep it because                                                                   |
| -------------- | --------------------------------------------------------------------------------- |
| `operationId`  | It is the biller's reference for this payment — the one a dispute is settled with |
| `receiptUrl`   | It points at the receipt file. Download it; do not store the link                 |
| `total`        | The exact figure debited from your balance                                        |
| `selectedBill` | The bill that was actually paid, with its `amount` and `fee`                      |
| `completedAt`  | When the payment settled                                                          |

<Warning>
  `receiptUrl` requires your `X-Access-Token` header. It is **not** a link you can email to a customer or embed in a page. Download the bytes and serve them from your own system.
</Warning>

## Downloading the receipt

Fetch it in the same step that records the success, and take the file extension from the `Content-Type` header — a receipt is a PDF or an image depending on the biller.

<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>

A receipt exists only for a `SUCCESS` transaction. Anything else — still in flight, `FAILED`, `REFUNDED`, or belonging to another partner — answers `404 NOT_FOUND` in the ordinary JSON envelope.

## Storing it

<Steps>
  <Step title="Download once, at settlement">
    Fetch the receipt in the same step that marks your order paid. Retrying later is fine, but do not leave it until a customer asks.
  </Step>

  <Step title="Store the bytes, not the URL">
    Put the file in your own object storage, keyed by your order identifier. The API URL needs your key and is useless to anyone else.
  </Step>

  <Step title="Store operationId alongside it">
    The receipt is the document; `operationId` is the reference. Keep both on the order row.
  </Step>

  <Step title="Serve it behind your own authentication">
    Your customer downloads it from you, not from us.
  </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,
  });
}
```

## Reconciling a day

`GET /v3/bills/transactions` with `from` and `to` gives you everything that happened in a window. Bound the window — an unbounded list grows with your business, and a bounded one cannot shift underneath you while you page.

<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>

## What each mismatch means

| Finding            | Meaning                                                   | Action                                                                 |
| ------------------ | --------------------------------------------------------- | ---------------------------------------------------------------------- |
| `missedSuccess`    | We charged you; your order is not marked paid             | Settle it now, download the receipt, notify the customer               |
| `refundMarkedPaid` | The money came back; your order still says paid           | Reverse it on your side and release the customer's funds               |
| `stillOpen`        | A transaction from that day has not reached a final state | Keep it in reconciliation until it does. Never close it as failed      |
| `unknownToUs`      | A transaction whose `ref` matches no order of yours       | Investigate — usually a discovery that was never paid, or a lost write |

<Note>
  Reconcile on `SUCCESS` and `REFUNDED` only. A `FAILED` transaction never moved money, and a `PENDING` or `READY` discovery never charged anything.
</Note>

## A daily routine

<Steps>
  <Step title="Run once a day, for yesterday">
    Bound the window with `from` and `to`. Yesterday is complete; today is still moving.
  </Step>

  <Step title="Match on ref">
    Your `ref` is the join key between our transactions and your orders. That is what it is for.
  </Step>

  <Step title="Sum SUCCESS and REFUNDED separately">
    Net movement is `SUCCESS` totals minus `REFUNDED` totals. Both belong in your ledger.
  </Step>

  <Step title="Carry open transactions forward">
    Anything still `PENDING`, `PROCESSING` or `UNKNOWN` stays on the list until it resolves.
  </Step>

  <Step title="Alert on any mismatch">
    A reconciliation that finds nothing should be silent. One that finds something should page someone.
  </Step>
</Steps>

## Best practices

<CardGroup cols={2}>
  <Card title="Store both proofs" icon="receipt">
    `operationId` and the receipt file. One without the other is half an answer to a dispute.
  </Card>

  <Card title="Reconcile daily, not monthly" icon="calendar">
    A one-day window is small enough to investigate by hand. A month is not.
  </Card>

  <Card title="Never close an open transaction" icon="clock-rotate-left">
    `UNKNOWN` and `PROCESSING` resolve on their own. Carry them forward instead of writing them off.
  </Card>

  <Card title="Serve receipts yourself" icon="shield-halved">
    Behind your own authentication, from your own storage. Never share the API URL.
  </Card>
</CardGroup>

## Next step

<Card title="Step 6: Sandbox testing" icon="flask" href="/en/bill-payment-guides/6-sandbox-testing">
  Reproduce every outcome on demand, then switch to production with confidence
</Card>

## Related pages

<CardGroup cols={2}>
  <Card title="Download Receipt" icon="file-arrow-down" href="/en/api-reference/bill-payment/get-receipt">
    The endpoint reference
  </Card>

  <Card title="List Transactions" icon="list" href="/en/api-reference/bill-payment/list-transactions">
    Filters, paging and `meta.total`
  </Card>

  <Card title="Status Polling" icon="arrows-rotate" href="/en/bill-payment-guides/4-status-polling">
    Getting to a final state
  </Card>

  <Card title="Get Transaction by ID" icon="id-card" href="/en/api-reference/bill-payment/check-by-id">
    Where `operationId` appears
  </Card>
</CardGroup>
