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

# Tests en sandbox

> Reproduisez chaque résultat à la demande, puis passez en production en toute confiance

## Vue d'ensemble

Le sandbox est l'endroit où vous prouvez que votre intégration gère un refus, un remboursement et un paiement non confirmé — des résultats que vous ne pouvez pas produire à la demande avec de l'argent réel.

L'identifiant de compte que vous envoyez **choisit le résultat**. Chaque scénario ci-dessous est déterministe : le même identifiant produit toujours le même résultat.

<Note>
  Le sandbox utilise **le même hôte, les mêmes routes et le même en-tête de clé** que la production. La seule chose qui change, c'est la clé. Appelez [Valider la clé API](/fr/api-reference/bill-payment/validate-key) et lisez `key.type` pour confirmer dans quel environnement vous vous trouvez.
</Note>

## Ce qui est identique

Tout ce qui compte pour votre code :

* l'URL de base, `https://billapi.oneclickdz.com`
* l'en-tête `X-Access-Token`
* les huit routes
* l'enveloppe de réponse, `requestId` et l'en-tête `X-Request-Id`
* les sept statuts et le cycle de vie asynchrone
* l'interrogation, l'idempotence par `ref` et les garde-fous `403` / `409`
* les codes d'erreur et leurs statuts HTTP

Le passage en production change votre clé. Il ne change pas une ligne de votre intégration.

## Ce qui diffère

* Une requête sandbox n'atteint jamais un facturier et ne déplace jamais d'argent.
* Les résultats sont choisis par l'identifiant de compte, non par ce qu'un compte doit réellement.
* `GET /v3/partners` renvoie une table fixe plutôt que la disponibilité réelle.
* Les factures du sandbox sont renvoyées avec `fee: 0`, donc `total` est égal à `amount`. Lisez `fee` et `total` dans la réponse — en production, ils ne seront pas nuls.
* Les transitions sont rapides : une découverte aboutit en bien moins d'une seconde, un paiement en environ une demi-seconde. Le scénario `UNKNOWN` reste délibérément en attente pendant environ 60 secondes pour que vous puissiez exercer votre parcours d'examen.

<Warning>
  Le sandbox n'est ni un test de charge ni un test de disponibilité. Un facturier `ACTIVE` dans la table du sandbox peut être hors service en production — gérez `503 PARTNER_UNAVAILABLE` quoi que le sandbox vous ait indiqué.
</Warning>

## Scénarios de flux métier

Envoyez l'identifiant dans l'objet `account` pour le partenaire indiqué. « Découverte » est l'état que la transaction atteint après `POST /v3/bills/discover` ; « Paiement » est celui qu'elle atteint après `POST /v3/bills/pay`.

