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

# Paiement des factures

> Débitez une facture découverte en toute sécurité, et jamais deux fois

## Vue d'ensemble

Le paiement est la moitié écriture du Paiement de Factures. Vous prenez un `billId` d'une découverte `READY`, vous le soumettez, et la même transaction porte le paiement jusqu'à un état final.

C'est l'appel qui déplace de l'argent, donc l'ordre des opérations compte ici plus que partout ailleurs dans l'intégration : **écrivez d'abord votre propre enregistrement, envoyez une seule fois, puis interrogez.**

<Warning>
  Le `200` de `pay` signifie que le paiement a été **accepté et est en cours**. Cela ne signifie pas que la facture a été payée. Le résultat n'apparaît jamais ailleurs que dans le `status` de la transaction.
</Warning>

## Choisir une facture

`bills[]` sur une transaction `READY` peut contenir plusieurs entrées. Choisissez-en une — un paiement paie exactement une facture.

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

Pour payer plusieurs factures d'un même compte, payez la première, attendez qu'elle atteigne un état final, puis lancez une nouvelle découverte. Deux paiements en cours pour le même compte sont refusés avec `409 PAYMENT_IN_PROGRESS`.

## Ce que votre client paie

| Champ    | Signification                                     |
| -------- | ------------------------------------------------- |
| `amount` | Ce qui est dû au facturier                        |
| `fee`    | Les frais de service OneClickDz pour la payer     |
| `total`  | `amount + fee` — le montant débité de votre solde |

Les frais sont un pourcentage du montant, borné entre un minimum et un maximum, et ils sont **définis par facturier** :

| Facturier         | Pourcentage | 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` et `AADL` sont actuellement indisponibles : aucun frais n'est publié pour eux.

En détail, pour `ADE` et `SONELGAZ` :

| Montant de la facture | 0,5 % de ce montant | Frais appliqués               | Total                                                          |
| --------------------- | ------------------- | ----------------------------- | -------------------------------------------------------------- |
| 150.00                | 0.75                | —                             | Sous le plancher de 200 DZD ; n'apparaît jamais dans `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 (le pourcentage, enfin) | 6030.00                                                        |
| 15000.00              | 75.00               | 60.00 (maximum)               | 15060.00                                                       |

<Note>
  Le pourcentage ne commence à compter que sur les grosses factures. Pour `ADE` et `SONELGAZ`, toute facture jusqu'à 6 000 DZD est facturée au minimum de 30 DZD, et rien n'est jamais facturé plus de 60 DZD. Pour `Algérie Télécom`, les seuils équivalents sont 2 000 DZD et 10 000 DZD.
</Note>

<Warning>
  Lisez `fee` depuis la réponse. Le pourcentage et les bornes relèvent de la configuration, ce ne sont pas des constantes — des frais que vous calculez vous-même finiront par diverger de ceux qui vous sont facturés.
</Warning>

## Avant d'envoyer

<Steps>
  <Step title="Enregistrez votre propre trace">
    Écrivez la commande — client, `transactionId`, `billId`, `amount`, `fee`, `total`, et le `ref` que vous vous apprêtez à utiliser — **avant** que la requête ne quitte votre processus. Si la réponse est perdue, cette ligne est ce qui vous permet de retrouver le paiement.
  </Step>

  <Step title="Confirmez le total avec votre client">
    Montrez `amount` et `fee` séparément, ainsi que le `total` que vous allez débiter. N'affichez jamais une estimation.
  </Step>

  <Step title="Envoyez une seule fois">
    Un seul appel, avec un `ref` nouveau pour ce facturier.
  </Step>

  <Step title="Interrogez jusqu'à un état final">
    `SUCCESS`, `FAILED` ou `REFUNDED`.

    → [Étape 4 : Suivi du statut](/fr/bill-payment-guides/4-status-polling)
  </Step>
</Steps>

## Soumettre le paiement

Trois champs, tous obligatoires.

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

<Note>
  Utilisez un **nouveau** `ref`, différent de celui de la découverte. Réutiliser ici le `ref` de découverte renvoie `403 DUPLICATED_REF`. La transaction conserve son `ref` de découverte d'origine — c'est celui que la réponse renvoie en écho et celui que `by-ref` recherche.
