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

# Paying Bills

> Charge one discovered bill safely, and never twice

## Overview

Payment is the write half of Bill Payment. You take one `billId` from a `READY` discovery, submit it, and the same transaction carries the payment through to a final state.

This is the call that moves money, so the order of operations matters more here than anywhere else in the integration: **write your own record first, send once, then poll.**

<Warning>
  The `200` from `pay` means the payment was **accepted and is in flight**. It does not mean the bill was paid. The outcome only ever appears in the transaction's `status`.
</Warning>

## Choosing a bill

`bills[]` on a `READY` transaction may hold several entries. Pick one — a payment pays exactly one bill.

```json theme={null}
{
  "status": "READY",
  "bills": [
    {
      "billId": "sbx_bill_68b2f4c1a7d3e9f204c81a55_0",
      "amount": 1200.0,
      "fee": 30.0,
      "label": "Facture SONELGAZ"
    },
    {
      "billId": "sbx_bill_68b2f4c1a7d3e9f204c81a55_1",
      "amount": 850.0,
      "fee": 30.00,
      "label": "Facture SONELGAZ"
    }
  ]
}
```

To pay several bills for one account, pay the first, wait for it to reach a final state, then run a fresh discovery. Two payments in flight for the same account are refused with `409 PAYMENT_IN_PROGRESS`.

## What your customer pays

| Field    | Meaning                                               |
| -------- | ----------------------------------------------------- |
| `amount` | What the biller is owed                               |
| `fee`    | The OneClickDz service fee for paying it              |
| `total`  | `amount + fee` — the figure debited from your balance |

The fee is a percentage of the amount, clamped between a minimum and a maximum, and it is **set per biller**:

| Biller            | Percentage | Minimum | Maximum |
| ----------------- | ---------- | ------- | ------- |
| `ADE`             | 0.5%       | 30 DZD  | 60 DZD  |
| `SONELGAZ`        | 0.5%       | 30 DZD  | 60 DZD  |
| `Algérie Télécom` | 0.5%       | 10 DZD  | 50 DZD  |

`SEAAL` and `AADL` are currently unavailable, so no fee is published for them.

Worked through, for `ADE` and `SONELGAZ`:

| Bill amount | 0.5% of it | Applied fee                     | Total                                               |
| ----------- | ---------- | ------------------------------- | --------------------------------------------------- |
| 150.00      | 0.75       | —                               | Below the 200 DZD floor; never appears in `bills[]` |
| 320.00      | 1.60       | 30.00 (minimum)                 | 350.00                                              |
| 443.39      | 2.22       | 30.00 (minimum)                 | 473.39                                              |
| 1200.00     | 6.00       | 30.00 (minimum)                 | 1230.00                                             |
| 6000.00     | 30.00      | 30.00 (the percentage, at last) | 6030.00                                             |
| 15000.00    | 75.00      | 60.00 (maximum)                 | 15060.00                                            |

<Note>
  The percentage only starts to matter on large bills. For `ADE` and `SONELGAZ` every bill up to 6,000 DZD is charged the 30 DZD minimum, and nothing is ever charged more than 60 DZD. For `Algérie Télécom` the equivalent thresholds are 2,000 DZD and 10,000 DZD.
</Note>

<Warning>
  Read `fee` from the response. The percentage and the clamps are configuration, not constants — a fee you calculate yourself will eventually disagree with the one you are charged.
</Warning>

## Before you send

<Steps>
  <Step title="Persist your own record">
    Write the order — customer, `transactionId`, `billId`, `amount`, `fee`, `total`, and the `ref` you are about to use — **before** the request leaves your process. If the response is lost, that row is how you find the payment again.
  </Step>

  <Step title="Confirm the total with your customer">
    Show `amount` and `fee` separately, and the `total` you will charge. Never show an estimate.
  </Step>

  <Step title="Send once">
    One call, with a `ref` that is new for this biller.
  </Step>

  <Step title="Poll to a final state">
    `SUCCESS`, `FAILED` or `REFUNDED`.

    → [Step 4: Status polling](/en/bill-payment-guides/4-status-polling)
  </Step>
</Steps>

## Submitting the payment

Three fields, all required.