| Scénario                                           | Partenaire                          | Identifiant de compte                    | Découverte                                  | Paiement                                        |
| -------------------------------------------------- | ----------------------------------- | ---------------------------------------- | ------------------------------------------- | ----------------------------------------------- |
| Cas nominal — facture payable                      | `ADE`                               | `reference: "0123456789012345678901234"` | `READY`, 1 facture @ 443.39 DZD             | `SUCCESS`                                       |
| Cas nominal — plusieurs factures                   | `SONELGAZ`                          | `sonelgaz.invoice_number: "9876543210"`  | `READY`, 2 factures @ 1200.00 et 850.00 DZD | `SUCCESS`                                       |
| Factures trouvées (payables)                       | `ADE`                               | `reference: "0123456789012340000000001"` | `READY`, 1 facture @ 320.00 DZD             | `SUCCESS`                                       |
| Aucune facture à payer                             | `ADE`                               | `reference: "0123456789012340000000002"` | `READY`, `bills: []`                        | Rien à payer — `404 NOT_FOUND`                  |
| Aucune facture à payer                             | `SEAAL`                             | `reference: "0123456789012340000000000"` | `503 PARTNER_UNAVAILABLE` — voir ci-dessous | —                                               |
| Sous le plancher de 200 DZD (toutes filtrées)      | `SONELGAZ`                          | `sonelgaz.invoice_number: "0000000003"`  | `READY`, `bills: []`                        | Rien à payer — `404 NOT_FOUND`                  |
| Sous le plancher de 200 DZD (150 DZD filtrée)      | `ADE`                               | `reference: "0123456789012341111111111"` | `READY`, `bills: []`                        | Rien à payer — `404 NOT_FOUND`                  |
| Paiement refusé (sans débit)                       | `SONELGAZ`                          | `sonelgaz.invoice_number: "4004004004"`  | `READY`, 1 facture @ 500.00 DZD             | `FAILED` + `PAYMENT_DECLINED`                   |
| Paiement refusé                                    | `ADE`                               | `reference: "0123456789012340000000004"` | `READY`, 1 facture @ 400.00 DZD             | `FAILED` + `PAYMENT_DECLINED`                   |
| Refus après débit — remboursement                  | `ADE`                               | `ade.sub_id: "000123456789"`             | `READY`, 1 facture @ 600.00 DZD             | `REFUNDED` + `PAYMENT_DECLINED`                 |
| Échec tardif — remboursement                       | `ADE`                               | `reference: "0123456789012340000000005"` | `READY`, 1 facture @ 550.00 DZD             | `REFUNDED` + `PAYMENT_DECLINED`                 |
| Résultat non confirmé — examen, puis remboursement | `SONELGAZ`                          | `sonelgaz.invoice_number: "6006006006"`  | `READY`, 1 facture @ 900.00 DZD             | `UNKNOWN` pendant environ 60 s, puis `REFUNDED` |
| Compte invalide (mal formé)                        | `ADE`                               | `reference: "abc0000000000000000000000"` | `400 INVALID_ACCOUNT`                       | —                                               |
| Compte invalide (bien formé, inexistant)           | `SEAAL`                             | `reference: "0123456789012340000000009"` | `503 PARTNER_UNAVAILABLE` — voir ci-dessous | —                                               |
| Facturier injoignable / délai dépassé              | `ADE`                               | `reference: "0123456789012345005005005"` | `503 PARTNER_UNAVAILABLE`                   | —                                               |
| Facturier indisponible                             | `AADL`                              | `aadlNumber: "1112223334"`               | `503 PARTNER_UNAVAILABLE`                   | —                                               |
| Déjà payée (garde-fou de 24 heures)                | `ADE`                               | `reference: "0123456789012347777777777"` | `409 BILL_ALREADY_PAID`                     | —                                               |
| Déjà payée                                         | `ADE`                               | `reference: "0123456789012340000000006"` | `409 BILL_ALREADY_PAID`                     | —                                               |
| Identifiant réutilisé                              | `Algérie Télécom`                   | `phoneNumber: "023456789"`               | `READY`, 1 facture @ 300.00 DZD             | `SUCCESS`                                       |
| Tout autre identifiant bien formé                  | N'importe quel facturier disponible | Tout ce qui n'est pas listé ci-dessus    | `READY`, 1 facture @ 500.00 DZD             | `SUCCESS`                                       |

<Note>
  **Les deux lignes SEAAL et la ligne AADL.** `SEAAL` et `AADL` sont actuellement désactivés dans les deux environnements, et cette vérification s'exécute avant le choix du scénario sandbox — ces trois identifiants répondent donc `503 PARTNER_UNAVAILABLE` plutôt que le résultat décrit par leur scénario. Ils sont listés ici parce qu'ils redeviendront accessibles dès que ces facturiers seront réactivés. Pour tester « aucune facture à payer » et « compte invalide » aujourd'hui, utilisez les lignes `ADE`.
</Note>

Les formes complètes des objets, à copier :

```json theme={null}
{
  "partner": "SONELGAZ",
  "account": {
    "sonelgaz": {
      "invoice_number": "9876543210",
      "amount_without_stamp": "15000",
      "ebb_key": "ABC123"
    }
  },
  "ref": "disc-sbx-multi-001"
}
```

