# Limites de débit

Les compartiments de requêtes de votre organisation, les en-têtes RateLimit, la gestion d’un 429, et comment demander une limite plus haute.

L’API limite le nombre de requêtes que votre organisation envoie par minute, pour que le pic d’une
intégration ne ralentisse jamais les autres. Les limites comptent **par organisation et par mode** :
toutes les clés de votre organisation les partagent, et le mode test a les siennes. Utiliser le
tableau de bord ne les consomme pas, pas plus que les webhooks que nous vous envoyons.

## Les compartiments

Chaque requête compte dans un ou plusieurs **compartiments** (*buckets*). Elle doit trouver de la
place dans chacun de ses compartiments ; une requête refusée par un compartiment ne consomme rien
dans les autres.

| Compartiment          | Couvre                                                     | Production, par minute | Test, par minute |
| --------------------- | ---------------------------------------------------------- | ---------------------- | ---------------- |
| Toutes les requêtes   | Chaque requête à l’API                                     | 1 200                  | 600              |
| Écritures             | Chaque `POST`, `PUT` et `DELETE`                           | 300                    | 150              |
| Créations de paiement | `POST /v1/payment_intents` et `POST /v1/checkout/sessions` | 120                    | 60               |
| Recherches et exports | Les recherches dans les listes et les exports              | 30                     | 30               |

Un compartiment se remplit à son rythme par minute, et garde **10 secondes de réserve** en plus pour
les pics courts, arrondies à la requête entière. Une limite de 120 par minute se remplit de 2 requêtes
par seconde et en laisse passer jusqu’à 140 d’un coup.

Ce sont les valeurs par défaut ; les vôtres peuvent différer. **Développeurs → Limites de l’API**
montre les valeurs qui s’appliquent à votre organisation.

## Lire les en-têtes

Une fois votre clé acceptée, chaque réponse porte trois en-têtes sur le compartiment le plus serré de
la requête, un `429` compris :

* `RateLimit-Limit` : la limite du compartiment par minute ;
* `RateLimit-Remaining` : les requêtes que vous pouvez encore envoyer tout de suite. Un compartiment
  plein garde sa réserve en plus de sa limite : ce chiffre peut donc dépasser `RateLimit-Limit`,
  jusqu’à 699 pour une limite de 600 ;
* `RateLimit-Reset` : les secondes avant que le compartiment soit de nouveau plein.

Une requête refusée avant d’être comptée n’en porte aucun : une clé absente, refusée ou qui n’a pas le
droit de faire la requête, une version de l’API qui n’existe pas, une adresse que l’API ne connaît pas.

Lisez `RateLimit-Remaining` pour ralentir avant d’atteindre la limite, plutôt qu’après. Pendant un
incident de notre côté, le compte peut être moins régulier un moment : prenez les en-têtes comme un
repère, et un `429` comme la règle.

```bash
curl -i https://pay-api.adpromax.com/v1/account \
  -u "$APX_SECRET_KEY:"
```

```js title="headers.mjs"
const response = await fetch('https://pay-api.adpromax.com/v1/account', {
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const names = ['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset']
for (const name of names)
  console.log(`${name}: ${response.headers.get(name)}`)
```

```ts title="headers.ts"
const response: Response = await fetch('https://pay-api.adpromax.com/v1/account', {
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const names = ['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset']
for (const name of names) {
  const value: string | null = response.headers.get(name)
  console.log(`${name}: ${value}`)
}
```

```python title="headers.py"
import os

import requests

response = requests.get(
    "https://pay-api.adpromax.com/v1/account",
    auth=(os.environ["APX_SECRET_KEY"], ""),
)
for name in ("RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset"):
    print(f"{name}: {response.headers.get(name)}")
```

```php title="headers.php"
<?php
$headers = [];
$ch = curl_init('https://pay-api.adpromax.com/v1/account');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
        $parts = explode(':', $line, 2);
        if (count($parts) === 2) {
            $headers[strtolower($parts[0])] = trim($parts[1]);
        }
        return strlen($line);
    },
]);
curl_exec($ch);
foreach (['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset'] as $name) {
    echo $name, ': ', $headers[strtolower($name)] ?? '', PHP_EOL;
}
```

```ruby title="headers.rb"
require 'net/http'

uri = URI('https://pay-api.adpromax.com/v1/account')
request = Net::HTTP::Get.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
%w[RateLimit-Limit RateLimit-Remaining RateLimit-Reset].each do |name|
  puts "#{name}: #{response[name]}"
end
```

