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.
Sur cette page
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_intentsPOST /v1/payment_intents/{id}/confirmPOST /v1/checkout/sessionsPOST /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 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
endpackage 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 envoyez | L’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ée | La 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 encore | 409, 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_smallrépond de nouveauamount_too_smallpour 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_idde la première requête, celui que nommeOriginal-Request; l’identifiant de la répétition est dans son en-têteRequest-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, un429, toute réponse avec un en-têteRetry-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.
# 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]}"
endpackage 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(())
}HTTP/1.1 200 OK
Idempotent-Replayed: true
Original-Request: req_01k7d7m2n5b8v1c4x7z0q3w6es
Request-Id: req_01k7d7p9a2s5d8f1g4h7j0k3hmRé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.
# --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
endpackage 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.