```json theme={null}
{
  "partner": "ADE",
  "account": {
    "ade": {
      "sub_id": "000123456789",
      "period": "07/2026",
      "amount": "12000",
      "pay_key": "1234567"
    }
  },
  "ref": "disc-sbx-refund-001"
}
```

## Scénarios d'authentification et de contrôle

| Déclencheur                                | Réponse                    |
| ------------------------------------------ | -------------------------- |
| Omettre l'en-tête `X-Access-Token`         | `401 MISSING_ACCESS_TOKEN` |
| Envoyer une clé inconnue                   | `401 INVALID_ACCESS_TOKEN` |
| Réutiliser un `ref` pour le même facturier | `403 DUPLICATED_REF`       |

<Warning>
  Exercez les trois. Le chemin `403 DUPLICATED_REF` en particulier est celui dont dépend votre logique de reprise — si la réutilisation d'un `ref` surprend votre code en sandbox, elle le surprendra en production avec de l'argent en jeu.
</Warning>

## Un parcours sandbox de bout en bout

Le cas nominal ADE, de la découverte au reçu. Chaque valeur ci-dessous est réelle et reproductible.

<CodeGroup>
  ```bash cURL theme={null}
  BASE="https://billapi.oneclickdz.com"
  KEY="YOUR_SANDBOX_API_KEY"

  # 0. Confirm the environment
  curl -s "$BASE/v3/validate" -H "X-Access-Token: $KEY"

  # 1. Discover
  TXN=$(curl -s "$BASE/v3/bills/discover" \
    -X POST \
    -H "Content-Type: application/json" \
    -H "X-Access-Token: $KEY" \
    -d '{
      "partner": "ADE",
      "account": { "reference": "0123456789012345678901234" },
      "ref": "disc-sbx-001"
    }' | grep -o '"transactionId":"[a-f0-9]*"' | cut -d'"' -f4)

  echo "transaction: $TXN"
  sleep 2

  # 2. Read the bills
  curl -s "$BASE/v3/bills/transactions/$TXN" -H "X-Access-Token: $KEY"

  # 3. Pay the first bill (note the NEW ref)
  curl -s "$BASE/v3/bills/pay" \
    -X POST \
    -H "Content-Type: application/json" \
    -H "X-Access-Token: $KEY" \
    -d "{
      \"transactionId\": \"$TXN\",
      \"billId\": \"sbx_bill_${TXN}_0\",
      \"ref\": \"pay-sbx-001\"
    }"

  sleep 3

  # 4. Confirm the outcome
  curl -s "$BASE/v3/bills/transactions/$TXN" -H "X-Access-Token: $KEY"

  # 5. Download the receipt
  curl -s "$BASE/v3/bills/transactions/$TXN/receipt" \
    -H "X-Access-Token: $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_SANDBOX_API_KEY;
  const headers = { "X-Access-Token": KEY, "Content-Type": "application/json" };
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  async function call(path, options = {}) {
    const response = await fetch(`${BASE}${path}`, { headers, ...options });
    const body = await response.json();
    if (!body.success) throw new Error(`${body.error.code}: ${body.error.message}`);
    return body;
  }

  async function waitFor(transactionId, predicate, timeoutMs = 120_000) {
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
      const { data } = await call(`/v3/bills/transactions/${transactionId}`);
      if (predicate(data)) return data;
      await sleep(2000);
    }
    throw new Error("Timed out");
  }

  // 0. Confirm the environment
  const { data: identity } = await call("/v3/validate");
  if (identity.key.type !== "SANDBOX") throw new Error("Not a sandbox key");

  // 1. Discover
  const { data: started } = await call("/v3/bills/discover", {
    method: "POST",
    body: JSON.stringify({
      partner: "ADE",
      account: { reference: "0123456789012345678901234" },
      ref: "disc-sbx-001",
    }),
  });

  // 2. Wait for the bills
  const ready = await waitFor(started.transactionId, (t) => t.status === "READY");
  console.log(ready.bills); // 1 bill @ 443.39 DZD

  // 3. Pay the first one, with a NEW ref
  await call("/v3/bills/pay", {
    method: "POST",
    body: JSON.stringify({
      transactionId: started.transactionId,
      billId: ready.bills[0].billId,
      ref: "pay-sbx-001",
    }),
  });

  // 4. Wait for a final state
  const final = await waitFor(started.transactionId, (t) =>
    ["SUCCESS", "FAILED", "REFUNDED"].includes(t.status),
  );
  console.log(final.status, final.operationId);

  // 5. Download the receipt
  const receipt = await fetch(
    `${BASE}/v3/bills/transactions/${started.transactionId}/receipt`,
    { headers: { "X-Access-Token": KEY } },
  );
  await writeFile("receipt-sbx.bin", Buffer.from(await receipt.arrayBuffer()));
  ```

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

  BASE = 'https://billapi.oneclickdz.com'
  KEY = os.getenv('ONECLICKDZ_SANDBOX_API_KEY')
  HEADERS = {'X-Access-Token': KEY, 'Content-Type': 'application/json'}


  def call(method, path, payload=None):
      response = requests.request(method, f'{BASE}{path}', headers=HEADERS, json=payload)
      body = response.json()
      if not body['success']:
          raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
      return body


  def wait_for(transaction_id, predicate, timeout_s=120):
      deadline = time.monotonic() + timeout_s
      while time.monotonic() < deadline:
          data = call('GET', f'/v3/bills/transactions/{transaction_id}')['data']
          if predicate(data):
              return data
          time.sleep(2)
      raise TimeoutError('Timed out')


  # 0. Confirm the environment
  identity = call('GET', '/v3/validate')['data']
  assert identity['key']['type'] == 'SANDBOX', 'Not a sandbox key'

  # 1. Discover
  started = call('POST', '/v3/bills/discover', {
      'partner': 'ADE',
      'account': {'reference': '0123456789012345678901234'},
      'ref': 'disc-sbx-001'
  })['data']

  # 2. Wait for the bills
  ready = wait_for(started['transactionId'], lambda t: t['status'] == 'READY')
  print(ready['bills'])  # 1 bill @ 443.39 DZD

  # 3. Pay the first one, with a NEW ref
  call('POST', '/v3/bills/pay', {
      'transactionId': started['transactionId'],
      'billId': ready['bills'][0]['billId'],
      'ref': 'pay-sbx-001'
  })

  # 4. Wait for a final state
  final = wait_for(
      started['transactionId'],
      lambda t: t['status'] in ('SUCCESS', 'FAILED', 'REFUNDED')
  )
  print(final['status'], final.get('operationId'))

  # 5. Download the receipt
  receipt = requests.get(
      f"{BASE}/v3/bills/transactions/{started['transactionId']}/receipt",
      headers={'X-Access-Token': KEY}
  )
  with open('receipt-sbx.bin', 'wb') as handle:
      handle.write(receipt.content)
  ```

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

  function call(string $method, string $path, ?array $payload = null): array
  {
      $ch = curl_init(BASE . $path);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
      curl_setopt($ch, CURLOPT_HTTPHEADER, [
          'Content-Type: application/json',
          'X-Access-Token: ' . getenv('ONECLICKDZ_SANDBOX_API_KEY')
      ]);
      if ($payload !== null) {
          curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
      }

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

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

      return $body;
  }

  function waitFor(string $transactionId, callable $predicate, int $timeoutSeconds = 120): array
  {
      $deadline = time() + $timeoutSeconds;
      while (time() < $deadline) {
          $data = call('GET', "/v3/bills/transactions/$transactionId")['data'];
          if ($predicate($data)) {
              return $data;
          }
          sleep(2);
      }
      throw new Exception('Timed out');
  }

  // 0. Confirm the environment
  $identity = call('GET', '/v3/validate')['data'];
  if ($identity['key']['type'] !== 'SANDBOX') {
      throw new Exception('Not a sandbox key');
  }

  // 1. Discover
  $started = call('POST', '/v3/bills/discover', [
      'partner' => 'ADE',
      'account' => ['reference' => '0123456789012345678901234'],
      'ref'     => 'disc-sbx-001'
  ])['data'];

  // 2. Wait for the bills
  $ready = waitFor($started['transactionId'], fn($t) => $t['status'] === 'READY');
  print_r($ready['bills']); // 1 bill @ 443.39 DZD

  // 3. Pay the first one, with a NEW ref
  call('POST', '/v3/bills/pay', [
      'transactionId' => $started['transactionId'],
      'billId'        => $ready['bills'][0]['billId'],
      'ref'           => 'pay-sbx-001'
  ]);

  // 4. Wait for a final state
  $final = waitFor(
      $started['transactionId'],
      fn($t) => in_array($t['status'], ['SUCCESS', 'FAILED', 'REFUNDED'], true)
  );
  echo $final['status'] . ' ' . ($final['operationId'] ?? '');

  // 5. Download the receipt
  $ch = curl_init(BASE . "/v3/bills/transactions/{$started['transactionId']}/receipt");
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'X-Access-Token: ' . getenv('ONECLICKDZ_SANDBOX_API_KEY')
  ]);
  file_put_contents('receipt-sbx.bin', curl_exec($ch));
  curl_close($ch);
  ?>
  ```
