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

# Interrogation du statut

> Suivez une transaction jusqu'à un état final sans jamais payer deux fois

## Vue d'ensemble

La découverte et le paiement sont asynchrones : l'interrogation n'est donc pas une optimisation, c'est le mécanisme même. Tout ce que vous devez savoir sur une transaction se trouve dans son `status`, lu via [Obtenir la transaction par ID](/fr/api-reference/bill-payment/check-by-id).

<Note>
  Le paiement de factures ne vous envoie pas de notifications. L'interrogation est la façon dont les résultats vous parviennent, aussi bien pour les découvertes que pour les paiements.
</Note>

## La machine à états

Toutes les transitions qu'une transaction peut effectuer :

| De           | Vers         | Déclencheur                                                  |
| ------------ | ------------ | ------------------------------------------------------------ |
| —            | `PENDING`    | `POST /v3/bills/discover`                                    |
| `PENDING`    | `READY`      | Découverte terminée — `bills[]` présent, éventuellement vide |
| `PENDING`    | `FAILED`     | La découverte n'a pas pu aboutir                             |
| `READY`      | `PROCESSING` | `POST /v3/bills/pay`                                         |
| `PROCESSING` | `SUCCESS`    | Payé                                                         |
| `PROCESSING` | `FAILED`     | Refusé ; rien n'a été débité                                 |
| `PROCESSING` | `REFUNDED`   | Débité, puis restitué intégralement                          |
| `PROCESSING` | `UNKNOWN`    | Résultat non confirmé                                        |
| `UNKNOWN`    | `SUCCESS`    | Paiement confirmé                                            |
| `UNKNOWN`    | `REFUNDED`   | Non-paiement confirmé ; argent restitué                      |

`SUCCESS`, `FAILED` et `REFUNDED` sont finaux — une transaction ne les quitte jamais.

| Statut       | Signification                             | Continuer l'interrogation ? | Mouvement d'argent  |
| ------------ | ----------------------------------------- | --------------------------- | ------------------- |
| `PENDING`    | Découverte acceptée, pas terminée         | Oui                         | Non                 |
| `READY`      | Découverte terminée ; `bills[]` présent   | Non — c'est à vous d'agir   | Non                 |
| `PROCESSING` | Paiement en cours                         | Oui                         | Pas encore confirmé |
| `SUCCESS`    | Payé                                      | Non                         | Oui — débité        |
| `FAILED`     | N'a pas abouti                            | Non                         | Non                 |
| `REFUNDED`   | Débité, puis restitué intégralement       | Non                         | Solde nul           |
| `UNKNOWN`    | Résultat non confirmé ; en cours d'examen | **Oui**                     | Pas encore connu    |

## Réglages recommandés

Ce sont des points de départ, pas des garanties. Mesurez votre propre trafic et ajustez.

| Phase                   | Intervalle        | Abandonner après                                             |
| ----------------------- | ----------------- | ------------------------------------------------------------ |
| Découverte (`PENDING`)  | 2 s, jusqu'à 10 s | 2 minutes                                                    |
| Paiement (`PROCESSING`) | 3 s, jusqu'à 10 s | 5 minutes                                                    |
| Examen (`UNKNOWN`)      | 30 s              | N'abandonnez pas — confiez-la à une tâche de fond plus lente |

<Warning>
  « Abandonner » signifie **arrêter l'interrogation au premier plan**, pas « décider que le paiement a échoué ». Une transaction que vous avez cessé de surveiller a toujours un résultat réel ; confiez-la à une tâche de rapprochement en arrière-plan qui continue de vérifier.
</Warning>

Augmentez l'intervalle au lieu d'en marteler un fixe. Un paiement qui n'est pas terminé au bout de trois secondes ne se terminera pas plus vite parce que vous avez redemandé.

## Une boucle d'interrogation de qualité production

La même structure en quatre langages : un intervalle initial, une croissance jusqu'à un plafond, une échéance globale, et une branche par statut.

