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

# Discovering Bills

> Ask a biller what an account owes, and read the result

## Overview

Discovery is the read half of Bill Payment: you ask a biller what an account currently owes, and you get back a list of payable bills. Nothing is charged, and nothing is committed.

It happens in two parts. `POST /v3/bills/discover` accepts the request and gives you a `transactionId`. The bills themselves arrive on that transaction a moment later, when its `status` becomes `READY`.

<Warning>
  The `200` from `discover` is an **acknowledgement**. It carries no bills and says nothing about what the account owes. Read the transaction before you tell your customer anything.
</Warning>

## Building the request

Three fields, all required.

```json theme={null}
{
  "partner": "ADE",
  "account": { "reference": "0123456789012345678901234" },
  "ref": "disc-inv-2026-0042"
}
```

| Field     | Rule                                                                                   |
| --------- | -------------------------------------------------------------------------------------- |
| `partner` | One of `ADE`, `SONELGAZ`, `SEAAL`, `AADL`, `Algérie Télécom`                           |
| `account` | Exactly one identifier — see [Step 1](/en/bill-payment-guides/1-partners-and-accounts) |
| `ref`     | Your own reference, at most 100 characters, unique per biller                          |

## Choosing a `ref`

`ref` is what makes a discovery safe to retry. If the response is lost, you look the `ref` up instead of sending a second discovery — so a `ref` you cannot reconstruct from your own data is a transaction you cannot recover.

**A workable recipe:** a fixed prefix, your own order or invoice identifier, and nothing else.

```javascript theme={null}
// Deterministic: the same order always produces the same ref.
const discoveryRef = `disc-${order.id}`;
const paymentRef = `pay-${order.id}`;
```

| Do                                                      | Do not                                             |
| ------------------------------------------------------- | -------------------------------------------------- |
| `disc-inv-2026-0042` — derived from your invoice number | `1693476000000` — a timestamp you cannot reproduce |
| `disc-order-88213` — derived from your order id         | `abc123` — a random string you did not store       |
| Keep discovery and payment refs distinct                | Reuse the discovery `ref` on the payment           |

<Note>
  A `ref` is unique per biller, not globally. `disc-inv-2026-0042` for `ADE` and the same string for `SONELGAZ` are two different references. Passing `partner` when you look one up removes any ambiguity.
</Note>

## Sending the discovery

<CodeGroup>
  ```bash cURL theme={null}
  curl https://billapi.oneclickdz.com/v3/bills/discover \
    -X POST \
    -H "Content-Type: application/json" \
    -H "X-Access-Token: YOUR_API_KEY" \
    -d '{
      "partner": "ADE",
      "account": { "reference": "0123456789012345678901234" },
      "ref": "disc-inv-2026-0042"
    }'
  ```

  ```javascript Node.js theme={null}
  const BASE = "https://billapi.oneclickdz.com";
  const KEY = process.env.ONECLICKDZ_API_KEY;

  async function startDiscovery(partner, account, ref) {
    const response = await fetch(`${BASE}/v3/bills/discover`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Access-Token": KEY,
      },
      body: JSON.stringify({ partner, account, ref }),
    });

    const body = await response.json();

    if (!body.success) {
      throw new Error(`${body.error.code}: ${body.error.message}`);
    }

    // PENDING — the bills are not here yet.
    return body.data.transactionId;
  }

  const transactionId = await startDiscovery(
    "ADE",
    { reference: "0123456789012345678901234" },
    "disc-inv-2026-0042",
  );
  ```

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

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


  def start_discovery(partner, account, ref):
      response = requests.post(
          f'{BASE}/v3/bills/discover',
          headers={
              'Content-Type': 'application/json',
              'X-Access-Token': KEY
          },
          json={'partner': partner, 'account': account, 'ref': ref}
      )

      body = response.json()

      if not body['success']:
          raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")

      # PENDING — the bills are not here yet.
      return body['data']['transactionId']


  transaction_id = start_discovery(
      'ADE',
      {'reference': '0123456789012345678901234'},
      'disc-inv-2026-0042'
  )
  ```

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

  function startDiscovery(string $partner, array $account, string $ref): string
  {
      $ch = curl_init(BASE . '/v3/bills/discover');
      curl_setopt($ch, CURLOPT_POST, true);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($ch, CURLOPT_HTTPHEADER, [
          'Content-Type: application/json',
          'X-Access-Token: ' . getenv('ONECLICKDZ_API_KEY')
      ]);
      curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
          'partner' => $partner,
          'account' => $account,
          'ref'     => $ref
      ]));

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

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

      // PENDING — the bills are not here yet.
      return $body['data']['transactionId'];
  }

  $transactionId = startDiscovery(
      'ADE',
      ['reference' => '0123456789012345678901234'],
      'disc-inv-2026-0042'
  );
  ?>
  ```
