API reference
Idempotency
Send a request again without doing it twice. Which requests need an Idempotency-Key, and what the API answers to a repeat.
A network can fail after the API has done the work but before you read the answer. Did the payment get created? With an idempotency key you do not need to know: send the same request again with the same key, and the API answers what it answered the first time, without doing the work twice.
Send a key
Add an Idempotency-Key header to a POST. The key is yours: up to 255 printable ASCII
characters, unique for each operation you mean to do once. A random UUID works; so does a key built
from your own references, such as order-1042-create. A key that is longer, or holds other
characters, is refused with 400, parameter_invalid.
Generate the key before the first try and keep it for the retries of that operation. A new key means a new operation.
Required on the requests that move money:
POST /v1/payment_intentsPOST /v1/payment_intents/{id}/confirmPOST /v1/checkout/sessionsPOST /v1/refunds
Without it they answer 400, idempotency_key_missing. Every other POST accepts a key and does
not require one. GET and DELETE ignore it: they are safe to repeat as they are.
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(())
}What a repeat answers
The API keeps each key for 24 hours, separately for each organization and each mode: a test key and a live key never share a key's answer.
| You send | The API answers |
|---|---|
| A new key | Runs the request and keeps its answer. |
| The same key and the same request, after it finished | The kept answer, with the same status and body, and the headers Idempotent-Replayed: true and Original-Request: req_… (the first request's ID). Nothing runs again. |
| The same key with another request (another path, query string or body) | 400, idempotency_error, idempotency_key_reused. Nothing runs. |
| The same key while the first request still runs | 409, idempotency_error, idempotency_in_progress. Try again in a moment. |
"The same request" means the same method, path, query string and body. The body is compared as the API read it, not byte for byte: the order of its fields and its spacing do not matter, and a field you leave out counts like the same field sent with its default value. Headers are not compared.
What is kept, and what is not
- A result is kept, including a refusal on the merits: a payment refused with
amount_too_smallanswersamount_too_smallagain for the same key. Fix the request and send it with a new key. - A replay is the first answer, byte for byte. A replayed error still carries the first request's
request_id, the oneOriginal-Requestnames; the replay's own ID is in itsRequest-Idheader. - A request refused before it ran is not kept: a bad key, a rate limit (
429), or a parameter missing or invalid. Correct it and send it again with the same key. - An answer that says "try again" is not kept: a
5xx, a429, any answer with aRetry-Afterheader, and a refusal whose code ends in_busy. The same key runs the request again. - A secret shown once is not kept. The replay of a webhook endpoint's creation answers the same
endpoint with
"secret": null: store the secret from the first answer. - An answer over 64 KB is not kept: the same key would run the request again. The answers of the requests that move money are far smaller.
# 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_01k7d7p9a2s5d8f1g4h7j0k3hmRetry safely
Retry when the answer never came (a timeout, a dropped connection), on 409 idempotency_in_progress,
on 429 after the Retry-After delay, and on a 5xx. Wait longer between tries, and keep the
same key every time. Do not retry another 4xx: it will fail the same way. Do not sleep on a
Retry-After of minutes or hours either: it is the daily cap of test mode, which lasts until
midnight. See Rate limits.
# --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;
}
}On the dashboard
The dashboard's own refund and cash-payment forms send a key for you: pressing the button twice
makes one refund. Every request you send with a key shows it in the dashboard's request logs, next to
its Request-Id.