<CodeGroup>
  ```bash cURL theme={null}
  #!/usr/bin/env bash
  # Poll one transaction until it reaches a final state.
  BASE="https://billapi.oneclickdz.com"
  TXN="68b2f4c1a7d3e9f204c81a55"
  INTERVAL=3
  DEADLINE=$(( $(date +%s) + 300 ))

  while [ "$(date +%s)" -lt "$DEADLINE" ]; do
    STATUS=$(curl -s "$BASE/v3/bills/transactions/$TXN" \
      -H "X-Access-Token: YOUR_API_KEY" \
      | grep -o '"status":"[A-Z_]*"' | head -1 | cut -d'"' -f4)

    echo "status=$STATUS"

    case "$STATUS" in
      SUCCESS|FAILED|REFUNDED) exit 0 ;;
      UNKNOWN)                 INTERVAL=30 ;;
      *)                       [ "$INTERVAL" -lt 10 ] && INTERVAL=$((INTERVAL + 2)) ;;
    esac

    sleep "$INTERVAL"
  done

  echo "still not final — hand over to background reconciliation"
  exit 1
  ```

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

  const FINAL = new Set(["SUCCESS", "FAILED", "REFUNDED"]);
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

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

    const body = await response.json();

    // Transient — worth another attempt.
    if (["AUTH_UNAVAILABLE", "SERVICE_UNAVAILABLE"].includes(body?.error?.code)) {
      return null;
    }

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

    return body.data;
  }

  async function pollUntilFinal(transactionId, { timeoutMs = 300_000 } = {}) {
    const deadline = Date.now() + timeoutMs;
    let interval = 3000;

    while (Date.now() < deadline) {
      const transaction = await getTransaction(transactionId);

      if (transaction) {
        if (FINAL.has(transaction.status)) return transaction;

        // Under review — slow right down, but never stop.
        interval = transaction.status === "UNKNOWN" ? 30_000 : Math.min(interval + 2000, 10_000);
      }

      await sleep(interval);
    }

    // Not final yet. Hand over — never assume a failure.
    await queueForReconciliation(transactionId);
    return null;
  }
  ```

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

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

  FINAL = {'SUCCESS', 'FAILED', 'REFUNDED'}
  TRANSIENT = {'AUTH_UNAVAILABLE', 'SERVICE_UNAVAILABLE'}


  def get_transaction(transaction_id):
      response = requests.get(
          f'{BASE}/v3/bills/transactions/{transaction_id}',
          headers={'X-Access-Token': KEY}
      )

      body = response.json()

      # Transient — worth another attempt.
      if body.get('error', {}).get('code') in TRANSIENT:
          return None

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

      return body['data']


  def poll_until_final(transaction_id, timeout_s=300):
      deadline = time.monotonic() + timeout_s
      interval = 3.0

      while time.monotonic() < deadline:
          transaction = get_transaction(transaction_id)

          if transaction:
              if transaction['status'] in FINAL:
                  return transaction

              # Under review — slow right down, but never stop.
              interval = 30.0 if transaction['status'] == 'UNKNOWN' else min(interval + 2, 10)

          time.sleep(interval)

      # Not final yet. Hand over — never assume a failure.
      queue_for_reconciliation(transaction_id)
      return None
  ```

  ```php PHP theme={null}
  <?php
  const BASE = 'https://billapi.oneclickdz.com';
  const FINAL_STATUSES = ['SUCCESS', 'FAILED', 'REFUNDED'];
  const TRANSIENT = ['AUTH_UNAVAILABLE', 'SERVICE_UNAVAILABLE'];

  function getTransaction(string $transactionId): ?array
  {
      $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);

      // Transient — worth another attempt.
      if (in_array($body['error']['code'] ?? '', TRANSIENT, true)) {
          return null;
      }

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

      return $body['data'];
  }

  function pollUntilFinal(string $transactionId, int $timeoutSeconds = 300): ?array
  {
      $deadline = time() + $timeoutSeconds;
      $interval = 3;

      while (time() < $deadline) {
          $transaction = getTransaction($transactionId);

          if ($transaction !== null) {
              if (in_array($transaction['status'], FINAL_STATUSES, true)) {
                  return $transaction;
              }

              // Under review — slow right down, but never stop.
              $interval = $transaction['status'] === 'UNKNOWN'
                  ? 30
                  : min($interval + 2, 10);
          }

          sleep($interval);
      }

      // Not final yet. Hand over — never assume a failure.
      queueForReconciliation($transactionId);
      return null;
  }
  ?>
  ```
