مرجع الـ API
عدم التكرار
أرسل طلبًا من جديد دون أن يُنفَّذ مرتين. أي الطلبات تحتاج إلى Idempotency-Key، وما تجيب به واجهة API عند التكرار.
قد تتعطل الشبكة بعد أن تنجز واجهة API العمل وقبل أن تقرأ الاستجابة. هل أُنشئت الدفعة؟ مع مفتاح عدم التكرار (idempotency key) لا تحتاج إلى معرفة ذلك: أرسل الطلب نفسه من جديد بالمفتاح نفسه، فتجيب الواجهة بما أجابت به أول مرة، دون أن تنجز العمل مرتين.
أرسل مفتاحًا
أضف ترويسة Idempotency-Key إلى طلب POST. المفتاح من اختيارك: حتى 255 حرفًا من أحرف ASCII القابلة
للطباعة، فريد لكل عملية تريد إجراءها مرة واحدة. يصلح UUID عشوائي؛ ويصلح كذلك مفتاح مبني
من مراجعك الخاصة، مثل order-1042-create. يُرفض المفتاح الأطول من ذلك، أو الذي يحمل أحرفًا
أخرى، بـ 400، parameter_invalid.
أنشئ المفتاح قبل المحاولة الأولى واحتفظ به لإعادات المحاولة في تلك العملية. المفتاح الجديد يعني عملية جديدة.
إلزامي في الطلبات التي تحرّك أموالًا:
POST /v1/payment_intentsPOST /v1/payment_intents/{id}/confirmPOST /v1/checkout/sessionsPOST /v1/refunds
من دونه تجيب بـ 400، idempotency_key_missing. يقبل كل طلب POST آخر مفتاحًا ولا
يشترطه. أما GET وDELETE فيتجاهلانه: تكرارهما كما هما آمن.
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(())
}بماذا يُجاب التكرار
تحتفظ واجهة API بكل مفتاح مدة 24 ساعة، منفصلًا لكل مؤسسة ولكل وضع: لا يتشارك مفتاح API للاختبار ومفتاح API للوضع المباشر استجابة مفتاح عدم تكرار واحد أبدًا.
| ما ترسله | ما تجيب به واجهة API |
|---|---|
| مفتاح جديد | تنفّذ الطلب وتحتفظ باستجابته. |
| المفتاح نفسه والطلب نفسه، بعد اكتماله | الاستجابة المحفوظة، بالحالة والنص نفسيهما، مع الترويستين Idempotent-Replayed: true وOriginal-Request: req_… (معرّف الطلب الأول). لا يُنفَّذ شيء من جديد. |
| المفتاح نفسه مع طلب آخر (مسار أو سلسلة استعلام أو نص آخر) | 400، idempotency_error، idempotency_key_reused. لا يُنفَّذ شيء. |
| المفتاح نفسه والطلب الأول ما زال قيد التنفيذ | 409، idempotency_error، idempotency_in_progress. حاول من جديد بعد لحظة. |
«الطلب نفسه» يعني الطريقة والمسار وسلسلة الاستعلام والنص أنفسها. يُقارَن النص كما قرأته واجهة API، لا بايتًا ببايت: لا يهم ترتيب حقوله ولا مسافاته، والحقل الذي تتركه يُحتسب كالحقل نفسه مرسلًا بقيمته الافتراضية. لا تُقارَن الترويسات.
ما يُحفظ وما لا يُحفظ
- النتيجة تُحفظ، بما فيها الرفض لسبب في مضمون الطلب: الدفعة المرفوضة بالرمز
amount_too_smallتجيب بـamount_too_smallمن جديد للمفتاح نفسه. صحّح الطلب وأرسله بمفتاح جديد. - الرد المعاد هو الاستجابة الأولى، بايتًا ببايت. يحمل الخطأ المعاد
request_idالخاص بالطلب الأول، وهو الذي تذكرهOriginal-Request؛ أما معرّف الطلب المعاد نفسه فيوجد في ترويستهRequest-Id. - الطلب المرفوض قبل تنفيذه لا يُحفظ: مفتاح API خاطئ، أو حد معدل (
429)، أو معامل ناقص أو غير صالح. صحّحه وأرسله من جديد بالمفتاح نفسه. - الاستجابة التي تقول «حاول من جديد» لا تُحفظ:
5xx، و429، وأي استجابة فيها ترويسةRetry-After، وأي رفض ينتهي رمزه بـ_busy. ينفّذ المفتاح نفسه الطلب من جديد. - السرّ الذي يُعرض مرة واحدة لا يُحفظ. إعادة طلب إنشاء نقطة استقبال webhook تجيب بنقطة الاستقبال
نفسها مع
"secret": null: احفظ السرّ من الاستجابة الأولى. - الاستجابة التي تتجاوز 64 KB لا تُحفظ: سينفّذ المفتاح نفسه الطلب من جديد. واستجابات الطلبات التي تحرّك أموالًا أصغر من ذلك بكثير.
# 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_01k7d7p9a2s5d8f1g4h7j0k3hmأعد المحاولة بأمان
أعد المحاولة عندما لا تصل الاستجابة أبدًا (انتهاء مهلة، أو انقطاع اتصال)، وعند 409 idempotency_in_progress،
وعند 429 بعد مهلة Retry-After، وعند 5xx. انتظر مدة أطول بين المحاولات، واحتفظ
بالمفتاح نفسه في كل مرة. لا تعد المحاولة عند أي 4xx آخر: سيفشل بالطريقة نفسها. ولا تنتظر كذلك
Retry-After قيمته دقائق أو ساعات: إنه السقف اليومي لوضع الاختبار، ويستمر حتى
منتصف الليل. انظر حدود المعدل.
# --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;
}
}في لوحة التحكم
نموذجا الاسترداد والدفع نقدًا في لوحة التحكم يرسلان مفتاحًا عنك: الضغط على الزر مرتين
ينفّذ استردادًا واحدًا. وكل طلب ترسله بمفتاح يظهر مفتاحه في سجلات الطلبات في لوحة التحكم، بجانب
Request-Id الخاص به.