</CodeGroup>

<Note>
  Utilisez un `ref` neuf à chaque exécution, sinon la deuxième exécution répond `403 DUPLICATED_REF`. Suffixer le ref avec votre propre compteur d'exécutions de test est l'approche la plus simple.
</Note>

## Validation d'intégration recommandée

Trois vérifications qui prouvent le contrat avant d'aller plus loin :

<Steps>
  <Step title="Vérifier la forme de /v3/validate">
    `account.id`, `account.status`, `account.currency` et `key.type` sont tous présents, et `key.type` vaut `SANDBOX`.
  </Step>

  <Step title="Confirmer la table /v3/partners">
    Cinq clés, chacune avec un `status` valant `ACTIVE` ou `UNAVAILABLE`, y compris `Algérie Télécom` avec ses accents.
  </Step>

  <Step title="Exécuter un aller-retour complet">
    Découverte, paiement et recherche par `ref` — prouvant que votre `ref` renvoie bien à la transaction que vous avez créée.
  </Step>
</Steps>

## Scénarios qui méritent d'être automatisés

Au-delà du cas nominal, ces quatre-là sont ceux qui débusquent de vrais bugs :

| Test           | Identifiant                                        | Ce qu'il prouve                                                                          |
| -------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Factures vides | `ADE` `reference: "0123456789012340000000002"`     | Vous dites « rien à payer », pas « rien dû », et vous ne plantez pas sur un tableau vide |
| Refus          | `ADE` `reference: "0123456789012340000000004"`     | Vous libérez les fonds du client sur `FAILED` et ne le débitez jamais                    |
| Remboursement  | `ADE` `reference: "0123456789012340000000005"`     | Vous gérez de l'argent qui est parti et revenu                                           |
| Examen         | `SONELGAZ` `sonelgaz.invoice_number: "6006006006"` | Vous bloquez les fonds sur `UNKNOWN` pendant une minute sans rembourser ni renvoyer      |

