Référence de l’API

Idempotence

Renvoyer une requête sans l’exécuter deux fois. Quelles requêtes demandent une Idempotency-Key, et ce que l’API répond à une répétition.

Ouvrir en Markdown
Sur cette page
  1. Envoyer une clé
  2. Ce que répond une répétition
  3. Réessayer sans risque
  4. Sur le tableau de bord

Un réseau peut tomber après que l’API a fait le travail mais avant que vous ne lisiez la réponse. Le paiement a-t-il été créé ? Avec une clé d’idempotence, inutile de le savoir : renvoyez la même requête avec la même clé, et l’API répond ce qu’elle a répondu la première fois, sans refaire le travail.

Envoyer une clé

Ajoutez un en-tête Idempotency-Key à un POST. La clé est à vous : jusqu’à 255 caractères ASCII imprimables, unique pour chaque opération à faire une seule fois. Un UUID aléatoire convient ; une clé bâtie sur vos propres références aussi, comme order-1042-create. Une clé plus longue, ou qui contient d’autres caractères, est refusée avec 400, parameter_invalid.

Générez la clé avant le premier essai et gardez-la pour les nouvelles tentatives de cette opération. Une nouvelle clé veut dire une nouvelle opération.

Obligatoire sur les requêtes qui font circuler de l’argent :

  • POST /v1/payment_intents
  • POST /v1/payment_intents/{id}/confirm
  • POST /v1/checkout/sessions
  • POST /v1/refunds

Sans elle, elles répondent 400, idempotency_key_missing. Tout autre POST accepte une clé sans l’exiger. GET et DELETE l’ignorent : on peut les répéter tels quels sans risque.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
create.mjscreate.tscreate.pycreate.phpcreate.rbcreate.goCreate.javaCreate.csmain.rs
curl https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14" \
  -d '{"amount": 25000, "currency": "mad",
       "payment_method_types": ["card", "cash"]}'
import { randomUUID } from 'node:crypto'

// One key per operation, made before the first try.
const idempotencyKey = randomUUID()

async function createPaymentIntent() {
  const response = await fetch('https://pay-api.adpromax.com/v1/payment_intents', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey,
    },
    body: JSON.stringify({
      amount: 25000,
      currency: 'mad',
      payment_method_types: ['card', 'cash'],
    }),
  })
  return { response, body: await response.json() }
}
import { randomUUID } from 'node:crypto'

interface PaymentIntent {
  id: string
  object: 'payment_intent'
  amount: number
  currency: string
  status: string
}

interface ApiError {
  error: { type: string, code: string, message: string, request_id: string }
}

interface Created {
  response: Response
  body: PaymentIntent | ApiError
}

// One key per operation, made before the first try.
const idempotencyKey = randomUUID()

async function createPaymentIntent(): Promise<Created> {
  const response = await fetch('https://pay-api.adpromax.com/v1/payment_intents', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey,
    },
    body: JSON.stringify({
      amount: 25000,
      currency: 'mad',
      payment_method_types: ['card', 'cash'],
    }),
  })
  const body = (await response.json()) as PaymentIntent | ApiError
  return { response, body }
}
import os
import uuid

import requests

# One key per operation, made before the first try.
idempotency_key = str(uuid.uuid4())


def create_payment_intent():
    return requests.post(
        "https://pay-api.adpromax.com/v1/payment_intents",
        auth=(os.environ["APX_SECRET_KEY"], ""),
        headers={"Idempotency-Key": idempotency_key},
        json={
            "amount": 25000,
            "currency": "mad",
            "payment_method_types": ["card", "cash"],
        },
        timeout=30,
    )
<?php
// One key per operation, made before the first try.
$idempotencyKey = bin2hex(random_bytes(16));