</CodeGroup>

## Agir selon chaque statut

Une branche par statut, et aucun cas par défaut qui suppose un échec.

```javascript theme={null}
async function applyOutcome(order, transaction) {
  switch (transaction.status) {
    case "SUCCESS":
      // Money moved. Keep the proof.
      await db.orders.update(order.id, {
        state: "PAID",
        operationId: transaction.operationId,
        total: transaction.total,
      });
      await downloadReceipt(transaction.transactionId);
      await notifyCustomerPaid(order);
      return;

    case "FAILED":
      // Nothing was charged. Safe to release the customer's funds.
      await db.orders.update(order.id, {
        state: "FAILED",
        reason: transaction.error?.code,
      });
      await releaseCustomerFunds(order);
      return;

    case "REFUNDED":
      // Money moved and came back in full. Same customer outcome as FAILED.
      await db.orders.update(order.id, {
        state: "REFUNDED",
        reason: transaction.error?.code,
      });
      await releaseCustomerFunds(order);
      return;

    case "UNKNOWN":
      // Not an outcome. Keep watching, and keep the customer's money held.
      await db.orders.update(order.id, { state: "UNDER_REVIEW" });
      await queueForReconciliation(transaction.transactionId);
      return;

    case "PENDING":
    case "PROCESSING":
      // Still working. Nothing to do but poll.
      return;
  }
}
```

## Gérer `UNKNOWN`

`UNKNOWN` signifie que le résultat n'a pas été confirmé. Ce n'est ni un échec ni un succès. Il se résout de lui-même en `SUCCESS` ou en `REFUNDED`.

<Warning>
  Tant qu'une transaction est `UNKNOWN` :

  * Ne remboursez **jamais** votre propre client.
  * Ne renvoyez **jamais** le paiement.
  * N'affichez **jamais** « paiement échoué » dans votre interface.

  Faire l'une de ces choses transforme un paiement incertain en perte certaine — soit vous remboursez une facture qui a réellement été payée, soit vous la payez deux fois.
</Warning>

Ce qu'il faut faire à la place :

<Steps>
  <Step title="Bloquer les fonds du client">
    Gardez le montant réservé de votre côté et affichez un état neutre — « paiement en cours de confirmation », pas « échoué ».
  </Step>

  <Step title="Ralentir fortement l'interrogation">
    Passez à une cadence de 30 secondes, ou à une tâche en arrière-plan qui vérifie périodiquement. Interroger fréquemment n'accélère pas un examen.
  </Step>

  <Step title="N'agir que sur la résolution">
    `SUCCESS` — soldez et conservez le reçu. `REFUNDED` — libérez les fonds. Ce n'est qu'ensuite que vous informez le client.
  </Step>
</Steps>

## Gérer `REFUNDED`

`REFUNDED` signifie que le paiement a été débité puis restitué intégralement. Le résultat côté client est le même que pour `FAILED` — la facture n'est pas payée — mais votre comptabilité diffère : l'argent est parti et revenu, les deux mouvements doivent donc figurer dans votre grand livre.

`error.code` explique pourquoi le paiement n'a pas abouti :

| `error.code`          | `error.message`                                         |
| --------------------- | ------------------------------------------------------- |
| `PAYMENT_DECLINED`    | The payment was declined by the bank or partner portal. |
| `PARTNER_UNAVAILABLE` | The partner service is temporarily unavailable.         |
| `INVALID_ACCOUNT`     | The provided account identifier is invalid.             |
| `BILL_ALREADY_PAID`   | This bill has already been paid.                        |

Les quatre mêmes codes apparaissent sur `FAILED`. Branchez sur `code`, jamais sur `message`.