<Warning>
  Le scénario d'examen est le test le plus important de ce tableau. C'est le seul moyen économique de prouver que votre code ne rembourse pas un client dont la facture a réellement été payée.
</Warning>

## Passage en production

<Steps>
  <Step title="Changer la clé, ne rien changer d'autre">
    Même URL de base, même en-tête, mêmes routes. Seule la valeur de la clé change.
  </Step>

  <Step title="Vérifier l'environnement au démarrage">
    Appelez `/v3/validate` et faites échouer votre séquence de démarrage si `key.type` n'est pas celui que ce déploiement attend.

    → [Valider la clé API](/fr/api-reference/bill-payment/validate-key)
  </Step>

  <Step title="Relire fee et total dans la réponse">
    Le sandbox renvoie `fee: 0`. Pas la production. Si quoi que ce soit dans votre code supposait des frais nuls, cela casse ici.
  </Step>

  <Step title="Confirmer que votre interrogation gère UNKNOWN">
    En production, cet état est rare et coûteux à mal gérer. Prouvez que la branche existe avant d'en avoir besoin.
  </Step>

  <Step title="Vérifier que votre tâche de rapprochement s'exécute">
    Elle aurait déjà dû tourner face au sandbox, sans rien trouver. Le premier jour en production, c'est votre filet de sécurité.

    → [Reçus et rapprochement](/fr/bill-payment-guides/5-receipts-and-reconciliation)
  </Step>

  <Step title="Conserver la clé sandbox">
    Chaque changement futur est testé contre ces scénarios avant d'atteindre la production.
  </Step>