</CodeGroup>

```json theme={null}
{
  "success": true,
  "data": {
    "transactionId": "68b2f4c1a7d3e9f204c81a55",
    "ref": "disc-inv-2026-0042",
    "status": "PENDING"
  },
  "meta": { "timestamp": "2026-08-31T10:15:32.194Z" },
  "requestId": "req_9f3a1c72e0b84d51aB3xZq07"
}
```

## Polling to `READY`

Read the transaction until its `status` leaves `PENDING`. A discovery normally settles in a few seconds.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://billapi.oneclickdz.com/v3/bills/transactions/68b2f4c1a7d3e9f204c81a55 \
    -H "X-Access-Token: YOUR_API_KEY"
  ```

  ```javascript Node.js theme={null}
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  async function waitForBills(transactionId, { timeoutMs = 90_000 } = {}) {
    const deadline = Date.now() + timeoutMs;
    let interval = 2000;

    while (Date.now() < deadline) {
      const response = await fetch(
        `${BASE}/v3/bills/transactions/${transactionId}`,
        { headers: { "X-Access-Token": KEY } },
      );

      const body = await response.json();
      if (!body.success) throw new Error(body.error.code);

      const transaction = body.data;

      if (transaction.status === "READY") return transaction.bills;
      if (transaction.status === "FAILED") {
        throw new Error(transaction.error?.code ?? "FAILED");
      }

      await sleep(interval);
      interval = Math.min(interval * 1.5, 10_000);
    }

    throw new Error("Discovery did not finish in time");
  }

  const bills = await waitForBills(transactionId);
  ```

  ```python Python theme={null}
  import time


  def wait_for_bills(transaction_id, timeout_s=90):
      deadline = time.monotonic() + timeout_s
      interval = 2.0

      while time.monotonic() < deadline:
          response = requests.get(
              f'{BASE}/v3/bills/transactions/{transaction_id}',
              headers={'X-Access-Token': KEY}
          )

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

          transaction = body['data']

          if transaction['status'] == 'READY':
              return transaction.get('bills', [])
          if transaction['status'] == 'FAILED':
              raise RuntimeError(transaction.get('error', {}).get('code', 'FAILED'))

          time.sleep(interval)
          interval = min(interval * 1.5, 10.0)

      raise TimeoutError('Discovery did not finish in time')


  bills = wait_for_bills(transaction_id)
  ```

  ```php PHP theme={null}
  <?php
  function waitForBills(string $transactionId, int $timeoutSeconds = 90): array
  {
      $deadline = time() + $timeoutSeconds;
      $interval = 2;

      while (time() < $deadline) {
          $ch = curl_init(BASE . "/v3/bills/transactions/$transactionId");
          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']);
          }

          $transaction = $body['data'];

          if ($transaction['status'] === 'READY') {
              return $transaction['bills'] ?? [];
          }
          if ($transaction['status'] === 'FAILED') {
              throw new Exception($transaction['error']['code'] ?? 'FAILED');
          }

          sleep($interval);
          $interval = min((int) ceil($interval * 1.5), 10);
      }

      throw new Exception('Discovery did not finish in time');
  }

  $bills = waitForBills($transactionId);
  ?>
  ```
</CodeGroup>

## Reading `bills[]`

A `READY` transaction carries the bills that are payable right now.

```json theme={null}
{
  "success": true,
  "data": {
    "transactionId": "68b2f4c1a7d3e9f204c81a55",
    "ref": "disc-inv-2026-0042",
    "type": "discovery",
    "status": "READY",
    "partner": "ADE",
    "account": { "reference": "0123456789012345678901234" },
    "bills": [
      {
        "billId": "sbx_bill_68b2f4c1a7d3e9f204c81a55_0",
        "amount": 443.39,
        "fee": 30.00,
        "label": "Facture ADE"
      }
    ],
    "currency": "DZD",
    "createdAt": "2026-08-31T10:15:32.194Z",
    "updatedAt": "2026-08-31T10:15:33.008Z",
    "completedAt": "2026-08-31T10:15:33.008Z"
  },
  "meta": { "timestamp": "2026-08-31T10:15:34.120Z" },
  "requestId": "req_9f3a1c72e0b84d51aB3xZq07"
}
```

| Field    | Meaning                                                    |
| -------- | ---------------------------------------------------------- |
| `billId` | What you send to `pay`. Copy it — never construct it       |
| `amount` | What the biller is owed, in DZD                            |
| `fee`    | The service fee for paying this bill, in DZD               |
| `label`  | A human-readable description, when the biller supplied one |
| `period` | The billing period, when the biller supplied one           |

Show the customer `amount + fee`. That sum is what will be debited, and it is returned as `total` on the transaction once a bill is selected.

<Note>
  `bills[]` is present **only** while `status` is `READY`. Once a payment starts, the transaction carries `selectedBill` instead. Read the bills while you have them.
</Note>

## When `bills[]` is empty

An empty array is a normal, successful outcome — not an error.

```json theme={null}
{
  "status": "READY",
  "bills": []
}
```

It means one of two things, and the API does not distinguish between them:

* the account owes nothing, or
* everything the account owes is below the **200 DZD discovery floor**.

Bills under 200 DZD are filtered out during discovery and never appear.

<Warning>
  Word this carefully. "No bills are payable right now" is accurate. "You owe nothing" is not — a 150 DZD bill exists but cannot be paid through this API.
</Warning>

```javascript theme={null}
if (bills.length === 0) {
  return {
    message: "No bills are payable for this account right now.",
    // Not: "This account has no outstanding balance."
  };
}
```

## Recovering a lost response

If the discovery request fails in a way you cannot explain — a timeout, a crash, a redeploy — ask what the `ref` became. Never send a second discovery.

```javascript theme={null}
async function discoverSafely(partner, account, ref) {
  try {
    return await startDiscovery(partner, account, ref);
  } catch (error) {
    const existing = await findByRef(ref, partner);
    if (existing) return existing.transactionId;
    throw error;
  }
}