```go title="headers.go"
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://pay-api.adpromax.com/v1/account", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	names := []string{"RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset"}
	for _, name := range names {
		fmt.Printf("%s: %s\n", name, res.Header.Get(name))
	}
}
```

```java title="Headers.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;

public class Headers {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://pay-api.adpromax.com/v1/account"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .GET()
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        List<String> names = List.of(
            "RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset");
        for (String name : names) {
            System.out.println(name + ": "
                + response.headers().firstValue(name).orElse(""));
        }
    }
}
```

```csharp title="Headers.cs"
using System.Net.Http.Headers;

using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

using var response = await http.GetAsync("https://pay-api.adpromax.com/v1/account");
string[] names = ["RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset"];
foreach (var name in names)
{
    response.Headers.TryGetValues(name, out var values);
    Console.WriteLine($"{name}: {values?.FirstOrDefault()}");
}
```

```rust title="main.rs"
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking"] }
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::blocking::Client::new()
        .get("https://pay-api.adpromax.com/v1/account")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .send()?;

    for name in ["RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset"] {
        let value = response.headers().get(name).and_then(|v| v.to_str().ok());
        println!("{name}: {}", value.unwrap_or(""));
    }
    Ok(())
}
```

```text response
HTTP/1.1 200 OK
Content-Type: application/json
Request-Id: req_01k7e6a2b5c8d1f4g7h0j3k6mx
RateLimit-Limit: 600
RateLimit-Remaining: 699
RateLimit-Reset: 1
```

## Quand une limite est atteinte

L’API répond `429` avec une `rate_limit_error` dont le code est `rate_limit_exceeded`. Le message nomme
le compartiment, sa limite et l’attente, et l’en-tête `Retry-After` donne cette attente en secondes.
Rien n’a été fait : la requête peut être renvoyée telle quelle.

```json title="429 Too Many Requests" response
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Too many payment creation requests: the limit is 60 per minute in test mode. Try again in 1 second.",
    "doc_url": "https://pay-docs.adpromax.com/errors#rate_limit_exceeded",
    "request_id": "req_01k7e6c9d3f6g9h2j5k8m1n4pq"
  }
}
```

## Le plafond quotidien du mode test

En plus de ses limites de débit, le mode test a un plafond quotidien par organisation :
**10 000 requêtes** et **1 000 créations de paiement** par jour, comptées en heure du Maroc. Au-delà,
l’API répond `429` avec le code `test_daily_cap_reached` jusqu’à minuit, heure du Maroc. La production
n’a pas de plafond quotidien.

Le plafond protège le bac à sable partagé. Une suite de tests automatisés l’atteint rarement ; un test
de charge, si : faites vos tests de charge sur votre propre code, pas sur le bac à sable.

```json title="429 Too Many Requests" response
{
  "error": {
    "type": "rate_limit_error",
    "code": "test_daily_cap_reached",
    "message": "Test mode's daily cap of 1,000 payment creations is reached. It resets at midnight, Morocco time.",
    "doc_url": "https://pay-docs.adpromax.com/errors#test_daily_cap_reached",
    "request_id": "req_01k7e6f5g8h1j4k7m0n3p6q9rs"
  }
}
```

## Gérer un 429