function create_payment_intent(string $idempotencyKey): array
{
    $ch = curl_init('https://pay-api.adpromax.com/v1/payment_intents');
    curl_setopt_array($ch, [
        CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
        CURLOPT_POST => true,
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json',
            'Idempotency-Key: ' . $idempotencyKey,
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'amount' => 25000,
            'currency' => 'mad',
            'payment_method_types' => ['card', 'cash'],
        ]),
        CURLOPT_RETURNTRANSFER => true,
    ]);
    $body = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    return [$status, $body];
}
require 'json'
require 'net/http'
require 'securerandom'

# One key per operation, made before the first try.
IDEMPOTENCY_KEY = SecureRandom.uuid

def create_payment_intent
  uri = URI('https://pay-api.adpromax.com/v1/payment_intents')
  request = Net::HTTP::Post.new(uri)
  request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
  request['Content-Type'] = 'application/json'
  request['Idempotency-Key'] = IDEMPOTENCY_KEY
  request.body = JSON.generate(
    amount: 25000,
    currency: 'mad',
    payment_method_types: %w[card cash]
  )
  Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
    http.request(request)
  end
end
package main

import (
	"bytes"
	"crypto/rand"
	"encoding/json"
	"net/http"
	"os"
)

// One key per operation, made before the first try.
var idempotencyKey = rand.Text()

func createPaymentIntent() (*http.Response, error) {
	body, err := json.Marshal(map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card", "cash"},
	})
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/payment_intents", bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", idempotencyKey)
	return http.DefaultClient.Do(req)
}
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;

public class Create {
    // One key per operation, made before the first try.
    static final String KEY = UUID.randomUUID().toString();

    static HttpResponse<String> createPaymentIntent()
            throws IOException, InterruptedException {
        String body = """
            {
              "amount": 25000,
              "currency": "mad",
              "payment_method_types": ["card", "cash"]
            }
            """;
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/payment_intents"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", KEY)
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        return HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
    }
}
using System.Net.Http.Headers;
using System.Net.Http.Json;

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

// One key per operation, made before the first try.
var idempotencyKey = Guid.NewGuid().ToString();

async Task<HttpResponseMessage> CreatePaymentIntent()
{
    using var request = new HttpRequestMessage(
        HttpMethod.Post, "https://pay-api.adpromax.com/v1/payment_intents");
    request.Headers.Add("Idempotency-Key", idempotencyKey);
    request.Content = JsonContent.Create(new
    {
        amount = 25000,
        currency = "mad",
        payment_method_types = new[] { "card", "cash" },
    });
    return await http.SendAsync(request);
}
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde_json = "1"
//             uuid = { version = "1", features = ["v4"] }
use reqwest::blocking::{Client, Response};
use serde_json::json;
use uuid::Uuid;

fn create_payment_intent(key: &str) -> reqwest::Result<Response> {
    Client::new()
        .post("https://pay-api.adpromax.com/v1/payment_intents")
        .bearer_auth(std::env::var("APX_SECRET_KEY").unwrap_or_default())
        .header("Idempotency-Key", key)
        .json(&json!({
            "amount": 25000,
            "currency": "mad",
            "payment_method_types": ["card", "cash"]
        }))
        .send()
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // One key per operation, made before the first try.
    let idempotency_key = Uuid::new_v4().to_string();
    let response = create_payment_intent(&idempotency_key)?;
    println!("{}", response.status());
    Ok(())
}

Ce que répond une répétition

L’API garde chaque clé 24 heures, séparément pour chaque organisation et chaque mode : une clé de test et une clé de production ne partagent jamais la réponse d’une clé d’idempotence.

Vous envoyezL’API répond
Une nouvelle cléExécute la requête et garde sa réponse.
La même clé et la même requête, une fois terminéeLa réponse gardée, avec le même statut et le même corps, et les en-têtes Idempotent-Replayed: true et Original-Request: req_… (l’identifiant de la première requête). Rien ne s’exécute de nouveau.
La même clé avec une autre requête (autre chemin, autre chaîne de requête ou autre corps)400, idempotency_error, idempotency_key_reused. Rien ne s’exécute.
La même clé pendant que la première requête s’exécute encore409, idempotency_error, idempotency_in_progress. Réessayez dans un instant.

