# Limites et quotas

import { ApiExample, ApiResponse } from "../../src/ApiExample";

## Limitation de débit

Chaque réponse porte l'état de votre quota :

```http title="En-têtes de réponse"
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 583
X-RateLimit-Reset: 1788171600
```

| En-tête | Signification |
|---|---|
| `X-RateLimit-Limit` | requêtes autorisées sur la fenêtre courante |
| `X-RateLimit-Remaining` | requêtes restantes |
| `X-RateLimit-Reset` | horodatage Unix (secondes) de la remise à zéro |

Un dépassement renvoie `429` :

<ApiResponse
  status={429}
  body={{
    error: {
      code: "RATE_LIMIT_EXCEEDED",
      message: "Trop de requêtes, réessayez après la fenêtre indiquée",
      retryAfterSeconds: 34,
    },
  }}
/>

**Respectez `X-RateLimit-Reset`** plutôt que de retenter immédiatement : retenter en boucle sur un `429` prolonge la coupure.

```javascript title="JavaScript"
async function appeler(url, options, tentative = 0) {
  const reponse = await fetch(url, options);

  if (reponse.status !== 429 || tentative >= 3) return reponse;

  const reset = Number(reponse.headers.get("X-RateLimit-Reset")) * 1000;
  const attente = Math.max(reset - Date.now(), 2 ** tentative * 1000);
  await new Promise((r) => setTimeout(r, attente));

  return appeler(url, options, tentative + 1);
}
```

```python title="Python"
import time

def appeler(session, url, entetes, tentative=0):
    reponse = session.get(url, headers=entetes, timeout=30)

    if reponse.status_code != 429 or tentative >= 3:
        return reponse

    reset = int(reponse.headers["X-RateLimit-Reset"])
    attente = max(reset - time.time(), 2**tentative)
    time.sleep(attente)

    return appeler(session, url, entetes, tentative + 1)
```

Les quotas sont fixés par compte apporteur, pas par clé : ajouter des clés ne les augmente pas. Si votre volume légitime les dépasse, demandez un relèvement à votre interlocuteur Instafuel plutôt que de contourner.

<Callout type="tip" title="Les écritures coûtent plus cher que les lectures">
  Les opérations d'écriture ont une fenêtre plus étroite que les lectures. Une boucle d'onboarding en masse doit être étalée, pas envoyée en rafale.
</Callout>

## Pagination

Toutes les collections sont paginées, avec la même forme.

| Paramètre | Défaut | Maximum |
|---|---|---|
| `page` | 1 | — |
| `perPage` | 20 | 100 |

<ApiExample
  path="/v1/papi/transactions"
  query={{ page: 2, perPage: 50 }}
/>

<ApiResponse
  status={200}
  body={{
    data: [
      {
        ref: "TRX-9D02C15E",
        companyId: 4790,
        station: { id: 12, name: "Station Yopougon Ananeraie" },
        driver: { id: 3311, name: "Salif Traoré" },
        vehicle: { id: 812, registration: "5678 CD 01" },
        fuelType: "SUPER",
        liters: { estimate: 30, actual: 30 },
        selfService: false,
        unitPriceFcfa: 880,
        amountFcfa: 26400,
        status: "CONFIRMED",
        createdAt: "2026-08-29T16:05:52.000Z",
        confirmedAt: "2026-08-29T16:05:52.000Z",
      },
      {
        ref: "TRX-1A73F4B0",
        companyId: 4832,
        station: { id: 12, name: "Station Plateau" },
        driver: { id: 3311, name: "Yao N'Guessan" },
        vehicle: { id: 812, registration: "1234 AB 01" },
        fuelType: "GASOIL",
        liters: { estimate: 55, actual: 55 },
        selfService: false,
        unitPriceFcfa: 750,
        amountFcfa: 41250,
        status: "PREAUTHORIZED",
        createdAt: "2026-08-29T11:22:07.000Z",
        confirmedAt: "2026-08-29T11:22:07.000Z",
      },
    ],
    pagination: {
      page: 2,
      perPage: 50,
      total: 318,
      totalPages: 7,
    },
  }}
/>

Une transaction `PENDING` est un plein ouvert : les fonds sont réservés, pas encore débités. Ne l'additionnez pas avec les `VALIDATED` dans un total de dépenses.

Parcours complet, sans surcharger l'API :

```javascript title="JavaScript"
async function toutesLesTransactions(base, params = {}) {
  const tout = [];
  let page = 1;
  let totalPages = 1;

  while (page <= totalPages) {
    const query = new URLSearchParams({
      ...params,
      page: String(page),
      perPage: "100",
    });

    const reponse = await appeler(`${base}/v1/papi/transactions?${query}`, {
      headers: { Authorization: `Bearer ${process.env.INSTAFUEL_API_KEY}` },
    });

    const { data, pagination } = await reponse.json();
    tout.push(...data);
    totalPages = pagination.totalPages;
    page += 1;
  }

  return tout;
}
```

```php title="PHP"
<?php
function toutesLesTransactions(string $base, array $params = []): array
{
    $tout = [];
    $page = 1;
    $totalPages = 1;

    while ($page <= $totalPages) {
        $query = http_build_query($params + ['page' => $page, 'perPage' => 100]);
        [$statut, $corps] = appeler($base . '/v1/papi/transactions?' . $query);

        $tout = array_merge($tout, $corps['data']);
        $totalPages = $corps['pagination']['totalPages'];
        $page++;
    }

    return $tout;
}
```

<Callout type="caution" title="Les collections triées par date bougent pendant que vous paginez">
  Les transactions sont renvoyées de la plus récente à la plus ancienne. Si de nouvelles arrivent entre deux pages, un élément peut être vu deux fois ou manqué.

  Pour un export fiable, bornez la période avec `from` et `to` sur des dates révolues, ou dédupliquez sur `ref` à l'arrivée.
</Callout>

## Gros volumes

Il n'existe pas d'export de fichier sur cette API : elle ne produit que du JSON. Deux réflexes suffisent pour tenir la charge.

**Découpez par période, pas par page.** Une plage d'un mois découpée en jours donne des lots stables, rejouables, et évite la dérive de pagination décrite plus haut.

**Consolidez avec la synthèse.** Pour des totaux, `GET /v1/papi/reports/portfolio-summary` renvoie en un appel ce que la pagination des transactions vous ferait recalculer sur des centaines de pages.

```http title="Endpoints"
GET /v1/papi/reports/portfolio-summary?from=2026-08-01&to=2026-08-31
```

Si vous avez besoin d'un classeur, il se télécharge depuis le portail partenaire.

## Fraîcheur des données

| Donnée | Fraîcheur |
|---|---|
| Solde de wallet, transactions | temps réel |
| Soldes de tout le portefeuille | temps réel, calculés à l'appel |
| Synthèse du portefeuille | calculée à l'appel, sur la période demandée |

Le solde renvoyé est celui de l'instant : `availableFcfa` retire déjà les pleins ouverts. C'est ce chiffre qu'il faut afficher à un client, pas `balanceFcfa`.

## Idempotence et rejeu

L'en-tête `X-Idempotency-Key` et la politique de rejeu sont décrits dans [Erreurs et idempotence](/guides/erreurs). Rappel utile ici : un `429` se retente **avec la même clé**.

## Délais et coupures

Prévoyez un délai d'attente côté client d'au moins **30 secondes** sur les écritures et les dépôts de preuve : un fichier de 10 Mo sur une liaison ivoirienne moyenne n'arrive pas en deux secondes.

En cas de coupure avant la réponse, ne devinez pas — rejouez avec la même clé d'idempotence.