</Steps>

## Liste de vérification avant mise en production

| Vérification                                                                                      | Pourquoi                                                                          |
| ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `/v3/validate` renvoie `PRODUCTION` dans le déploiement de production                             | Prouve que vous avez livré la bonne clé                                           |
| Les clés vivent dans un coffre à secrets, jamais dans le contrôle de version ni dans les journaux | Une clé de production divulguée déplace de l'argent réel                          |
| Chaque `ref` dérive de votre propre identifiant de commande                                       | La reprise après un délai dépassé en dépend                                       |
| La découverte et le paiement utilisent des valeurs de `ref` différentes                           | En réutiliser une répond `403 DUPLICATED_REF`                                     |
| L'enregistrement de votre commande est écrit avant l'envoi du paiement                            | C'est le seul moyen de retrouver un paiement perdu                                |
| `UNKNOWN` bloque les fonds et ne rembourse jamais                                                 | L'erreur la plus coûteuse à commettre                                             |
| `403` et `409` sont gérés par une recherche, jamais par une nouvelle tentative                    | Réessayer face à un garde-fou, c'est ainsi que les clients sont débités deux fois |
| `fee` et `total` proviennent de la réponse                                                        | Les frais relèvent de la configuration et peuvent changer                         |
| Les reçus sont téléchargés et stockés en cas de succès                                            | Les litiges arrivent des mois plus tard                                           |
| Le rapprochement quotidien s'exécute et alerte                                                    | Il rattrape tout ce que l'interrogation a manqué                                  |

## Bonnes pratiques

<CardGroup cols={2}>
  <Card title="Automatiser les quatre scénarios difficiles" icon="list-check">
    Factures vides, refus, remboursement et examen. Ils sont déterministes, donc ils ont leur place dans votre suite de tests.
  </Card>

  <Card title="Ne jamais présumer de la disponibilité en sandbox" icon="triangle-exclamation">
    La table des partenaires du sandbox est fixe. La disponibilité en production est réelle et change.
  </Card>

  <Card title="Varier le ref à chaque exécution" icon="fingerprint">
    Sinon, la deuxième exécution de votre suite de tests échoue sur `DUPLICATED_REF`.
  </Card>

  <Card title="Continuer à tester après la mise en production" icon="flask">
    Le sandbox ne coûte rien. Exécutez la suite à chaque version.
  </Card>
</CardGroup>

## Pages associées

<CardGroup cols={2}>
  <Card title="Valider la clé API" icon="key" href="/fr/api-reference/bill-payment/validate-key">
    Prouver à quel environnement appartient une clé
  </Card>

  <Card title="Vue d'ensemble du paiement de factures" icon="file-invoice-dollar" href="/fr/bill-payment-guides/overview">
    La carte en cinq étapes
  </Card>

  <Card title="Interrogation du statut" icon="arrows-rotate" href="/fr/bill-payment-guides/4-status-polling">
    Gérer `UNKNOWN` et `REFUNDED`
  </Card>

  <Card title="Gestion des erreurs" icon="triangle-exclamation" href="/fr/api-reference/error-handling">
    Tous les codes d'erreur au même endroit
  </Card>
</CardGroup>
