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

# Reçus et rapprochement

> Conservez la preuve de paiement et équilibrez vos comptes chaque jour

## Vue d'ensemble

Un paiement qui a atteint `SUCCESS` laisse deux preuves : `operationId`, la référence propre du facturier pour la transaction, et le fichier du reçu. Ensemble, ils tranchent n'importe quel litige qu'un client soulèvera des mois plus tard.

Cette étape couvre le téléchargement et le stockage des deux, ainsi qu'une routine quotidienne qui prouve que votre comptabilité et la nôtre concordent.

## Ce qu'un succès vous donne

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

| Champ          | Pourquoi le conserver                                                                     |
| -------------- | ----------------------------------------------------------------------------------------- |
| `operationId`  | C'est la référence du facturier pour ce paiement — celle avec laquelle un litige se règle |
| `receiptUrl`   | Il pointe vers le fichier du reçu. Téléchargez-le ; ne stockez pas le lien                |
| `total`        | Le montant exact débité de votre solde                                                    |
| `selectedBill` | La facture réellement payée, avec son `amount` et son `fee`                               |
| `completedAt`  | Le moment où le paiement a été soldé                                                      |

<Warning>
  `receiptUrl` exige votre en-tête `X-Access-Token`. Ce n'est **pas** un lien que vous pouvez envoyer par e-mail à un client ou intégrer dans une page. Téléchargez les octets et servez-les depuis votre propre système.
</Warning>

## Télécharger le reçu

Récupérez-le dans la même étape que celle qui enregistre le succès, et déduisez l'extension du fichier de l'en-tête `Content-Type` — un reçu est un PDF ou une image selon le facturier.

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

Un reçu n'existe que pour une transaction `SUCCESS`. Tout le reste — encore en cours, `FAILED`, `REFUNDED`, ou appartenant à un autre partenaire — répond `404 NOT_FOUND` dans l'enveloppe JSON habituelle.

## Le stocker

<Steps>
  <Step title="Télécharger une seule fois, au moment du règlement">
    Récupérez le reçu dans la même étape que celle qui marque votre commande comme payée. Réessayer plus tard est acceptable, mais n'attendez pas qu'un client le demande.
  </Step>

  <Step title="Stocker les octets, pas l'URL">
    Placez le fichier dans votre propre stockage d'objets, indexé par l'identifiant de votre commande. L'URL de l'API exige votre clé et n'est utile à personne d'autre.
  </Step>

  <Step title="Stocker operationId à côté">
    Le reçu est le document ; `operationId` est la référence. Conservez les deux dans l'enregistrement de la commande.
  </Step>

  <Step title="Le servir derrière votre propre authentification">
    Votre client le télécharge chez vous, pas chez nous.
  </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,
  });
}
```

## Rapprocher une journée

`GET /v3/bills/transactions` avec `from` et `to` vous donne tout ce qui s'est passé dans une fenêtre. Bornez la fenêtre — une liste non bornée grandit avec votre activité, et une liste bornée ne peut pas se décaler sous vos pieds pendant que vous paginez.

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

## Ce que signifie chaque écart

| Constat            | Signification                                                         | Action                                                                                              |
| ------------------ | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `missedSuccess`    | Nous vous avons débité ; votre commande n'est pas marquée comme payée | Soldez-la maintenant, téléchargez le reçu, notifiez le client                                       |
| `refundMarkedPaid` | L'argent est revenu ; votre commande indique toujours payée           | Annulez l'opération de votre côté et libérez les fonds du client                                    |
| `stillOpen`        | Une transaction de cette journée n'a pas atteint d'état final         | Gardez-la dans le rapprochement jusqu'à ce qu'elle y parvienne. Ne la clôturez jamais comme échouée |
| `unknownToUs`      | Une transaction dont le `ref` ne correspond à aucune de vos commandes | Enquêtez — généralement une découverte jamais payée, ou une écriture perdue                         |

<Note>
  Ne rapprochez que sur `SUCCESS` et `REFUNDED`. Une transaction `FAILED` n'a jamais déplacé d'argent, et une découverte `PENDING` ou `READY` n'a jamais rien débité.
</Note>

## Une routine quotidienne

<Steps>
  <Step title="Exécuter une fois par jour, pour la veille">
    Bornez la fenêtre avec `from` et `to`. La veille est complète ; aujourd'hui bouge encore.
  </Step>

  <Step title="Faire correspondre sur ref">
    Votre `ref` est la clé de jointure entre nos transactions et vos commandes. C'est à cela qu'il sert.
  </Step>

  <Step title="Additionner SUCCESS et REFUNDED séparément">
    Le mouvement net vaut les totaux `SUCCESS` moins les totaux `REFUNDED`. Les deux doivent figurer dans votre grand livre.
  </Step>

  <Step title="Reporter les transactions ouvertes">
    Tout ce qui est encore `PENDING`, `PROCESSING` ou `UNKNOWN` reste sur la liste jusqu'à sa résolution.
  </Step>

  <Step title="Alerter sur le moindre écart">
    Un rapprochement qui ne trouve rien doit rester silencieux. Un rapprochement qui trouve quelque chose doit alerter quelqu'un.
  </Step>
</Steps>

## Bonnes pratiques

<CardGroup cols={2}>
  <Card title="Conserver les deux preuves" icon="receipt">
    `operationId` et le fichier du reçu. L'un sans l'autre n'est qu'une demi-réponse à un litige.
  </Card>

  <Card title="Rapprocher chaque jour, pas chaque mois" icon="calendar">
    Une fenêtre d'une journée est assez petite pour être examinée à la main. Un mois ne l'est pas.
  </Card>

  <Card title="Ne jamais clôturer une transaction ouverte" icon="clock-rotate-left">
    `UNKNOWN` et `PROCESSING` se résolvent d'eux-mêmes. Reportez-les au lieu de les passer en perte.
  </Card>

  <Card title="Servir les reçus vous-même" icon="shield-halved">
    Derrière votre propre authentification, depuis votre propre stockage. Ne partagez jamais l'URL de l'API.
  </Card>
</CardGroup>

## Étape suivante

<Card title="Étape 6 : Tests en sandbox" icon="flask" href="/fr/bill-payment-guides/6-sandbox-testing">
  Reproduisez chaque résultat à la demande, puis passez en production en toute confiance
</Card>

## Pages associées

<CardGroup cols={2}>
  <Card title="Télécharger le reçu" icon="file-arrow-down" href="/fr/api-reference/bill-payment/get-receipt">
    La référence de l'endpoint
  </Card>

  <Card title="Lister les transactions" icon="list" href="/fr/api-reference/bill-payment/list-transactions">
    Filtres, pagination et `meta.total`
  </Card>

  <Card title="Interrogation du statut" icon="arrows-rotate" href="/fr/bill-payment-guides/4-status-polling">
    Atteindre un état final
  </Card>

  <Card title="Obtenir la transaction par ID" icon="id-card" href="/fr/api-reference/bill-payment/check-by-id">
    Où apparaît `operationId`
  </Card>
</CardGroup>
