> ## Documentation Index
> Fetch the complete documentation index at: https://docs.umeme.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Vendre de l'électricité SOCODE

> On part d'une vente codée en dur, puis on la rend dynamique et sûre : rejets évités, échecs gérés, aucune vente perdue ni vendue deux fois.

> [!NOTE]
>
> Tout se joue dans la sandbox, sur `sandbox.umeme.app`, avec une clé `umeme_test_…`. Pas encore de clé ? Commencez par [Démarrer](https://docs.umeme.app/demarrer.md).

## 1. Une première vente, tout en dur

Commençons par tout coder en dur.

**1. Envoyer la vente**

Envoyez `POST /v1/ventes` avec le compteur de test `09900000000`, qui réussit toujours.

```json POST https://sandbox.umeme.app/v1/ventes {2} theme={"dark"}
{
  "uuid": "94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb",
  "compte": "SOCODE",
  "reference": "09900000000",
  "montant": 5
}
```

L'`uuid` vient de vous : générez un UUID v4 et **enregistrez-le avant l'appel**. Si la réponse se perd, c'est lui qui vous permet de retrouver la vente, sans jamais la vendre deux fois. Le montant est en dollars.

Le même appel, dans votre langage :

```bash cURL icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/bash.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=c7cc496df6d82b1c6cdaa5c132b8d4bc" theme={"dark"}
curl -X POST "$UMEME_BASE_URL/ventes" \
  -H "Authorization: Bearer $UMEME_CLE" \
  -H "Content-Type: application/json" \
  -d '{
    "uuid": "94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb",
    "compte": "SOCODE",
    "reference": "09900000000",
    "montant": 5
  }'
```

```javascript Node.js · fetch icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/nodejs.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=60e7eb54ba25d5cb5e9d62e0896b99c7" theme={"dark"}
// Node.js 18 et plus : fetch est intégré.
const baseUrl = process.env.UMEME_BASE_URL;

const corps = {
  uuid: "94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb",
  compte: "SOCODE",
  reference: "09900000000",
  montant: 5,
};

const reponse = await fetch(`${baseUrl}/ventes`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.UMEME_CLE}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify(corps),
});
const donnees = await reponse.json();
```

```javascript Node.js · Axios icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/axios.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=7e8bd7e79f903d3d1bffc290e52fcdc9" theme={"dark"}
// npm install axios
import axios from "axios";

const umeme = axios.create({
  baseURL: process.env.UMEME_BASE_URL,
  headers: { Authorization: `Bearer ${process.env.UMEME_CLE}` },
  timeout: 30000,
});

const corps = {
  uuid: "94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb",
  compte: "SOCODE",
  reference: "09900000000",
  montant: 5,
};

const { data } = await umeme.post("/ventes", corps);
```

```python Python icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/python.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=41e570f5c5c97d07c3c878a6704f2bed" theme={"dark"}
import os

import requests

BASE_URL = os.environ["UMEME_BASE_URL"]
ENTETES = {"Authorization": f"Bearer {os.environ['UMEME_CLE']}"}

corps = {
    "uuid": "94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb",
    "compte": "SOCODE",
    "reference": "09900000000",
    "montant": 5,
}

reponse = requests.post(
    f"{BASE_URL}/ventes",
    headers=ENTETES,
    json=corps,
    timeout=30,
)
donnees = reponse.json()
```

```php PHP icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/php.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=706823b5881c1d4e78720e369520cc78" theme={"dark"}
// composer require guzzlehttp/guzzle
$client = new GuzzleHttp\Client([
    'base_uri' => getenv('UMEME_BASE_URL') . '/',
    'headers' => ['Authorization' => 'Bearer ' . getenv('UMEME_CLE')],
    'timeout' => 30,
]);

$corps = [
    'uuid' => '94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb',
    'compte' => 'SOCODE',
    'reference' => '09900000000',
    'montant' => 5,
];

$reponse = $client->post('ventes', [
    'json' => $corps,
]);
$donnees = json_decode($reponse->getBody(), true);
```

```java Java icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/java.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=a99792afd2b2bcd5b3c64f5157b8469e" theme={"dark"}
// Java 15 et plus : java.net.http et blocs de texte.
var baseUrl = System.getenv("UMEME_BASE_URL");
var client = HttpClient.newHttpClient();

var corps = """
    {
      "uuid": "94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb",
      "compte": "SOCODE",
      "reference": "09900000000",
      "montant": 5
    }
    """;

var requete = HttpRequest.newBuilder(URI.create(baseUrl + "/ventes"))
    .header("Authorization", "Bearer " + System.getenv("UMEME_CLE"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(corps))
    .build();

var reponse = client.send(requete, HttpResponse.BodyHandlers.ofString());
```

La réponse arrive tout de suite :

```json 200 OK {3} theme={"dark"}
{
  "uuid": "94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb",
  "statut": "ACCEPTEE",
  "creeLe": "2026-11-07T08:26:44Z"
}
```

`ACCEPTEE` veut dire que la vente est prise et le portefeuille débité, pas encore qu'elle a réussi.

**2. Umeme vend auprès du fournisseur**

Rien à faire pendant ce temps. Dans la sandbox, le compteur `09900000000` réussit en 3 s ; en production, une vente SOCODE prend environ 4 s.

**3. Recevoir le statut final**

Umeme envoie un `POST` signé à votre URL de rappel. Son corps est celui de `GET /v1/ventes/{uuid}` :

```json POST https://votre-serveur.example/rappels/umeme {6,7} theme={"dark"}
{
  "uuid": "94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb",
  "compte": "SOCODE",
  "reference": "09900000000",
  "montant": 5,
  "statut": "REUSSIE",
  "jeton": "47035727296518417129",
  "recu": "USD/1793782394028",
  "urlRecu": "https://recus.umeme.app/soc-94a223fe-fb34-47f2-aaa5-b80c4bf8a8eb.pdf",
  "creeLe": "2026-11-07T08:26:44Z",
  "termineLe": "2026-11-07T08:26:47Z"
}
```

Remettez le jeton à votre client, avec son reçu. Avant de croire un rappel, vérifiez sa signature : c'est l'objet du guide [Recevoir et vérifier les rappels](https://docs.umeme.app/guides/rappels.md).

Pas de rappel ? `GET /v1/ventes/{uuid}` donne la même réponse, à tout moment.

**4. Et voilà !**

Vous avez vendu de l'électricité SOCODE et reçu le jeton. Rendons maintenant la vente dynamique.

## 2. Demander les informations au client

Votre client choisit le compte, saisit son compteur et le montant. Voici comment construire cet écran.

**1. Afficher les comptes que vous pouvez vendre**

`GET /v1/configuration` donne les comptes autorisés, les montants minimum et maximum, et ce qui reste de votre limite du jour. `GET /v1/services` dit lesquels sont ouverts en ce moment.

```json GET /v1/configuration {2,6} theme={"dark"}
{
  "comptes": ["SOCODE", "VIRUNGA", "TV"],
  "montantMin": 0.5,
  "montantMax": 5000,
  "limiteJour": 500,
  "resteJour": 435
}
```

**2. Demander le compteur et le montant**

Un écran simple suffit :

Ce que voit votre clientSOCODEVIRUNGATVNuméro du compteur09900000000 **CLIENT SANDBOX 0000**Montant5,00 \$ **minimum 1,00 \$ pour ce compteur**

Payer 5,00 \$

Montants en dollars, deux décimales au plus. Ne présélectionnez aucun compte : c'est votre client qui sait ce qu'il paie.

**3. Vérifier le compteur**

`GET /v1/compteurs/{reference}?compte=SOCODE&montant=5` renvoie le nom du client et le minimum du fournisseur. Affichez le nom : votre client confirme qu'il paie le bon compteur.

```json GET /v1/compteurs/09900000000?compte=SOCODE&montant=5 {3} theme={"dark"}
{
  "reference": "09900000000",
  "client": "CLIENT SANDBOX 0000",
  "minimum": 1
}
```

**4. Créer la vente**

Générez un `uuid` neuf, enregistrez-le avec la commande, puis envoyez la vente comme à la section 1.

**5. Et voilà !**

Votre écran de vente est dynamique. Voyons comment éviter qu'une vente soit rejetée.

## 3. Éviter les rejets

Un rejet arrive à la soumission : HTTP `200`, statut `REJETEE` et un code dans `motifRejet.code`. Rien n'est débité. La plupart se préviennent avant l'appel.

**1. Le compte est fermé**

`SERVICE_INDISPONIBLE`. Relisez `GET /v1/services` avant d'afficher l'écran, et masquez un compte fermé.

**2. Le montant sort des limites**

`MONTANT_HORS_LIMITES`, `MONTANT_SOUS_MINIMUM`. Bornez la saisie avec la configuration et le minimum du compteur.

**3. Le compteur n'est pas valable**

`REFERENCE_INVALIDE`, `COMPTEUR_INCONNU`, `COMPTEUR_BLOQUE`. Vérifiez-le avant de vendre (section 2, étape 3).

**4. Le portefeuille ou la limite du jour ne suffit pas**

`SOLDE_INSUFFISANT`, `LIMITE_JOUR_ATTEINTE`. Surveillez `GET /v1/portefeuille` et rechargez avant d'arriver à zéro.

**5. La même vente part deux fois**

`DOUBLON_IGNORE` n'est pas un rejet : la vente existe déjà, ne refaites rien. Un `uuid` déjà utilisé pour une autre vente reçoit une erreur `409 UUID_REUTILISE` : un `uuid` par vente.

**6. Pas de réponse, ou une erreur 5xx**

Ne concluez pas : consultez `GET /v1/ventes/{uuid}`. Seul `NON_TROUVEE` prouve que la vente n'existe pas ; renvoyez-la alors, avec le même `uuid`.

**7. Et voilà !**

Vos ventes ne sont plus rejetées pour des raisons évitables. Passons aux échecs.

> [!TIP]
>
> `motifRejet.message` est écrit pour vos équipes et peut changer. Montrez à votre client un message tiré du code.

## 4. Gérer les échecs

Une vente `ACCEPTEE` peut échouer chez le fournisseur. Elle passe alors `ECHOUEE`, le portefeuille est déjà remboursé, et il reste à prévenir votre client.

| `motifEchec.code` | Ce qui s'est passé | Ce que vous dites au client |
| :- | :- | :- |
| `REFUS_COMPTEUR_FOURNISSEUR` | Le fournisseur refuse ce compteur | « Ce compteur ne peut pas être rechargé. Vérifiez le numéro. » |
| `MINIMUM_FOURNISSEUR` | Le montant est sous le minimum du fournisseur | « Le montant minimum pour ce compteur est plus élevé. » |
| `ECHEC_FOURNISSEUR` | Le fournisseur n'a pas pu vendre | « La vente n'a pas abouti. Réessayez plus tard. » |
| `ECHEC_APRES_VERIFICATION` | La vérification prouve que la vente n'a pas eu lieu | « La vente n'a pas abouti ; votre argent vous est rendu. » |

> [!NOTE]
>
> Un échec est définitif. Une nouvelle tentative est une nouvelle vente, avec un nouvel `uuid`.

## 5. Rester cohérent

Le réseau coupe, un serveur redémarre. Trois règles empêchent qu'une vente se perde, ou qu'elle soit vendue deux fois.

**1. Traiter chaque statut, même inattendu**

```text Pseudo-code theme={"dark"}
selon vente.statut
  REUSSIE            → remettre le jeton et le reçu
  ECHOUEE            → prévenir le client (motifEchec.code)
  EN_COURS           → attendre le rappel
  EN_RECONCILIATION  → prévenir le client, ne pas relancer
  autre              → à examiner par vos équipes
```

```javascript Node.js icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/nodejs.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=60e7eb54ba25d5cb5e9d62e0896b99c7" theme={"dark"}
// vente : le corps d'un rappel, ou la réponse de GET /v1/ventes/{uuid}.
export function traiter(vente, actions) {
  switch (vente.statut) {
    case "REUSSIE":
      return actions.remettreJetonEtRecu(vente.jeton, vente.urlRecu);
    case "ECHOUEE":
      return actions.prevenirClient(vente.motifEchec.code);
    case "EN_COURS":
      // Le rappel arrivera : rien à faire.
      return;
    case "EN_RECONCILIATION":
      // Umeme vérifie : prévenir le client, ne jamais relancer la vente.
      return actions.prevenirAttente(vente.uuid);
    default:
      // Un statut que ce code ne connaît pas : à examiner, sans rien conclure.
      return actions.signalerAuxEquipes(vente);
  }
}
```

```python Python icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/python.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=41e570f5c5c97d07c3c878a6704f2bed" theme={"dark"}
# vente : le corps d'un rappel, ou la réponse de GET /v1/ventes/{uuid}.
def traiter(vente, actions):
    statut = vente["statut"]
    if statut == "REUSSIE":
        return actions.remettre_jeton_et_recu(vente.get("jeton"), vente["urlRecu"])
    if statut == "ECHOUEE":
        return actions.prevenir_client(vente["motifEchec"]["code"])
    if statut == "EN_COURS":
        # Le rappel arrivera : rien à faire.
        return None
    if statut == "EN_RECONCILIATION":
        # Umeme vérifie : prévenir le client, ne jamais relancer la vente.
        return actions.prevenir_attente(vente["uuid"])
    # Un statut que ce code ne connaît pas : à examiner, sans rien conclure.
    return actions.signaler_aux_equipes(vente)
```

```php PHP icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/php.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=706823b5881c1d4e78720e369520cc78" theme={"dark"}
<?php

// $vente : le corps d'un rappel, ou la réponse de GET /v1/ventes/{uuid}.
function traiter(array $vente, Actions $actions): void
{
    match ($vente['statut']) {
        'REUSSIE' => $actions->remettreJetonEtRecu($vente['jeton'] ?? null, $vente['urlRecu']),
        'ECHOUEE' => $actions->prevenirClient($vente['motifEchec']['code']),
        // Le rappel arrivera : rien à faire.
        'EN_COURS' => null,
        // Umeme vérifie : prévenir le client, ne jamais relancer la vente.
        'EN_RECONCILIATION' => $actions->prevenirAttente($vente['uuid']),
        // Un statut que ce code ne connaît pas : à examiner, sans rien conclure.
        default => $actions->signalerAuxEquipes($vente),
    };
}
```

```java Java icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/java.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=a99792afd2b2bcd5b3c64f5157b8469e" theme={"dark"}
// vente : le corps d'un rappel, ou la réponse de GET /v1/ventes/{uuid}.
static void traiter(Vente vente, Actions actions) {
    switch (vente.statut()) {
        case "REUSSIE" -> actions.remettreJetonEtRecu(vente.jeton(), vente.urlRecu());
        case "ECHOUEE" -> actions.prevenirClient(vente.motifEchec().code());
        // Le rappel arrivera : rien à faire.
        case "EN_COURS" -> { }
        // Umeme vérifie : prévenir le client, ne jamais relancer la vente.
        case "EN_RECONCILIATION" -> actions.prevenirAttente(vente.uuid());
        // Un statut que ce code ne connaît pas : à examiner, sans rien conclure.
        default -> actions.signalerAuxEquipes(vente);
    }
}
```

**2. Enregistrer l'uuid avant l'appel**

```text Pseudo-code theme={"dark"}
uuid ← nouvel UUID v4
enregistrer(commande, uuid, statut = A_ENVOYER)
essayer
  reponse ← POST /v1/ventes
sinon
  // délai dépassé ou erreur 5xx
  vente ← GET /v1/ventes/{uuid}
  si NON_TROUVEE → renvoyer la même vente, même uuid
  sinon → suivre vente.statut
```

```javascript Node.js · fetch icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/nodejs.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=60e7eb54ba25d5cb5e9d62e0896b99c7" theme={"dark"}
import { randomUUID } from "node:crypto";

const BASE_URL = process.env.UMEME_BASE_URL;
const entetes = {
  Authorization: `Bearer ${process.env.UMEME_CLE}`,
  "Content-Type": "application/json",
};

// commandes : votre stockage, où l'uuid est écrit avant tout appel.
export async function vendre(commande, commandes) {
  const vente = {
    uuid: randomUUID(),
    compte: commande.compte,
    reference: commande.reference,
    montant: commande.montant,
  };
  await commandes.enregistrer(commande.id, { uuid: vente.uuid, statut: "A_ENVOYER" });

  try {
    const reponse = await fetch(`${BASE_URL}/ventes`, {
      method: "POST",
      headers: entetes,
      body: JSON.stringify(vente),
      signal: AbortSignal.timeout(30000),
    });
    if (reponse.status < 500) {
      return await reponse.json();
    }
  } catch {
    // Délai dépassé ou réseau coupé : rien n'est conclu, la vente est cherchée.
  }
  return retrouver(vente);
}

async function retrouver(vente) {
  const reponse = await fetch(`${BASE_URL}/ventes/${vente.uuid}`, { headers: entetes });
  if (reponse.status === 404) {
    // NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
    const renvoi = await fetch(`${BASE_URL}/ventes`, {
      method: "POST",
      headers: entetes,
      body: JSON.stringify(vente),
    });
    return renvoi.json();
  }
  return reponse.json();
}
```

```javascript Node.js · Axios icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/axios.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=7e8bd7e79f903d3d1bffc290e52fcdc9" theme={"dark"}
import { randomUUID } from "node:crypto";
import axios from "axios";

const umeme = axios.create({
  baseURL: process.env.UMEME_BASE_URL,
  headers: { Authorization: `Bearer ${process.env.UMEME_CLE}` },
  timeout: 30000,
  // Les réponses 4xx sont des réponses : seul un 5xx ou un délai fait douter.
  validateStatus: (statut) => statut < 500,
});

// commandes : votre stockage, où l'uuid est écrit avant tout appel.
export async function vendre(commande, commandes) {
  const vente = {
    uuid: randomUUID(),
    compte: commande.compte,
    reference: commande.reference,
    montant: commande.montant,
  };
  await commandes.enregistrer(commande.id, { uuid: vente.uuid, statut: "A_ENVOYER" });

  try {
    const { data } = await umeme.post("/ventes", vente);
    return data;
  } catch {
    // Délai dépassé, réseau coupé ou erreur 5xx : rien n'est conclu, la vente est cherchée.
    return retrouver(vente);
  }
}

async function retrouver(vente) {
  const { status, data } = await umeme.get(`/ventes/${vente.uuid}`);
  if (status === 404) {
    // NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
    return (await umeme.post("/ventes", vente)).data;
  }
  return data;
}
```

```python Python icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/python.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=41e570f5c5c97d07c3c878a6704f2bed" theme={"dark"}
import os
import uuid

import requests

BASE_URL = os.environ["UMEME_BASE_URL"]
ENTETES = {"Authorization": f"Bearer {os.environ['UMEME_CLE']}"}


# commandes : votre stockage, où l'uuid est écrit avant tout appel.
def vendre(commande, commandes):
    vente = {
        "uuid": str(uuid.uuid4()),
        "compte": commande["compte"],
        "reference": commande["reference"],
        "montant": commande["montant"],
    }
    commandes.enregistrer(commande["id"], uuid=vente["uuid"], statut="A_ENVOYER")

    try:
        reponse = requests.post(f"{BASE_URL}/ventes", headers=ENTETES, json=vente, timeout=30)
        if reponse.status_code < 500:
            return reponse.json()
    except requests.RequestException:
        # Délai dépassé ou réseau coupé : rien n'est conclu, la vente est cherchée.
        pass
    return retrouver(vente)


def retrouver(vente):
    reponse = requests.get(f"{BASE_URL}/ventes/{vente['uuid']}", headers=ENTETES, timeout=30)
    if reponse.status_code == 404:
        # NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
        return requests.post(f"{BASE_URL}/ventes", headers=ENTETES, json=vente, timeout=30).json()
    return reponse.json()
```

```php PHP icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/php.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=706823b5881c1d4e78720e369520cc78" theme={"dark"}
<?php

use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
use Ramsey\Uuid\Uuid;

function umeme(): Client
{
    return new Client([
        'base_uri' => getenv('UMEME_BASE_URL') . '/',
        'headers' => ['Authorization' => 'Bearer ' . getenv('UMEME_CLE')],
        'timeout' => 30,
        // Les réponses 4xx sont des réponses : seul un 5xx ou un délai fait douter.
        'http_errors' => false,
    ]);
}

// $commandes : votre stockage, où l'uuid est écrit avant tout appel.
function vendre(array $commande, Commandes $commandes): array
{
    $vente = [
        'uuid' => Uuid::uuid4()->toString(),
        'compte' => $commande['compte'],
        'reference' => $commande['reference'],
        'montant' => $commande['montant'],
    ];
    $commandes->enregistrer($commande['id'], $vente['uuid'], 'A_ENVOYER');

    try {
        $reponse = umeme()->post('ventes', ['json' => $vente]);
        if ($reponse->getStatusCode() < 500) {
            return json_decode($reponse->getBody(), true);
        }
    } catch (TransferException $e) {
        // Délai dépassé ou réseau coupé : rien n'est conclu, la vente est cherchée.
    }
    return retrouver($vente);
}

function retrouver(array $vente): array
{
    $reponse = umeme()->get('ventes/' . $vente['uuid']);
    if ($reponse->getStatusCode() === 404) {
        // NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
        $reponse = umeme()->post('ventes', ['json' => $vente]);
    }
    return json_decode($reponse->getBody(), true);
}
```

```java Java icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/java.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=a99792afd2b2bcd5b3c64f5157b8469e" theme={"dark"}
static final String BASE_URL = os.environ["UMEME_BASE_URL"];
static final HttpClient CLIENT = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(10))
    .build();

// commandes : votre stockage, où l'uuid est écrit avant tout appel.
static String vendre(Commande commande, Commandes commandes) throws Exception {
    var uuid = UUID.randomUUID().toString();
    commandes.enregistrer(commande.id(), uuid, "A_ENVOYER");
    var vente = """
        {"uuid": "%s", "compte": "%s", "reference": "%s", "montant": %s}
        """.formatted(uuid, commande.compte(), commande.reference(), commande.montant());

    try {
        var reponse = CLIENT.send(requete("/ventes", vente), HttpResponse.BodyHandlers.ofString());
        if (reponse.statusCode() < 500) {
            return reponse.body();
        }
    } catch (IOException e) {
        // Délai dépassé ou réseau coupé : rien n'est conclu, la vente est cherchée.
    }
    return retrouver(uuid, vente);
}

static String retrouver(String uuid, String vente) throws Exception {
    var reponse = CLIENT.send(requete("/ventes/" + uuid, null), HttpResponse.BodyHandlers.ofString());
    if (reponse.statusCode() == 404) {
        // NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
        reponse = CLIENT.send(requete("/ventes", vente), HttpResponse.BodyHandlers.ofString());
    }
    return reponse.body();
}

static HttpRequest requete(String chemin, String corps) {
    var requete = HttpRequest.newBuilder(URI.create(BASE_URL + chemin))
        .header("Authorization", "Bearer " + System.getenv("UMEME_CLE"))
        .timeout(Duration.ofSeconds(30));
    if (corps == null) {
        return requete.GET().build();
    }
    return requete.header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(corps))
        .build();
}
```

**3. Reprendre les ventes en attente**

```text Pseudo-code theme={"dark"}
toutes les 5 minutes
  pour chaque vente EN_COURS depuis plus de 15 minutes
    vente ← GET /v1/ventes/{uuid}
    si NON_TROUVEE → renvoyer la même vente, même uuid
    sinon → traiter(vente.statut)
```

```javascript Node.js · fetch icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/nodejs.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=60e7eb54ba25d5cb5e9d62e0896b99c7" theme={"dark"}
const BASE_URL = process.env.UMEME_BASE_URL;
const entetes = { Authorization: `Bearer ${process.env.UMEME_CLE}` };

// commandes : votre stockage ; traiter : la fonction qui applique un statut.
export async function reprendre(commandes, traiter) {
  const enAttente = await commandes.enCoursDepuis(15 * 60 * 1000);
  for (const commande of enAttente) {
    const reponse = await fetch(`${BASE_URL}/ventes/${commande.uuid}`, { headers: entetes });
    if (reponse.status === 404) {
      // NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
      await commandes.renvoyer(commande);
      continue;
    }
    traiter(await reponse.json());
  }
}

// Toutes les 5 minutes.
export function planifier(commandes, traiter) {
  return setInterval(() => reprendre(commandes, traiter), 5 * 60 * 1000);
}
```

```javascript Node.js · Axios icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/axios.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=7e8bd7e79f903d3d1bffc290e52fcdc9" theme={"dark"}
import axios from "axios";

const umeme = axios.create({
  baseURL: process.env.UMEME_BASE_URL,
  headers: { Authorization: `Bearer ${process.env.UMEME_CLE}` },
  timeout: 30000,
  validateStatus: (statut) => statut < 500,
});

// commandes : votre stockage ; traiter : la fonction qui applique un statut.
export async function reprendre(commandes, traiter) {
  const enAttente = await commandes.enCoursDepuis(15 * 60 * 1000);
  for (const commande of enAttente) {
    const { status, data } = await umeme.get(`/ventes/${commande.uuid}`);
    if (status === 404) {
      // NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
      await commandes.renvoyer(commande);
      continue;
    }
    traiter(data);
  }
}

// Toutes les 5 minutes.
export function planifier(commandes, traiter) {
  return setInterval(() => reprendre(commandes, traiter), 5 * 60 * 1000);
}
```

```python Python icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/python.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=41e570f5c5c97d07c3c878a6704f2bed" theme={"dark"}
import os
import time

import requests

BASE_URL = os.environ["UMEME_BASE_URL"]
ENTETES = {"Authorization": f"Bearer {os.environ['UMEME_CLE']}"}


# commandes : votre stockage ; traiter : la fonction qui applique un statut.
def reprendre(commandes, traiter):
    for commande in commandes.en_cours_depuis(minutes=15):
        reponse = requests.get(f"{BASE_URL}/ventes/{commande['uuid']}", headers=ENTETES, timeout=30)
        if reponse.status_code == 404:
            # NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
            commandes.renvoyer(commande)
            continue
        traiter(reponse.json())


# Toutes les 5 minutes.
def planifier(commandes, traiter):
    while True:
        reprendre(commandes, traiter)
        time.sleep(5 * 60)
```

```php PHP icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/php.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=706823b5881c1d4e78720e369520cc78" theme={"dark"}
<?php

use GuzzleHttp\Client;

// $commandes : votre stockage ; $traiter : la fonction qui applique un statut.
// À lancer toutes les 5 minutes, par exemple depuis le planificateur de Laravel ou cron.
function reprendre(Commandes $commandes, callable $traiter): void
{
    $umeme = new Client([
        'base_uri' => getenv('UMEME_BASE_URL') . '/',
        'headers' => ['Authorization' => 'Bearer ' . getenv('UMEME_CLE')],
        'timeout' => 30,
        'http_errors' => false,
    ]);
    foreach ($commandes->enCoursDepuis(15) as $commande) {
        $reponse = $umeme->get('ventes/' . $commande['uuid']);
        if ($reponse->getStatusCode() === 404) {
            // NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
            $commandes->renvoyer($commande);
            continue;
        }
        $traiter(json_decode($reponse->getBody(), true));
    }
}
```

```java Java icon="https://mintcdn.com/dommi10/MMGXqk_5twdsE4Rl/logos/langages/java.svg?fit=max&auto=format&n=MMGXqk_5twdsE4Rl&q=85&s=a99792afd2b2bcd5b3c64f5157b8469e" theme={"dark"}
static final HttpClient CLIENT = HttpClient.newHttpClient();

// commandes : votre stockage ; traiter : la fonction qui applique un statut.
static void reprendre(Commandes commandes, Consumer<String> traiter) throws Exception {
    for (var commande : commandes.enCoursDepuis(Duration.ofMinutes(15))) {
        var requete = HttpRequest.newBuilder(URI.create(System.getenv("UMEME_BASE_URL") + "/ventes/" + commande.uuid()))
            .header("Authorization", "Bearer " + System.getenv("UMEME_CLE"))
            .timeout(Duration.ofSeconds(30))
            .GET()
            .build();
        var reponse = CLIENT.send(requete, HttpResponse.BodyHandlers.ofString());
        if (reponse.statusCode() == 404) {
            // NON_TROUVEE : la vente n'est jamais arrivée, elle repart avec le même uuid.
            commandes.renvoyer(commande);
            continue;
        }
        traiter.accept(reponse.body());
    }
}

// Toutes les 5 minutes.
static void planifier(Commandes commandes, Consumer<String> traiter) {
    Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
        try {
            reprendre(commandes, traiter);
        } catch (Exception e) {
            // Le prochain passage reprendra les mêmes ventes.
        }
    }, 0, 5, TimeUnit.MINUTES);
}
```

**4. Et voilà !**

Aucune vente ne se perd, aucune n'est vendue deux fois.

## 6. Ventes en réconciliation

Quand Umeme n'a pas pu confirmer une vente auprès du fournisseur, elle passe `EN_RECONCILIATION`. Rien à faire : Umeme vérifie, automatiquement puis à la main, et le rappel part dès le résultat.

> [!WARNING]
>
> Ne relancez pas la vente : ce serait la vendre deux fois. Prévenez votre client que le résultat arrive ; cela peut prendre plusieurs heures.

Pour vous entraîner : `09900000020` est retrouvée et réussit, `09900000021` passe en revue manuelle et échoue. Voir les [compteurs de test](https://docs.umeme.app/ressources/compteurs-de-test.md).

## 7. Et ensuite ?

> [card] [Compteurs de test](https://docs.umeme.app/ressources/compteurs-de-test.md)
>
> Jouer chaque issue : échec, réconciliation, revue manuelle.

> [card] [Recevoir et vérifier les rappels](https://docs.umeme.app/guides/rappels.md)
>
> La signature par clé publique, étape par étape.

> [card] [Codes d'erreur](https://docs.umeme.app/ressources/codes-erreur.md)
>
> Tous les codes de rejet et d'échec, et leur sens.

> [card] [Mise en production](https://docs.umeme.app/mise-en-production.md)
>
> La liste de contrôle avant de passer aux clés `umeme_live_`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.