</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"
}
```

## Les trois garde-fous

Trois règles empêchent le même argent de bouger deux fois. Toutes les trois répondent **avant** que quoi que ce soit ne soit débité.

| Garde-fou           | Réponse                   | Signification                                                       |
| ------------------- | ------------------------- | ------------------------------------------------------------------- |
| Référence en double | `403 DUPLICATED_REF`      | Ce `ref` appartient déjà à une transaction active pour ce facturier |
| Déjà payée          | `409 BILL_ALREADY_PAID`   | Cette facture a déjà été payée avec succès                          |
| Paiement en cours   | `409 PAYMENT_IN_PROGRESS` | Un autre paiement pour cette facture n'est pas terminé              |

<Warning>
  Aucun d'eux n'est une raison de réessayer avec un `ref` différent. Chacun signifie que le travail est soit déjà fait, soit déjà en cours. Contourner un garde-fou en réessayant, c'est ainsi qu'un client est débité deux fois.
</Warning>

La bonne réaction aux trois est la même : lisez la transaction et repartez de son état.

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

## Quand la réponse n'arrive jamais

Un timeout ne vous dit rien sur le fait que le paiement ait eu lieu ou non. Lisez la transaction avant de faire quoi que ce soit d'autre.

```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>
  **Ne renvoyez jamais un paiement parce qu'une requête a expiré.** Une transaction encore sur `READY` est le seul état qui prouve que le paiement n'a pas démarré.
</Warning>

## Les erreurs que vous rencontrerez

| Erreur                | HTTP | Ce que cela signifie                                                            | Que faire                                                                     |
| --------------------- | ---- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `ERR_VALIDATION`      | 400  | Le corps ne correspond pas au schéma                                            | Corrigez la requête ; ne réessayez jamais sans changement                     |
| `DUPLICATED_REF`      | 403  | Le `ref` est déjà utilisé pour ce facturier                                     | Lisez la transaction ; ne la renvoyez pas                                     |
| `NOT_FOUND`           | 404  | Elle n'est pas à vous, elle n'est pas `READY`, ou ce `billId` ne s'y trouve pas | Relisez la transaction avant de faire quoi que ce soit                        |
| `BILL_ALREADY_PAID`   | 409  | Cette facture a déjà été payée                                                  | Utilisez le reçu existant ; ne débitez pas deux fois                          |
| `PAYMENT_IN_PROGRESS` | 409  | Un autre paiement pour cette facture est en cours                               | Interrogez celui qui est en cours                                             |
| `PARTNER_UNAVAILABLE` | 503  | Le facturier est injoignable ou désactivé                                       | Rien n'a été débité ; la découverte est toujours `READY`                      |
| `AUTH_UNAVAILABLE`    | 503  | Nous n'avons pas pu vérifier votre clé à temps                                  | Respectez `Retry-After` ; la requête n'a jamais atteint le chemin de paiement |
| `SERVICE_UNAVAILABLE` | 503  | Maintenance planifiée                                                           | Respectez `Retry-After` et réessayez                                          |
| `INTERNAL_ERROR`      | 500  | Quelque chose a échoué de notre côté                                            | Lisez d'abord la transaction ; ne renvoyez jamais à l'aveugle                 |

## Bonnes pratiques

<CardGroup cols={2}>
  <Card title="Écrivez avant d'envoyer" icon="database">
    Enregistrez d'abord la commande et son `ref`. Une réponse perdue est alors toujours récupérable.
  </Card>

  <Card title="Un ref par appel" icon="fingerprint">
    `disc-` pour la découverte, `pay-` pour le paiement, tous deux dérivés de votre identifiant de commande.
  </Card>

  <Card title="Facturez le total renvoyé" icon="calculator">
    Débitez à votre client le `total` produit par l'API, jamais un montant que vous avez calculé.
  </Card>

  <Card title="Traitez un garde-fou comme une réponse" icon="shield-halved">
    `403` et `409` signifient que le travail est fait ou en cours. Consultez-le plutôt que de le contourner.
  </Card>
</CardGroup>

## Étape suivante

<Card title="Étape 4 : Suivi du statut" icon="arrows-rotate" href="/fr/bill-payment-guides/4-status-polling">
  Suivez le paiement jusqu'à un état final, et gérez `UNKNOWN` correctement
</Card>

## Pages liées

<CardGroup cols={2}>
  <Card title="Payer une facture" icon="money-bill-transfer" href="/fr/api-reference/bill-payment/pay-bill">
    La référence de l'endpoint
  </Card>

  <Card title="Obtenir une transaction par ID" icon="id-card" href="/fr/api-reference/bill-payment/check-by-id">
    L'objet transaction en entier
  </Card>

  <Card title="Découverte des factures" icon="magnifying-glass-dollar" href="/fr/bill-payment-guides/2-discovering-bills">
    D'où vient `billId`
  </Card>

  <Card title="Reçus et rapprochement" icon="scale-balanced" href="/fr/bill-payment-guides/5-receipts-and-reconciliation">
    Ce qu'il faut conserver après un succès
  </Card>
</CardGroup>