« La même requête » veut dire la même méthode, le même chemin, la même chaîne de requête et le même corps. Le corps est comparé tel que l’API l’a lu, pas octet par octet : l’ordre de ses champs et ses espaces ne comptent pas, et un champ omis vaut le même champ envoyé avec sa valeur par défaut. Les en-têtes ne sont pas comparés.

Ce qui est gardé, et ce qui ne l’est pas

  • Un résultat est gardé, y compris un refus sur le fond : un paiement refusé avec amount_too_small répond de nouveau amount_too_small pour la même clé. Corrigez la requête et envoyez-la avec une nouvelle clé.
  • Une répétition est la première réponse, octet pour octet. Une erreur rejouée porte toujours le request_id de la première requête, celui que nomme Original-Request ; l’identifiant de la répétition est dans son en-tête Request-Id.
  • Une requête refusée avant de s’exécuter n’est pas gardée : une mauvaise clé d’API, une limite de débit (429), un paramètre manquant ou invalide. Corrigez-la et renvoyez-la avec la même clé.
  • Une réponse qui dit « réessayez » n’est pas gardée : un 5xx, un 429, toute réponse avec un en-tête Retry-After, et un refus dont le code finit par _busy. La même clé exécute de nouveau la requête.
  • Un secret montré une seule fois n’est pas gardé. La répétition de la création d’un endpoint de webhook répond le même endpoint avec "secret": null : conservez le secret de la première réponse.
  • Une réponse de plus de 64 Ko n’est pas gardée : la même clé exécuterait de nouveau la requête. Les réponses des requêtes qui font circuler de l’argent sont bien plus petites.
  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
replay.mjsreplay.tsreplay.pyreplay.phpreplay.rbreplay.goReplay.javaReplay.csmain.rs
# the same command again: the answer is replayed
curl -i https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14" \
  -d '{"amount": 25000, "currency": "mad",
       "payment_method_types": ["card", "cash"]}'
// The same request again, with the same key: the answer is replayed.
const response = await fetch('https://pay-api.adpromax.com/v1/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': '8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
  }),
})
console.log(response.status)
for (const name of ['Idempotent-Replayed', 'Original-Request', 'Request-Id'])
  console.log(`${name}: ${response.headers.get(name)}`)
// The same request again, with the same key: the answer is replayed.
const response: Response = await fetch('https://pay-api.adpromax.com/v1/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': '8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
  }),
})
const names: string[] = ['Idempotent-Replayed', 'Original-Request', 'Request-Id']
console.log(response.status)
for (const name of names)
  console.log(`${name}: ${response.headers.get(name)}`)
import os

import requests

# The same request again, with the same key: the answer is replayed.
response = requests.post(
    "https://pay-api.adpromax.com/v1/payment_intents",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    headers={"Idempotency-Key": "8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14"},
    json={
        "amount": 25000,
        "currency": "mad",
        "payment_method_types": ["card", "cash"],
    },
)
print(response.status_code)
for name in ("Idempotent-Replayed", "Original-Request", "Request-Id"):
    print(f"{name}: {response.headers.get(name)}")
<?php
// The same request again, with the same key: the answer is replayed.
$headers = [];
$ch = curl_init('https://pay-api.adpromax.com/v1/payment_intents');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Idempotency-Key: 8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => 25000,
        'currency' => 'mad',
        'payment_method_types' => ['card', 'cash'],
    ]),
    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);
echo curl_getinfo($ch, CURLINFO_RESPONSE_CODE), PHP_EOL;
foreach (['Idempotent-Replayed', 'Original-Request', 'Request-Id'] as $name) {
    echo $name, ': ', $headers[strtolower($name)] ?? '', PHP_EOL;
}
require 'json'
require 'net/http'