1. **Attendez** le nombre de secondes de `Retry-After`, plus un peu de hasard, pour que plusieurs
   serveurs ne réessaient pas au même instant. Une limite de débit demande quelques secondes au plus.
   Une attente de plusieurs minutes ou heures, c’est le
   [plafond quotidien](#le-plafond-quotidien-du-mode-test), qui dure jusqu’à minuit, heure du Maroc :
   n’attendez pas dessus. Arrêtez, et remontez l’erreur.
2. **Renvoyez la même requête**, avec la même `Idempotency-Key`. Un refus pour la limite de débit
   n’est pas gardé avec la clé : le nouvel essai s’exécute, et l’opération n’a toujours lieu qu’une
   fois. Voir [Idempotence](https://pay-docs.adpromax.com/fr/api-reference/idempotency).
3. **Abandonnez après quelques essais**, et journalisez le `request_id`.

Pour rester sous les limites :

* étalez les traitements par lots dans le temps au lieu de tout envoyer d’un coup ;
* surveillez `RateLimit-Remaining` ;
* apprenez l’issue des paiements par les [webhooks](https://pay-docs.adpromax.com/fr/webhooks) plutôt qu’en les relisant sans cesse.

```bash
# --retry sends it again after a 429, waiting as long as Retry-After says;
# --retry-max-time gives up when that wait is longer (test mode's daily cap)
curl https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  --retry 5 \
  --retry-max-time 60 \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1046-create" \
  -d '{"amount": 25000, "currency": "mad",
       "payment_method_types": ["card"]}'
```

```js title="retry-429.mjs"
const api = 'https://pay-api.adpromax.com/v1'
const LONGEST_WAIT = 60 // seconds

// Sends a request again after a 429, waiting as long as Retry-After says.
async function send(path, init, attempts = 5) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`${api}${path}`, init)
    if (response.status !== 429 || attempt === attempts)
      return response
    const wait = Number(response.headers.get('Retry-After')) || 1
    if (wait > LONGEST_WAIT) // test mode's daily cap, until midnight
      return response
    const jitter = Math.random() * 500
    await new Promise(resolve => setTimeout(resolve, wait * 1000 + jitter))
  }
}

const response = await send('/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    // The same key on every try: the payment is created once.
    'Idempotency-Key': 'order-1046-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card'],
  }),
})
console.log(response.status, response.headers.get('RateLimit-Remaining'))
```

```ts title="retry-429.ts"
const api = 'https://pay-api.adpromax.com/v1'
const LONGEST_WAIT = 60 // seconds

// Sends a request again after a 429, waiting as long as Retry-After says.
async function send(
  path: string,
  init: RequestInit,
  attempts = 5,
): Promise<Response> {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`${api}${path}`, init)
    if (response.status !== 429 || attempt === attempts)
      return response
    const wait = Number(response.headers.get('Retry-After')) || 1
    if (wait > LONGEST_WAIT) // test mode's daily cap, until midnight
      return response
    const jitter = Math.random() * 500
    await new Promise(resolve => setTimeout(resolve, wait * 1000 + jitter))
  }
}

const response = await send('/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    // The same key on every try: the payment is created once.
    'Idempotency-Key': 'order-1046-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card'],
  }),
})
console.log(response.status, response.headers.get('RateLimit-Remaining'))
```

```python title="retry_429.py"
import os
import random
import time

import requests

API = "https://pay-api.adpromax.com/v1"
AUTH = (os.environ["APX_SECRET_KEY"], "")
LONGEST_WAIT = 60  # seconds

# Sends a request again after a 429, waiting as long as Retry-After says.
def send(path, body, key, attempts=5):
    for attempt in range(1, attempts + 1):
        response = requests.post(
            f"{API}{path}", auth=AUTH, json=body,
            # The same key on every try: the payment is created once.
            headers={"Idempotency-Key": key},
        )
        if response.status_code != 429 or attempt == attempts:
            return response
        wait = int(response.headers.get("Retry-After", 1))
        if wait > LONGEST_WAIT:  # test mode's daily cap, until midnight
            return response
        time.sleep(wait + random.random() / 2)

response = send("/payment_intents", {
    "amount": 25000,
    "currency": "mad",
    "payment_method_types": ["card"],
}, "order-1046-create")
print(response.status_code, response.headers.get("RateLimit-Remaining"))
```

```php title="retry_429.php"
<?php
const LONGEST_WAIT = 60; // seconds

// Sends a request again after a 429, waiting as long as Retry-After says.
function send(string $path, array $body, string $key, int $attempts = 5): array
{
    for ($attempt = 1; ; $attempt++) {
        $retryAfter = 1;
        $ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
        curl_setopt_array($ch, [
            CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                // The same key on every try: the payment is created once.
                'Idempotency-Key: ' . $key,
            ],
            CURLOPT_POSTFIELDS => json_encode($body),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$retryAfter) {
                if (stripos($line, 'Retry-After:') === 0) {
                    $retryAfter = (int) trim(substr($line, 12));
                }
                return strlen($line);
            },
        ]);
        $answer = json_decode(curl_exec($ch), true);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        if ($status !== 429 || $attempt === $attempts) {
            return [$status, $answer];
        }
        if ($retryAfter > LONGEST_WAIT) { // test mode's daily cap, until midnight
            return [$status, $answer];
        }
        usleep(($retryAfter * 1000 + random_int(0, 500)) * 1000);
    }
}

[$status, $intent] = send('/payment_intents', [
    'amount' => 25000,
    'currency' => 'mad',
    'payment_method_types' => ['card'],
], 'order-1046-create');
echo $status, PHP_EOL;
```

```ruby title="retry_429.rb"
require 'json'
require 'net/http'

API = 'https://pay-api.adpromax.com/v1'.freeze
LONGEST_WAIT = 60 # seconds

# Sends a request again after a 429, waiting as long as Retry-After says.
def send_again(path, body, key, attempts = 5)
  uri = URI("#{API}#{path}")
  request = Net::HTTP::Post.new(uri)
  request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
  request['Content-Type'] = 'application/json'
  # The same key on every try: the payment is created once.
  request['Idempotency-Key'] = key
  request.body = JSON.generate(body)

  1.upto(attempts) do |attempt|
    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(request)
    end
    return response if response.code != '429' || attempt == attempts

    wait = (response['Retry-After'] || 1).to_i
    # A longer wait is test mode's daily cap, until midnight.
    return response if wait > LONGEST_WAIT

    sleep(wait + rand / 2)
  end
end

response = send_again('/payment_intents', {
  amount: 25000,
  currency: 'mad',
  payment_method_types: ['card']
}, 'order-1046-create')
puts "#{response.code} #{response['RateLimit-Remaining']}"
```

```go title="retry_429.go"
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"os"
	"strconv"
	"time"
)

const api = "https://pay-api.adpromax.com/v1"

const longestWait = 60 // seconds

// send posts a request again after a 429, waiting as long as Retry-After says.
func send(
	path string, body any, key string, attempts int,
) (*http.Response, error) {
	payload, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	for attempt := 1; ; attempt++ {
		req, err := http.NewRequest(http.MethodPost, api+path,
			bytes.NewReader(payload))
		if err != nil {
			return nil, err
		}
		req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
		req.Header.Set("Content-Type", "application/json")
		// The same key on every try: the payment is created once.
		req.Header.Set("Idempotency-Key", key)

		res, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, err
		}
		if res.StatusCode != http.StatusTooManyRequests || attempt == attempts {
			return res, nil
		}
		wait, err := strconv.Atoi(res.Header.Get("Retry-After"))
		if err != nil {
			wait = 1
		}
		if wait > longestWait { // test mode's daily cap, until midnight
			return res, nil
		}
		res.Body.Close()
		jitter := time.Duration(rand.Intn(500)) * time.Millisecond
		time.Sleep(time.Duration(wait)*time.Second + jitter)
	}
}

func main() {
	res, err := send("/payment_intents", map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card"},
	}, "order-1046-create", 5)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	fmt.Println(res.StatusCode, res.Header.Get("RateLimit-Remaining"))
}
```

```java title="Retry429.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.ThreadLocalRandom;

public class Retry429 {
    static final String API = "https://pay-api.adpromax.com/v1";
    static final HttpClient HTTP = HttpClient.newHttpClient();
    static final long LONGEST_WAIT = 60; // seconds

    // Sends a request again after a 429, waiting as long as Retry-After says.
    static HttpResponse<String> send(
            String path, String body, String key, int attempts) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(API + path))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            // The same key on every try: the payment is created once.
            .header("Idempotency-Key", key)
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        for (int attempt = 1; ; attempt++) {
            HttpResponse<String> response =
                HTTP.send(request, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() != 429 || attempt == attempts) {
                return response;
            }
            long wait = response.headers()
                .firstValueAsLong("Retry-After").orElse(1);
            if (wait > LONGEST_WAIT) { // test mode's daily cap, until midnight
                return response;
            }
            long jitter = ThreadLocalRandom.current().nextLong(500);
            Thread.sleep(wait * 1000 + jitter);
        }
    }

    public static void main(String[] args) throws Exception {
        HttpResponse<String> response = send("/payment_intents", """
            {
              "amount": 25000,
              "currency": "mad",
              "payment_method_types": ["card"]
            }
            """, "order-1046-create", 5);
        System.out.println(response.statusCode() + " "
            + response.headers().firstValue("RateLimit-Remaining").orElse(""));
    }
}
```

```csharp title="Retry429.cs"
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;

const string Api = "https://pay-api.adpromax.com/v1";
var longestWait = TimeSpan.FromSeconds(60);
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

// Sends a request again after a 429, waiting as long as Retry-After says.
async Task<HttpResponseMessage> Send(
    string path, object body, string key, int attempts = 5)
{
    for (var attempt = 1; ; attempt++)
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, Api + path);
        // The same key on every try: the payment is created once.
        request.Headers.Add("Idempotency-Key", key);
        request.Content = JsonContent.Create(body);

        var answer = await http.SendAsync(request);
        var limited = answer.StatusCode == HttpStatusCode.TooManyRequests;
        if (!limited || attempt == attempts)
            return answer;
        var wait = answer.Headers.RetryAfter?.Delta
            ?? TimeSpan.FromSeconds(1);
        if (wait > longestWait) // test mode's daily cap, until midnight
            return answer;
        var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(500));
        await Task.Delay(wait + jitter);
    }
}

using var response = await Send("/payment_intents", new
{
    amount = 25000,
    currency = "mad",
    payment_method_types = new[] { "card" },
}, "order-1046-create");
response.Headers.TryGetValues("RateLimit-Remaining", out var remaining);
Console.WriteLine($"{(int)response.StatusCode} {remaining?.FirstOrDefault()}");
```

```rust title="main.rs"
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde_json = "1"
//             rand = "0.9"
use std::{thread::sleep, time::Duration};

use reqwest::blocking::{Client, Response};
use serde_json::{json, Value};

const API: &str = "https://pay-api.adpromax.com/v1";
const LONGEST_WAIT: u64 = 60; // seconds

// Sends a request again after a 429, waiting as long as Retry-After says.
fn send(
    path: &str,
    body: &Value,
    key: &str,
    attempts: u32,
) -> reqwest::Result<Response> {
    let client = Client::new();
    let secret = std::env::var("APX_SECRET_KEY").unwrap_or_default();
    let mut attempt = 1;
    loop {
        let response = client
            .post(format!("{API}{path}"))
            .bearer_auth(&secret)
            // The same key on every try: the payment is created once.
            .header("Idempotency-Key", key)
            .json(body)
            .send()?;
        if response.status().as_u16() != 429 || attempt == attempts {
            return Ok(response);
        }
        let wait: u64 = response
            .headers()
            .get("Retry-After")
            .and_then(|value| value.to_str().ok()?.parse().ok())
            .unwrap_or(1);
        if wait > LONGEST_WAIT {
            // test mode's daily cap, until midnight
            return Ok(response);
        }
        let jitter: u64 = rand::random_range(0..500);
        sleep(Duration::from_millis(wait * 1000 + jitter));
        attempt += 1;
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = send(
        "/payment_intents",
        &json!({
            "amount": 25000,
            "currency": "mad",
            "payment_method_types": ["card"]
        }),
        "order-1046-create",
        5,
    )?;
    let remaining = response
        .headers()
        .get("RateLimit-Remaining")
        .and_then(|value| value.to_str().ok())
        .unwrap_or("");
    println!("{} {}", response.status().as_u16(), remaining);
    Ok(())
}
```

## Voir vos limites

**Développeurs → Limites de l’API**, sur le tableau de bord, montre pour chaque compartiment :

* sa limite par minute et sa réserve ;
* d’où vient la limite : *Valeur globale par défaut*, *Offre*, ou *Personnalisée jusqu’au …* quand
  nous en avons fixé une pour vous ;
* votre pic d’utilisation et le nombre de réponses `429` sur les dernières 24 heures.

En mode test, la page montre aussi la part du plafond quotidien que vous avez utilisée.

## Demander une limite plus haute

Quand votre trafic grandit, ou avant une campagne, demandez davantage dans
**Développeurs → Limites de l’API → Demander une limite plus haute** :

1. le compartiment, et le mode : production, test ou les deux ;
2. la valeur dont vous avez besoin : deux ou cinq fois la limite actuelle, ou votre propre chiffre ;
3. jusqu’à quand : une date, ou sans fin ;
4. la raison, par exemple « Campagne du Ramadan, environ 3 fois le trafic habituel » ;
5. une adresse e-mail pour notre réponse.

Notre équipe examine votre utilisation et votre compte, puis approuve ou refuse. Une forte hausse
demande l’approbation d’une deuxième personne de notre côté, ce qui peut prendre un peu plus de temps.
La décision arrive par e-mail et s’applique en quelques secondes ; la source du compartiment devient
alors *Personnalisée*. Vous pouvez avoir une demande ouverte par compartiment et par mode, et
l’annuler tant qu’elle est ouverte.

Le **Propriétaire**, les **Administrateurs** et les **Développeurs** de votre organisation peuvent
demander. Faites-le quelques jours avant une campagne plutôt que le jour même.

Nous pouvons aussi abaisser une limite quand une intégration nuit à la plateforme. Nous vous le
disons alors par e-mail.

## Autres limites

Elles protègent la plateforme et ne changent pas d’une organisation à l’autre :

* la connexion, l’inscription, la page de paiement hébergée et la page du bon sont limitées par
  visiteur ;
* une organisation garde au plus 16 endpoints de webhook par mode ;
* au plus cinq envois de webhook sont en cours vers un même endpoint à la fois ;
* une liste renvoie au plus 100 objets par requête (`limit`) ;
* une `Idempotency-Key` fait au plus 255 caractères.