```json theme={null}
{
  "transactionId": "68b2f4c1a7d3e9f204c81a55",
  "billId": "sbx_bill_68b2f4c1a7d3e9f204c81a55_0",
  "ref": "pay-inv-2026-0042"
}
```

<Note>
  Use a **new** `ref`, different from the discovery's. Reusing the discovery `ref` here answers `403 DUPLICATED_REF`. The transaction keeps its original discovery `ref` — that is what the response echoes back and what `by-ref` looks up.
</Note>

<CodeGroup>
  ```bash cURL theme={null}
  curl https://billapi.oneclickdz.com/v3/bills/pay \
    -X POST \
    -H "Content-Type: application/json" \
    -H "X-Access-Token: YOUR_API_KEY" \
    -d '{
      "transactionId": "68b2f4c1a7d3e9f204c81a55",
      "billId": "sbx_bill_68b2f4c1a7d3e9f204c81a55_0",
      "ref": "pay-inv-2026-0042"
    }'
  ```

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

  async function payBill(order, bill) {
    // 1. Persist before sending — this row is your recovery path.
    await db.payments.insert({
      orderId: order.id,
      transactionId: order.transactionId,
      billId: bill.billId,
      amount: bill.amount,
      fee: bill.fee,
      total: bill.amount + bill.fee,
      discoveryRef: order.discoveryRef,
      paymentRef: `pay-${order.id}`,
      state: "SUBMITTING",
    });

    const response = await fetch(`${BASE}/v3/bills/pay`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Access-Token": KEY,
      },
      body: JSON.stringify({
        transactionId: order.transactionId,
        billId: bill.billId,
        ref: `pay-${order.id}`,
      }),
    });

    const body = await response.json();

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

    // PROCESSING — in flight, not paid.
    await db.payments.update(order.id, { state: "PROCESSING" });
    return body.data.transactionId;
  }
  ```

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

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


  def pay_bill(order, bill):
      # 1. Persist before sending — this row is your recovery path.
      db.payments.insert({
          'order_id': order['id'],
          'transaction_id': order['transaction_id'],
          'bill_id': bill['billId'],
          'amount': bill['amount'],
          'fee': bill['fee'],
          'total': bill['amount'] + bill['fee'],
          'discovery_ref': order['discovery_ref'],
          'payment_ref': f"pay-{order['id']}",
          'state': 'SUBMITTING'
      })

      response = requests.post(
          f'{BASE}/v3/bills/pay',
          headers={
              'Content-Type': 'application/json',
              'X-Access-Token': KEY
          },
          json={
              'transactionId': order['transaction_id'],
              'billId': bill['billId'],
              'ref': f"pay-{order['id']}"
          }
      )

      body = response.json()

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

      # PROCESSING — in flight, not paid.
      db.payments.update(order['id'], {'state': 'PROCESSING'})
      return body['data']['transactionId']
  ```

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

  function payBill(array $order, array $bill): string
  {
      // 1. Persist before sending — this row is your recovery path.
      $db->payments->insert([
          'order_id'       => $order['id'],
          'transaction_id' => $order['transaction_id'],
          'bill_id'        => $bill['billId'],
          'amount'         => $bill['amount'],
          'fee'            => $bill['fee'],
          'total'          => $bill['amount'] + $bill['fee'],
          'discovery_ref'  => $order['discovery_ref'],
          'payment_ref'    => 'pay-' . $order['id'],
          'state'          => 'SUBMITTING'
      ]);

      $ch = curl_init(BASE . '/v3/bills/pay');
      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([
          'transactionId' => $order['transaction_id'],
          'billId'        => $bill['billId'],
          'ref'           => 'pay-' . $order['id']
      ]));

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

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

      // PROCESSING — in flight, not paid.
      $db->payments->update($order['id'], ['state' => 'PROCESSING']);
      return $body['data']['transactionId'];
  }
  ?>
  ```
</CodeGroup>

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

## The three guards

Three rules stop the same money moving twice. All three answer **before** anything is charged.

| Guard               | Response                  | Meaning                                                          |
| ------------------- | ------------------------- | ---------------------------------------------------------------- |
| Duplicate reference | `403 DUPLICATED_REF`      | This `ref` already belongs to a live transaction for this biller |
| Already paid        | `409 BILL_ALREADY_PAID`   | This bill has already been paid successfully                     |
| Payment in flight   | `409 PAYMENT_IN_PROGRESS` | Another payment for this bill has not finished                   |

<Warning>
  None of these is a reason to retry with a different `ref`. Each one means the work is either already done or already running. Retrying around a guard is how a customer gets charged twice.
</Warning>

The right response to all three is the same: read the transaction and continue from its state.

```javascript theme={null}
async function payOnce(order, bill) {
  try {
    return await payBill(order, bill);
  } catch (error) {
    const [code] = String(error.message).split(":");

    if (["DUPLICATED_REF", "BILL_ALREADY_PAID", "PAYMENT_IN_PROGRESS"].includes(code)) {
      // Already handled by us or by an earlier attempt — do not send again.
      const transaction = await getTransaction(order.transactionId);
      return transaction.transactionId;
    }

    throw error;
  }
}
```

## When the response never arrives

A timeout tells you nothing about whether the payment happened. Read the transaction before you do anything else.

```javascript theme={null}
async function settleUnknownOutcome(order) {
  const response = await fetch(
    `${BASE}/v3/bills/transactions/${order.transactionId}`,
    { headers: { "X-Access-Token": KEY } },
  );

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

  switch (body.data.status) {
    case "READY":
      return "NOT_SENT"; // The payment never started — safe to send it.
    case "PROCESSING":
    case "UNKNOWN":
      return "IN_FLIGHT"; // Keep polling. Do not resend.
    case "SUCCESS":
    case "FAILED":
    case "REFUNDED":
      return body.data.status; // Already settled.
    default:
      return "IN_FLIGHT";
  }
}
```

<Warning>
  **Never resend a payment because a request timed out.** A transaction still on `READY` is the only state that proves the payment did not start.
</Warning>

## 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                           |
| `DUPLICATED_REF`      | 403  | The `ref` is already in use for this biller           | Read the transaction; do not resend                              |
| `NOT_FOUND`           | 404  | Not yours, not `READY`, or that `billId` is not on it | Re-read the transaction before doing anything                    |
| `BILL_ALREADY_PAID`   | 409  | This bill was already paid                            | Use the existing receipt; do not charge twice                    |
| `PAYMENT_IN_PROGRESS` | 409  | Another payment for this bill is running              | Poll the running one                                             |
| `PARTNER_UNAVAILABLE` | 503  | The biller is unreachable or switched off             | Nothing was charged; the discovery is still `READY`              |
| `AUTH_UNAVAILABLE`    | 503  | We could not verify your key in time                  | Honour `Retry-After`; the request never reached the payment path |
| `SERVICE_UNAVAILABLE` | 503  | Planned maintenance                                   | Honour `Retry-After` and retry                                   |
| `INTERNAL_ERROR`      | 500  | Something failed on our side                          | Read the transaction first; never resend blindly                 |

## Best practices

<CardGroup cols={2}>
  <Card title="Write before you send" icon="database">
    Persist the order and its `ref` first. A lost response is then always recoverable.
  </Card>

  <Card title="One ref per call" icon="fingerprint">
    `disc-` for the discovery, `pay-` for the payment, both derived from your order id.
  </Card>

  <Card title="Charge the returned total" icon="calculator">
    Debit your customer the `total` the API produced, never a figure you computed.
  </Card>

  <Card title="Treat a guard as an answer" icon="shield-halved">
    `403` and `409` mean the work is done or running. Look it up rather than working around it.
  </Card>
</CardGroup>

## Next step

<Card title="Step 4: Status polling" icon="arrows-rotate" href="/en/bill-payment-guides/4-status-polling">
  Follow the payment to a final state, and handle `UNKNOWN` correctly
</Card>

## Related pages

<CardGroup cols={2}>
  <Card title="Pay a Bill" icon="money-bill-transfer" href="/en/api-reference/bill-payment/pay-bill">
    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="Discovering Bills" icon="magnifying-glass-dollar" href="/en/bill-payment-guides/2-discovering-bills">
    Where `billId` comes from
  </Card>

  <Card title="Receipts and Reconciliation" icon="scale-balanced" href="/en/bill-payment-guides/5-receipts-and-reconciliation">
    What to keep after a success
  </Card>
</CardGroup>