# The same request again, with the same key: the answer is replayed.
uri = URI('https://pay-api.adpromax.com/v1/payment_intents')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = '8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14'
request.body = JSON.generate(
  amount: 25000,
  currency: 'mad',
  payment_method_types: %w[card cash]
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
puts response.code
%w[Idempotent-Replayed Original-Request Request-Id].each do |name|
  puts "#{name}: #{response[name]}"
end
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	body, err := json.Marshal(map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card", "cash"},
	})
	if err != nil {
		log.Fatal(err)
	}
	// The same request again, with the same key: the answer is replayed.
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/payment_intents", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", "8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14")

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

	fmt.Println(res.Status)
	names := []string{"Idempotent-Replayed", "Original-Request", "Request-Id"}
	for _, name := range names {
		fmt.Printf("%s: %s\n", name, res.Header.Get(name))
	}
}
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 Replay {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "amount": 25000,
              "currency": "mad",
              "payment_method_types": ["card", "cash"]
            }
            """;
        // The same request again, with the same key: the answer is replayed.
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/payment_intents"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", "8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.statusCode());
        List<String> names = List.of(
            "Idempotent-Replayed", "Original-Request", "Request-Id");
        for (String name : names) {
            System.out.println(name + ": "
                + response.headers().firstValue(name).orElse(""));
        }
    }
}
using System.Net.Http.Headers;
using System.Net.Http.Json;

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

// The same request again, with the same key: the answer is replayed.
using var request = new HttpRequestMessage(
    HttpMethod.Post, "https://pay-api.adpromax.com/v1/payment_intents");
request.Headers.Add("Idempotency-Key", "8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14");
request.Content = JsonContent.Create(new
{
    amount = 25000,
    currency = "mad",
    payment_method_types = new[] { "card", "cash" },
});

using var response = await http.SendAsync(request);
Console.WriteLine((int)response.StatusCode);
string[] names = ["Idempotent-Replayed", "Original-Request", "Request-Id"];
foreach (var name in names)
{
    response.Headers.TryGetValues(name, out var values);
    Console.WriteLine($"{name}: {values?.FirstOrDefault()}");
}
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde_json = "1"
use serde_json::json;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // The same request again, with the same key: the answer is replayed.
    let response = reqwest::blocking::Client::new()
        .post("https://pay-api.adpromax.com/v1/payment_intents")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .header("Idempotency-Key", "8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14")
        .json(&json!({
            "amount": 25000,
            "currency": "mad",
            "payment_method_types": ["card", "cash"]
        }))
        .send()?;

    println!("{}", response.status());
    for name in ["Idempotent-Replayed", "Original-Request", "Request-Id"] {
        let value = response.headers().get(name).and_then(|v| v.to_str().ok());
        println!("{name}: {}", value.unwrap_or(""));
    }
    Ok(())
}
Réponse
HTTP/1.1 200 OK
Idempotent-Replayed: true
Original-Request: req_01k7d7m2n5b8v1c4x7z0q3w6es
Request-Id: req_01k7d7p9a2s5d8f1g4h7j0k3hm

Réessayer sans risque

Réessayez quand la réponse n’est jamais venue (délai dépassé, connexion coupée), sur 409 idempotency_in_progress, sur 429 après le délai Retry-After, et sur un 5xx. Attendez de plus en plus longtemps entre les essais, et gardez la même clé à chaque fois. Ne réessayez pas un autre 4xx : il échouera de la même façon. N’attendez pas non plus sur un Retry-After de plusieurs minutes ou heures : c’est le plafond quotidien du mode test, qui dure jusqu’à minuit. Voir Limites de débit.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
retry.mjsretry.tsretry.pyretry.phpretry.rbretry.goRetry.javaRetry.csmain.rs
# --retry sends it again after a timeout, a 429 or a 5xx, with the same key;
# --retry-max-time gives up when Retry-After is longer (test mode's daily cap)
curl https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  --retry 4 \
  --retry-max-time 60 \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8e0d2c5e-4f3a-4b8e-9c71-2a6f0d3b5e14" \
  -d '{"amount": 25000, "currency": "mad",
       "payment_method_types": ["card", "cash"]}'
const RETRY_STATUSES = new Set([409, 429, 500, 502, 503, 504])
const LONGEST_WAIT = 60 // seconds

// createPaymentIntent() and its key come from the step above.
async function withRetries(attempts = 4) {
  for (let attempt = 1; ; attempt++) {
    try {
      const { response, body } = await createPaymentIntent()
      if (!RETRY_STATUSES.has(response.status) || attempt === attempts)
        return body
      const wait = Number(response.headers.get('Retry-After')) || 2 ** attempt
      if (wait > LONGEST_WAIT) // test mode's daily cap, until midnight
        return body
      await new Promise(resolve => setTimeout(resolve, wait * 1000))
    }
    catch (error) { // no answer: the network failed
      if (attempt === attempts)
        throw error
      await new Promise(resolve => setTimeout(resolve, 2 ** attempt * 1000))
    }
  }
}
const RETRY_STATUSES = new Set([409, 429, 500, 502, 503, 504])
const LONGEST_WAIT = 60 // seconds

// createPaymentIntent() and its key come from the step above.
declare function createPaymentIntent(): Promise<{
  response: Response
  body: unknown
}>

function sleep(seconds: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, seconds * 1000))
}

async function withRetries(attempts = 4): Promise<unknown> {
  for (let attempt = 1; ; attempt++) {
    try {
      const { response, body } = await createPaymentIntent()
      if (!RETRY_STATUSES.has(response.status) || attempt === attempts)
        return body
      const wait = Number(response.headers.get('Retry-After')) || 2 ** attempt
      if (wait > LONGEST_WAIT) // test mode's daily cap, until midnight
        return body
      await sleep(wait)
    }
    catch (error) { // no answer: the network failed
      if (attempt === attempts)
        throw error
      await sleep(2 ** attempt)
    }
  }
}
import time

import requests

RETRY_STATUSES = {409, 429, 500, 502, 503, 504}
LONGEST_WAIT = 60  # seconds


# create_payment_intent() and its key come from the step above.
def with_retries(attempts=4):
    for attempt in range(1, attempts + 1):
        try:
            response = create_payment_intent()
        except requests.ConnectionError:  # no answer: the network failed
            if attempt == attempts:
                raise
            time.sleep(2 ** attempt)
            continue
        if response.status_code not in RETRY_STATUSES or attempt == attempts:
            return response.json()
        wait = int(response.headers.get("Retry-After", 2 ** attempt))
        if wait > LONGEST_WAIT:  # test mode's daily cap, until midnight
            return response.json()
        time.sleep(wait)
<?php
// create_payment_intent() and $idempotencyKey come from the step above.
function with_retries(string $idempotencyKey, int $attempts = 4): ?array
{
    $retry = [0, 409, 429, 500, 502, 503, 504]; // 0: no answer at all
    for ($attempt = 1; ; $attempt++) {
        [$status, $body] = create_payment_intent($idempotencyKey);
        if (!in_array($status, $retry, true) || $attempt === $attempts) {
            return $body;
        }
        sleep(2 ** $attempt);
    }
}
require 'json'
require 'net/http'

RETRY_STATUSES = [409, 429, 500, 502, 503, 504].freeze
LONGEST_WAIT = 60 # seconds

# create_payment_intent and its key come from the step above.
def with_retries(attempts = 4)
  1.upto(attempts) do |attempt|
    begin
      response = create_payment_intent
      retry_it = RETRY_STATUSES.include?(response.code.to_i)
      return JSON.parse(response.body) unless retry_it && attempt < attempts

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

      sleep(wait)
    rescue SocketError, SystemCallError, Timeout::Error
      # no answer: the network failed
      raise if attempt == attempts

      sleep(2**attempt)
    end
  end
end
package main

import (
	"net/http"
	"strconv"
	"time"
)

const longestWait = 60 * time.Second

var retryStatuses = map[int]bool{
	409: true, 429: true, 500: true, 502: true, 503: true, 504: true,
}

// createPaymentIntent() and its key come from the step above.
func withRetries(attempts int) (*http.Response, error) {
	for attempt := 1; ; attempt++ {
		wait := time.Duration(1<<attempt) * time.Second
		res, err := createPaymentIntent()
		if err != nil { // no answer: the network failed
			if attempt == attempts {
				return nil, err
			}
			time.Sleep(wait)
			continue
		}
		if !retryStatuses[res.StatusCode] || attempt == attempts {
			return res, nil
		}
		after, err := strconv.Atoi(res.Header.Get("Retry-After"))
		if err == nil {
			wait = time.Duration(after) * time.Second
		}
		if wait > longestWait { // test mode's daily cap, until midnight
			return res, nil
		}
		res.Body.Close()
		time.Sleep(wait)
	}
}
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.Set;

public class Retry {
    static final Set<Integer> RETRY_STATUSES =
        Set.of(409, 429, 500, 502, 503, 504);
    static final long LONGEST_WAIT = 60; // seconds

    // Create.createPaymentIntent() and its key come from the step above.
    static HttpResponse<String> withRetries(int attempts)
            throws IOException, InterruptedException {
        for (int attempt = 1; ; attempt++) {
            long wait = 1L << attempt;
            try {
                HttpResponse<String> response = Create.createPaymentIntent();
                boolean retry = RETRY_STATUSES.contains(response.statusCode());
                if (!retry || attempt == attempts) {
                    return response;
                }
                wait = response.headers()
                    .firstValueAsLong("Retry-After").orElse(wait);
                if (wait > LONGEST_WAIT) { // test mode's daily cap
                    return response;
                }
            } catch (IOException e) { // no answer: the network failed
                if (attempt == attempts) {
                    throw e;
                }
            }
            Thread.sleep(wait * 1000);
        }
    }
}
int[] retryStatuses = [409, 429, 500, 502, 503, 504];
var longestWait = TimeSpan.FromSeconds(60);

// CreatePaymentIntent() and its key come from the step above.
async Task<HttpResponseMessage> WithRetries(int attempts = 4)
{
    for (var attempt = 1; ; attempt++)
    {
        var wait = TimeSpan.FromSeconds(Math.Pow(2, attempt));
        try
        {
            var response = await CreatePaymentIntent();
            var retry = retryStatuses.Contains((int)response.StatusCode);
            if (!retry || attempt == attempts)
                return response;
            wait = response.Headers.RetryAfter?.Delta ?? wait;
            if (wait > longestWait) // test mode's daily cap, until midnight
                return response;
        }
        catch (HttpRequestException) when (attempt < attempts)
        {
            // no answer: the network failed
        }
        await Task.Delay(wait);
    }
}
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
use std::{thread::sleep, time::Duration};

use reqwest::blocking::Response;

const RETRY_STATUSES: [u16; 6] = [409, 429, 500, 502, 503, 504];
const LONGEST_WAIT: u64 = 60; // seconds

// create_payment_intent() and its key come from the step above.
fn with_retries(key: &str, attempts: u32) -> reqwest::Result<Response> {
    let mut attempt = 1;
    loop {
        let wait = match create_payment_intent(key) {
            Ok(response) => {
                let status = response.status().as_u16();
                if !RETRY_STATUSES.contains(&status) || attempt == attempts {
                    return Ok(response);
                }
                let wait = response
                    .headers()
                    .get("Retry-After")
                    .and_then(|value| value.to_str().ok()?.parse().ok())
                    .unwrap_or(2u64.pow(attempt));
                if wait > LONGEST_WAIT {
                    // test mode's daily cap, until midnight
                    return Ok(response);
                }
                wait
            }
            // no answer: the network failed
            Err(error) if attempt == attempts => return Err(error),
            Err(_) => 2u64.pow(attempt),
        };
        sleep(Duration::from_secs(wait));
        attempt += 1;
    }
}

Sur le tableau de bord

Les formulaires de remboursement et de paiement en espèces du tableau de bord envoient une clé pour vous : appuyer deux fois sur le bouton fait un seul remboursement. Chaque requête que vous envoyez avec une clé l’affiche dans les journaux de requêtes du tableau de bord, à côté de son Request-Id.