## Gérer les erreurs transitoires pendant l'interrogation

Une interrogation qui échoue n'est pas une transaction qui a échoué.

| Réponse                   | Signification                                                        | Que faire                                                                       |
| ------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `503 AUTH_UNAVAILABLE`    | Nous n'avons pas pu vérifier votre clé à temps                       | Attendez le délai `Retry-After` (5 s), puis interrogez à nouveau                |
| `503 SERVICE_UNAVAILABLE` | Maintenance planifiée                                                | Attendez le délai `Retry-After`, puis interrogez à nouveau                      |
| `500 INTERNAL_ERROR`      | Quelque chose a échoué de notre côté                                 | Interrogez à nouveau ; escaladez avec le `requestId` si cela persiste           |
| `404 NOT_FOUND`           | Ce n'est pas votre transaction, ou ce n'est pas le bon environnement | Vérifiez l'identifiant et la clé — ne traitez pas cela comme un paiement échoué |

<Note>
  Ne laissez jamais une interrogation échouée modifier l'état de votre commande. Seul un `status` réel issu d'une lecture réussie peut le faire.
</Note>

## Ce qu'il ne faut jamais faire

| Jamais                                                      | Pourquoi                                               | À la place                                                       |
| ----------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------- |
| Renvoyer `pay` après un délai dépassé                       | Le premier est peut-être passé                         | Lisez la transaction ; seul `READY` prouve qu'il n'a pas démarré |
| Rembourser votre client sur `UNKNOWN`                       | Cela peut se résoudre en `SUCCESS`                     | Bloquez les fonds et continuez d'interroger                      |
| Traiter une erreur d'interrogation comme un paiement échoué | La transaction n'est pas affectée                      | Réessayez l'interrogation                                        |
| Interroger indéfiniment à un intervalle fixe d'une seconde  | Cela coûte aux deux parties et n'aide personne         | Augmentez l'intervalle jusqu'à un plafond                        |
| Se baser sur `completedAt`                                  | Il est renseigné dans des états qui ne sont pas finaux | Basez-vous sur `status`                                          |

## Bonnes pratiques

<CardGroup cols={2}>
  <Card title="Une boucle, une transaction" icon="route">
    Suivez une transaction par son identifiant. Lister les transactions à répétition pour la retrouver est plus lent et plus lourd.
  </Card>

  <Card title="Augmenter l'intervalle" icon="stopwatch">
    Commencez à quelques secondes, montez jusqu'à une dizaine. Ralentissez à 30 secondes dès qu'une transaction est `UNKNOWN`.
  </Card>

  <Card title="Transmettre, ne pas abandonner" icon="clock-rotate-left">
    Lorsque l'interrogation au premier plan expire, mettez la transaction en file pour un rapprochement en arrière-plan.
  </Card>

  <Card title="Journaliser le requestId" icon="fingerprint">
    Chaque interrogation en renvoie un. Conservez le dernier avec votre commande — c'est ce dont le support a besoin.
  </Card>
</CardGroup>

## Étape suivante

<Card title="Étape 5 : Reçus et rapprochement" icon="scale-balanced" href="/fr/bill-payment-guides/5-receipts-and-reconciliation">
  Conservez la preuve de paiement et rapprochez votre grand livre chaque jour
</Card>

## Pages associées

<CardGroup cols={2}>
  <Card title="Obtenir la transaction par ID" icon="id-card" href="/fr/api-reference/bill-payment/check-by-id">
    L'objet transaction et la matrice des champs
  </Card>

  <Card title="Stratégies d'interrogation" icon="chart-line" href="/fr/polling-strategies">
    Conseils d'interrogation valables pour tous les produits
  </Card>

  <Card title="Payer des factures" icon="money-bill-transfer" href="/fr/bill-payment-guides/3-paying-bills">
    Ce qu'il faut faire avant le paiement
  </Card>

  <Card title="Tests en sandbox" icon="flask" href="/fr/bill-payment-guides/6-sandbox-testing">
    Reproduire `UNKNOWN` et `REFUNDED` à la demande
  </Card>
</CardGroup>