async function findByRef(ref, partner) {
  const url = new URL(`${BASE}/v3/bills/transactions/by-ref`);
  url.searchParams.set("ref", ref);
  url.searchParams.set("partner", partner);

  const response = await fetch(url, { headers: { "X-Access-Token": KEY } });
  const body = await response.json();

  if (body.success) return body.data;
  if (body.error.code === "NOT_FOUND") return null; // Never landed — safe to resend.
  throw new Error(body.error.code);
}
```

The same lookup answers `403 DUPLICATED_REF`. That error means the discovery already exists; it is never a reason to retry with a different `ref`, which would start a second discovery for the same account.

## Errors you will meet

| Error                 | HTTP | What it means                                | What to do                                      |
| --------------------- | ---- | -------------------------------------------- | ----------------------------------------------- |
| `ERR_VALIDATION`      | 400  | The body does not match the schema           | Fix the request; never retry unchanged          |
| `INVALID_ACCOUNT`     | 400  | The identifier is not usable for this biller | Ask the customer to check their bill            |
| `DUPLICATED_REF`      | 403  | This `ref` already exists for this biller    | Look it up; do not resend                       |
| `BILL_ALREADY_PAID`   | 409  | This account was already paid recently       | Find the paid transaction and show its receipt  |
| `PAYMENT_IN_PROGRESS` | 409  | Another payment for this account is running  | Wait for it to finish                           |
| `PARTNER_UNAVAILABLE` | 503  | The biller is unreachable or switched off    | Retry later; nothing was created                |
| `AUTH_UNAVAILABLE`    | 503  | We could not verify your key in time         | Honour `Retry-After` and retry the same request |
| `SERVICE_UNAVAILABLE` | 503  | Planned maintenance                          | Honour `Retry-After` and retry                  |

A `FAILED` discovery carries its reason in `error.code`: `INVALID_ACCOUNT`, `PARTNER_UNAVAILABLE`, `BILL_ALREADY_PAID` or `PAYMENT_DECLINED`.

[Every code, with example bodies →](/en/api-reference/error-handling)

## Best practices

<CardGroup cols={2}>
  <Card title="Derive the ref" icon="fingerprint">
    Build it from your own order identifier so you can always reconstruct it after a failure.
  </Card>

  <Card title="Poll, never resend" icon="arrows-rotate">
    A slow discovery is not a lost one. Read the transaction instead of sending another request.
  </Card>

  <Card title="Cache nothing about bills" icon="clock">
    A discovery is a snapshot. If the customer waits, discover again rather than paying against stale figures.
  </Card>

  <Card title="Say payable, not owed" icon="quote-left">
    An empty `bills[]` means nothing is payable. It does not mean the account owes nothing.
  </Card>
</CardGroup>

## Next step

<Card title="Step 3: Paying bills" icon="money-bill-transfer" href="/en/bill-payment-guides/3-paying-bills">
  Choose a bill, confirm the total, and submit the payment safely
</Card>

## Related pages

<CardGroup cols={2}>
  <Card title="Discover Bills" icon="magnifying-glass-dollar" href="/en/api-reference/bill-payment/discover-bills">
    The endpoint reference
  </Card>

  <Card title="Get Transaction by ID" icon="id-card" href="/en/api-reference/bill-payment/check-by-id">
    The transaction object in full
  </Card>

  <Card title="Get Transaction by Reference" icon="tag" href="/en/api-reference/bill-payment/check-by-ref">
    The recovery path
  </Card>

  <Card title="Partners and Accounts" icon="address-card" href="/en/bill-payment-guides/1-partners-and-accounts">
    Identifier rules per biller
  </Card>
</CardGroup>
