مرجع الـ API

عدم التكرار

أرسل طلبًا من جديد دون أن يُنفَّذ مرتين. أي الطلبات تحتاج إلى Idempotency-Key، وما تجيب به واجهة API عند التكرار.

فتح بصيغة Markdown
في هذه الصفحة
  1. أرسل مفتاحًا
  2. بماذا يُجاب التكرار
  3. أعد المحاولة بأمان
  4. في لوحة التحكم

قد تتعطل الشبكة بعد أن تنجز واجهة API العمل وقبل أن تقرأ الاستجابة. هل أُنشئت الدفعة؟ مع مفتاح عدم التكرار (idempotency key) لا تحتاج إلى معرفة ذلك: أرسل الطلب نفسه من جديد بالمفتاح نفسه، فتجيب الواجهة بما أجابت به أول مرة، دون أن تنجز العمل مرتين.

أرسل مفتاحًا

أضف ترويسة Idempotency-Key إلى طلب POST. المفتاح من اختيارك: حتى 255 حرفًا من أحرف ASCII القابلة للطباعة، فريد لكل عملية تريد إجراءها مرة واحدة. يصلح UUID عشوائي؛ ويصلح كذلك مفتاح مبني من مراجعك الخاصة، مثل order-1042-create. يُرفض المفتاح الأطول من ذلك، أو الذي يحمل أحرفًا أخرى، بـ 400، parameter_invalid.

أنشئ المفتاح قبل المحاولة الأولى واحتفظ به لإعادات المحاولة في تلك العملية. المفتاح الجديد يعني عملية جديدة.

إلزامي في الطلبات التي تحرّك أموالًا:

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

من دونه تجيب بـ 400، idempotency_key_missing. يقبل كل طلب POST آخر مفتاحًا ولا يشترطه. أما GET وDELETE فيتجاهلانه: تكرارهما كما هما آمن.

  • 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(())
}

بماذا يُجاب التكرار

تحتفظ واجهة 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 لا تُحفظ: سينفّذ المفتاح نفسه الطلب من جديد. واستجابات الطلبات التي تحرّك أموالًا أصغر من ذلك بكثير.
  • 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(())
}
الاستجابة
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 قيمته دقائق أو ساعات: إنه السقف اليومي لوضع الاختبار، ويستمر حتى منتصف الليل. انظر حدود المعدل.

  • 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;
    }
}

في لوحة التحكم

نموذجا الاسترداد والدفع نقدًا في لوحة التحكم يرسلان مفتاحًا عنك: الضغط على الزر مرتين ينفّذ استردادًا واحدًا. وكل طلب ترسله بمفتاح يظهر مفتاحه في سجلات الطلبات في لوحة التحكم، بجانب Request-Id الخاص به.