# حدود المعدل

فئات الطلبات في مؤسستك، وترويسات RateLimit، ومعالجة 429، وكيف تطلب حدًا أعلى.

تحدّ واجهة API من عدد الطلبات التي ترسلها مؤسستك في الدقيقة، حتى لا تُبطئ ذروة تكامل واحد
التكاملات الأخرى. تُحتسب الحدود **لكل مؤسسة ولكل وضع**: تتشاركها كل مفاتيح
مؤسستك، ولوضع الاختبار حدوده الخاصة. استخدام لوحة التحكم لا يستهلكها، ولا
إشعارات الـ webhook التي نرسلها إليك.

## الفئات

يُحتسب كل طلب في **فئة** واحدة أو أكثر. يجب أن يجد متسعًا في كل فئة ينتمي إليها؛
والطلب الذي ترفضه فئة لا يستهلك شيئًا من الفئات الأخرى.

| الفئة          | ما تشمله                                                 | المباشر، في الدقيقة | الاختبار، في الدقيقة |
| -------------- | -------------------------------------------------------- | ------------------- | -------------------- |
| كل الطلبات     | كل طلب إلى API                                           | 1,200               | 600                  |
| عمليات الكتابة | كل طلب `POST` و`PUT` و`DELETE`                           | 300                 | 150                  |
| إنشاء الدفعات  | `POST /v1/payment_intents` و`POST /v1/checkout/sessions` | 120                 | 60                   |
| البحث والتصدير | عمليات البحث في القوائم والتصدير                         | 30                  | 30                   |

تمتلئ الفئة من جديد بمعدلها في الدقيقة، وتحتفظ فوق ذلك بما يعادل **10 ثوانٍ** للذروات القصيرة،
مقرَّبًا إلى طلب كامل. حدّ 120 في الدقيقة يعيد تعبئة طلبين في الثانية ويسمح بمرور 140 طلبًا
دفعة واحدة على الأكثر.

هذه هي القيم الافتراضية؛ وقد تختلف حدودك. تعرض **المطوّرون ← حدود API** القيم المطبّقة على
مؤسستك.

## اقرأ الترويسات

بعد قبول مفتاحك، تحمل كل استجابة ثلاث ترويسات عن أضيق فئة
للطلب، بما فيها `429`:

* `RateLimit-Limit`: حد الفئة في الدقيقة؛
* `RateLimit-Remaining`: الطلبات التي ما زلت تستطيع إرسالها الآن. تحتفظ الفئة الممتلئة باحتياطيها
  فوق حدها، فقد يكون هذا الرقم أعلى من `RateLimit-Limit`: حتى 699 لحد قدره 600؛
* `RateLimit-Reset`: الثواني المتبقية حتى تمتلئ الفئة من جديد.

الطلب المرفوض قبل احتسابه لا يحمل أيًّا منها: مفتاح غائب أو مرفوض أو
لا يحق له إجراء الطلب، أو إصدار API غير موجود، أو عنوان لا تعرفه واجهة API.

اقرأ `RateLimit-Remaining` لتبطئ قبل بلوغ الحد، لا بعده. وأثناء
حادث من جهتنا قد يكون العدّ أقل انتظامًا للحظات: اعتبر الترويسات دليلًا،
و`429` هي القاعدة.

```bash
curl -i https://pay-api.adpromax.com/v1/account \
  -u "$APX_SECRET_KEY:"
```

```js title="headers.mjs"
const response = await fetch('https://pay-api.adpromax.com/v1/account', {
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const names = ['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset']
for (const name of names)
  console.log(`${name}: ${response.headers.get(name)}`)
```

```ts title="headers.ts"
const response: Response = await fetch('https://pay-api.adpromax.com/v1/account', {
  headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const names = ['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset']
for (const name of names) {
  const value: string | null = response.headers.get(name)
  console.log(`${name}: ${value}`)
}
```

```python title="headers.py"
import os

import requests

response = requests.get(
    "https://pay-api.adpromax.com/v1/account",
    auth=(os.environ["APX_SECRET_KEY"], ""),
)
for name in ("RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset"):
    print(f"{name}: {response.headers.get(name)}")
```

```php title="headers.php"
<?php
$headers = [];
$ch = curl_init('https://pay-api.adpromax.com/v1/account');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    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);
foreach (['RateLimit-Limit', 'RateLimit-Remaining', 'RateLimit-Reset'] as $name) {
    echo $name, ': ', $headers[strtolower($name)] ?? '', PHP_EOL;
}
```

```ruby title="headers.rb"
require 'net/http'

uri = URI('https://pay-api.adpromax.com/v1/account')
request = Net::HTTP::Get.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
%w[RateLimit-Limit RateLimit-Remaining RateLimit-Reset].each do |name|
  puts "#{name}: #{response[name]}"
end
```

```go title="headers.go"
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://pay-api.adpromax.com/v1/account", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")

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

	names := []string{"RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset"}
	for _, name := range names {
		fmt.Printf("%s: %s\n", name, res.Header.Get(name))
	}
}
```

```java title="Headers.java"
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 Headers {
    public static void main(String[] args) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://pay-api.adpromax.com/v1/account"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .GET()
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());

        List<String> names = List.of(
            "RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset");
        for (String name : names) {
            System.out.println(name + ": "
                + response.headers().firstValue(name).orElse(""));
        }
    }
}
```

```csharp title="Headers.cs"
using System.Net.Http.Headers;

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

using var response = await http.GetAsync("https://pay-api.adpromax.com/v1/account");
string[] names = ["RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset"];
foreach (var name in names)
{
    response.Headers.TryGetValues(name, out var values);
    Console.WriteLine($"{name}: {values?.FirstOrDefault()}");
}
```

```rust title="main.rs"
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking"] }
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::blocking::Client::new()
        .get("https://pay-api.adpromax.com/v1/account")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .send()?;

    for name in ["RateLimit-Limit", "RateLimit-Remaining", "RateLimit-Reset"] {
        let value = response.headers().get(name).and_then(|v| v.to_str().ok());
        println!("{name}: {}", value.unwrap_or(""));
    }
    Ok(())
}
```

```text response
HTTP/1.1 200 OK
Content-Type: application/json
Request-Id: req_01k7e6a2b5c8d1f4g7h0j3k6mx
RateLimit-Limit: 600
RateLimit-Remaining: 699
RateLimit-Reset: 1
```

## عند بلوغ حد

تجيب واجهة API بـ `429` مع `rate_limit_error` رمزه `rate_limit_exceeded`. تذكر الرسالة
الفئة، وحدّ هذه الفئة، ومدة الانتظار، وتعطي ترويسة `Retry-After` هذه المدة بالثواني.
لم يُنفَّذ شيء: يمكن إرسال الطلب من جديد كما هو.

```json title="429 Too Many Requests" response
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Too many payment creation requests: the limit is 60 per minute in test mode. Try again in 1 second.",
    "doc_url": "https://pay-docs.adpromax.com/errors#rate_limit_exceeded",
    "request_id": "req_01k7e6c9d3f6g9h2j5k8m1n4pq"
  }
}
```

## السقف اليومي في وضع الاختبار

إضافة إلى حدود المعدل، لوضع الاختبار سقف يومي لكل مؤسسة: **10,000 طلب** و
**1,000 عملية إنشاء دفعة** في اليوم، تُحتسب بتوقيت المغرب. بعد تجاوزه، تجيب واجهة API بـ `429` مع
الرمز `test_daily_cap_reached` حتى منتصف الليل بتوقيت المغرب. لا سقف يوميًا للوضع المباشر.

يحمي السقف بيئة الاختبار المشتركة. قلّما تبلغه مجموعة اختبارات آلية؛ أما اختبار الحمل فيبلغه:
شغّل اختبارات الحمل على كودك الخاص، لا على بيئة الاختبار.

```json title="429 Too Many Requests" response
{
  "error": {
    "type": "rate_limit_error",
    "code": "test_daily_cap_reached",
    "message": "Test mode's daily cap of 1,000 payment creations is reached. It resets at midnight, Morocco time.",
    "doc_url": "https://pay-docs.adpromax.com/errors#test_daily_cap_reached",
    "request_id": "req_01k7e6f5g8h1j4k7m0n3p6q9rs"
  }
}
```

## عالج 429

1. **انتظر** عدد الثواني المحدد في `Retry-After`، مع قليل من العشوائية، حتى لا تعيد عدة
   خوادم المحاولة في اللحظة نفسها. حد المعدل يطلب ثوانيَ قليلة على الأكثر. أما انتظار
   دقائق أو ساعات فهو [السقف اليومي](#السقف-اليومي-في-وضع-الاختبار)، ويستمر حتى منتصف الليل
   بتوقيت المغرب: لا تنتظره. توقف، وأبلغ عن الخطأ.
2. **أرسل الطلب نفسه من جديد**، بقيمة `Idempotency-Key` نفسها. الرفض بسبب حد المعدل
   لا يُحفظ مع المفتاح، فتُنفَّذ إعادة المحاولة، وتبقى العملية تحدث مرة واحدة. انظر
   [عدم التكرار](https://pay-docs.adpromax.com/ar/api-reference/idempotency) (idempotency).
3. **توقف بعد بضع محاولات**، وسجّل `request_id`.

للبقاء دون الحدود:

* وزّع المهام المجمّعة على الوقت بدل إرسالها دفعة واحدة؛
* راقب `RateLimit-Remaining`؛
* اعرف أخبار المدفوعات من [إشعارات webhook](https://pay-docs.adpromax.com/ar/webhooks) بدل قراءتها مرة بعد مرة.

```bash
# --retry sends it again after a 429, waiting as long as Retry-After says;
# --retry-max-time gives up when that wait is longer (test mode's daily cap)
curl https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  --retry 5 \
  --retry-max-time 60 \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1046-create" \
  -d '{"amount": 25000, "currency": "mad",
       "payment_method_types": ["card"]}'
```

```js title="retry-429.mjs"
const api = 'https://pay-api.adpromax.com/v1'
const LONGEST_WAIT = 60 // seconds

// Sends a request again after a 429, waiting as long as Retry-After says.
async function send(path, init, attempts = 5) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`${api}${path}`, init)
    if (response.status !== 429 || attempt === attempts)
      return response
    const wait = Number(response.headers.get('Retry-After')) || 1
    if (wait > LONGEST_WAIT) // test mode's daily cap, until midnight
      return response
    const jitter = Math.random() * 500
    await new Promise(resolve => setTimeout(resolve, wait * 1000 + jitter))
  }
}

const response = await send('/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    // The same key on every try: the payment is created once.
    'Idempotency-Key': 'order-1046-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card'],
  }),
})
console.log(response.status, response.headers.get('RateLimit-Remaining'))
```

```ts title="retry-429.ts"
const api = 'https://pay-api.adpromax.com/v1'
const LONGEST_WAIT = 60 // seconds

// Sends a request again after a 429, waiting as long as Retry-After says.
async function send(
  path: string,
  init: RequestInit,
  attempts = 5,
): Promise<Response> {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`${api}${path}`, init)
    if (response.status !== 429 || attempt === attempts)
      return response
    const wait = Number(response.headers.get('Retry-After')) || 1
    if (wait > LONGEST_WAIT) // test mode's daily cap, until midnight
      return response
    const jitter = Math.random() * 500
    await new Promise(resolve => setTimeout(resolve, wait * 1000 + jitter))
  }
}

const response = await send('/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    // The same key on every try: the payment is created once.
    'Idempotency-Key': 'order-1046-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card'],
  }),
})
console.log(response.status, response.headers.get('RateLimit-Remaining'))
```

```python title="retry_429.py"
import os
import random
import time

import requests

API = "https://pay-api.adpromax.com/v1"
AUTH = (os.environ["APX_SECRET_KEY"], "")
LONGEST_WAIT = 60  # seconds

# Sends a request again after a 429, waiting as long as Retry-After says.
def send(path, body, key, attempts=5):
    for attempt in range(1, attempts + 1):
        response = requests.post(
            f"{API}{path}", auth=AUTH, json=body,
            # The same key on every try: the payment is created once.
            headers={"Idempotency-Key": key},
        )
        if response.status_code != 429 or attempt == attempts:
            return response
        wait = int(response.headers.get("Retry-After", 1))
        if wait > LONGEST_WAIT:  # test mode's daily cap, until midnight
            return response
        time.sleep(wait + random.random() / 2)

response = send("/payment_intents", {
    "amount": 25000,
    "currency": "mad",
    "payment_method_types": ["card"],
}, "order-1046-create")
print(response.status_code, response.headers.get("RateLimit-Remaining"))
```

```php title="retry_429.php"
<?php
const LONGEST_WAIT = 60; // seconds

// Sends a request again after a 429, waiting as long as Retry-After says.
function send(string $path, array $body, string $key, int $attempts = 5): array
{
    for ($attempt = 1; ; $attempt++) {
        $retryAfter = 1;
        $ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
        curl_setopt_array($ch, [
            CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
            CURLOPT_POST => true,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                // The same key on every try: the payment is created once.
                'Idempotency-Key: ' . $key,
            ],
            CURLOPT_POSTFIELDS => json_encode($body),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$retryAfter) {
                if (stripos($line, 'Retry-After:') === 0) {
                    $retryAfter = (int) trim(substr($line, 12));
                }
                return strlen($line);
            },
        ]);
        $answer = json_decode(curl_exec($ch), true);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        if ($status !== 429 || $attempt === $attempts) {
            return [$status, $answer];
        }
        if ($retryAfter > LONGEST_WAIT) { // test mode's daily cap, until midnight
            return [$status, $answer];
        }
        usleep(($retryAfter * 1000 + random_int(0, 500)) * 1000);
    }
}

[$status, $intent] = send('/payment_intents', [
    'amount' => 25000,
    'currency' => 'mad',
    'payment_method_types' => ['card'],
], 'order-1046-create');
echo $status, PHP_EOL;
```

```ruby title="retry_429.rb"
require 'json'
require 'net/http'

API = 'https://pay-api.adpromax.com/v1'.freeze
LONGEST_WAIT = 60 # seconds

# Sends a request again after a 429, waiting as long as Retry-After says.
def send_again(path, body, key, attempts = 5)
  uri = URI("#{API}#{path}")
  request = Net::HTTP::Post.new(uri)
  request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
  request['Content-Type'] = 'application/json'
  # The same key on every try: the payment is created once.
  request['Idempotency-Key'] = key
  request.body = JSON.generate(body)

  1.upto(attempts) do |attempt|
    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
      http.request(request)
    end
    return response if response.code != '429' || attempt == attempts

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

    sleep(wait + rand / 2)
  end
end

response = send_again('/payment_intents', {
  amount: 25000,
  currency: 'mad',
  payment_method_types: ['card']
}, 'order-1046-create')
puts "#{response.code} #{response['RateLimit-Remaining']}"
```

```go title="retry_429.go"
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"os"
	"strconv"
	"time"
)

const api = "https://pay-api.adpromax.com/v1"

const longestWait = 60 // seconds

// send posts a request again after a 429, waiting as long as Retry-After says.
func send(
	path string, body any, key string, attempts int,
) (*http.Response, error) {
	payload, err := json.Marshal(body)
	if err != nil {
		return nil, err
	}
	for attempt := 1; ; attempt++ {
		req, err := http.NewRequest(http.MethodPost, api+path,
			bytes.NewReader(payload))
		if err != nil {
			return nil, err
		}
		req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
		req.Header.Set("Content-Type", "application/json")
		// The same key on every try: the payment is created once.
		req.Header.Set("Idempotency-Key", key)

		res, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, err
		}
		if res.StatusCode != http.StatusTooManyRequests || attempt == attempts {
			return res, nil
		}
		wait, err := strconv.Atoi(res.Header.Get("Retry-After"))
		if err != nil {
			wait = 1
		}
		if wait > longestWait { // test mode's daily cap, until midnight
			return res, nil
		}
		res.Body.Close()
		jitter := time.Duration(rand.Intn(500)) * time.Millisecond
		time.Sleep(time.Duration(wait)*time.Second + jitter)
	}
}

func main() {
	res, err := send("/payment_intents", map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card"},
	}, "order-1046-create", 5)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()
	fmt.Println(res.StatusCode, res.Header.Get("RateLimit-Remaining"))
}
```

```java title="Retry429.java"
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.ThreadLocalRandom;

public class Retry429 {
    static final String API = "https://pay-api.adpromax.com/v1";
    static final HttpClient HTTP = HttpClient.newHttpClient();
    static final long LONGEST_WAIT = 60; // seconds

    // Sends a request again after a 429, waiting as long as Retry-After says.
    static HttpResponse<String> send(
            String path, String body, String key, int attempts) throws Exception {
        HttpRequest request = HttpRequest.newBuilder(URI.create(API + path))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            // The same key on every try: the payment is created once.
            .header("Idempotency-Key", key)
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        for (int attempt = 1; ; attempt++) {
            HttpResponse<String> response =
                HTTP.send(request, HttpResponse.BodyHandlers.ofString());
            if (response.statusCode() != 429 || attempt == attempts) {
                return response;
            }
            long wait = response.headers()
                .firstValueAsLong("Retry-After").orElse(1);
            if (wait > LONGEST_WAIT) { // test mode's daily cap, until midnight
                return response;
            }
            long jitter = ThreadLocalRandom.current().nextLong(500);
            Thread.sleep(wait * 1000 + jitter);
        }
    }

    public static void main(String[] args) throws Exception {
        HttpResponse<String> response = send("/payment_intents", """
            {
              "amount": 25000,
              "currency": "mad",
              "payment_method_types": ["card"]
            }
            """, "order-1046-create", 5);
        System.out.println(response.statusCode() + " "
            + response.headers().firstValue("RateLimit-Remaining").orElse(""));
    }
}
```

```csharp title="Retry429.cs"
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;

const string Api = "https://pay-api.adpromax.com/v1";
var longestWait = TimeSpan.FromSeconds(60);
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

// Sends a request again after a 429, waiting as long as Retry-After says.
async Task<HttpResponseMessage> Send(
    string path, object body, string key, int attempts = 5)
{
    for (var attempt = 1; ; attempt++)
    {
        using var request = new HttpRequestMessage(HttpMethod.Post, Api + path);
        // The same key on every try: the payment is created once.
        request.Headers.Add("Idempotency-Key", key);
        request.Content = JsonContent.Create(body);

        var answer = await http.SendAsync(request);
        var limited = answer.StatusCode == HttpStatusCode.TooManyRequests;
        if (!limited || attempt == attempts)
            return answer;
        var wait = answer.Headers.RetryAfter?.Delta
            ?? TimeSpan.FromSeconds(1);
        if (wait > longestWait) // test mode's daily cap, until midnight
            return answer;
        var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(500));
        await Task.Delay(wait + jitter);
    }
}

using var response = await Send("/payment_intents", new
{
    amount = 25000,
    currency = "mad",
    payment_method_types = new[] { "card" },
}, "order-1046-create");
response.Headers.TryGetValues("RateLimit-Remaining", out var remaining);
Console.WriteLine($"{(int)response.StatusCode} {remaining?.FirstOrDefault()}");
```

```rust title="main.rs"
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde_json = "1"
//             rand = "0.9"
use std::{thread::sleep, time::Duration};

use reqwest::blocking::{Client, Response};
use serde_json::{json, Value};

const API: &str = "https://pay-api.adpromax.com/v1";
const LONGEST_WAIT: u64 = 60; // seconds

// Sends a request again after a 429, waiting as long as Retry-After says.
fn send(
    path: &str,
    body: &Value,
    key: &str,
    attempts: u32,
) -> reqwest::Result<Response> {
    let client = Client::new();
    let secret = std::env::var("APX_SECRET_KEY").unwrap_or_default();
    let mut attempt = 1;
    loop {
        let response = client
            .post(format!("{API}{path}"))
            .bearer_auth(&secret)
            // The same key on every try: the payment is created once.
            .header("Idempotency-Key", key)
            .json(body)
            .send()?;
        if response.status().as_u16() != 429 || attempt == attempts {
            return Ok(response);
        }
        let wait: u64 = response
            .headers()
            .get("Retry-After")
            .and_then(|value| value.to_str().ok()?.parse().ok())
            .unwrap_or(1);
        if wait > LONGEST_WAIT {
            // test mode's daily cap, until midnight
            return Ok(response);
        }
        let jitter: u64 = rand::random_range(0..500);
        sleep(Duration::from_millis(wait * 1000 + jitter));
        attempt += 1;
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = send(
        "/payment_intents",
        &json!({
            "amount": 25000,
            "currency": "mad",
            "payment_method_types": ["card"]
        }),
        "order-1046-create",
        5,
    )?;
    let remaining = response
        .headers()
        .get("RateLimit-Remaining")
        .and_then(|value| value.to_str().ok())
        .unwrap_or("");
    println!("{} {}", response.status().as_u16(), remaining);
    Ok(())
}
```

## اطّلع على حدودك

تعرض **المطوّرون ← حدود API**، في لوحة التحكم، لكل فئة:

* حدها في الدقيقة واحتياطيها؛
* مصدر الحد: *القيمة العامة*، أو *الخطة*، أو *مخصّص حتى …* عندما نضع لك حدًا؛
* ذروة استخدامك وعدد إجابات `429` خلال آخر 24 ساعة.

في وضع الاختبار تعرض الصفحة أيضًا ما استهلكته من السقف اليومي.

## اطلب حدًا أعلى

عندما تزداد حركتك، أو قبل حملة، اطلب المزيد من
**المطوّرون ← حدود API ← طلب حد أعلى**:

1. الفئة، والوضع: المباشر أو الاختبار أو كلاهما؛
2. القيمة التي تحتاجها: ضعف الحد الحالي أو خمسة أضعافه، أو رقم تحدده أنت؛
3. إلى متى: تاريخ، أو بلا نهاية؛
4. السبب، مثل «حملة رمضان، نحو 3× الحركة المعتادة»؛
5. عنوان بريد إلكتروني لتلقي ردّنا.

ينظر فريقنا في استخدامك وحسابك، ثم يوافق أو يرفض. الزيادة الكبيرة تحتاج إلى موافقة
شخص ثانٍ من جهتنا، وقد يستغرق ذلك وقتًا أطول قليلًا. يصلك القرار بالبريد الإلكتروني
ويسري خلال ثوانٍ؛ ويصبح مصدر الفئة عندئذ *مخصّص*. يمكن أن يكون لك طلب مفتوح واحد لكل
فئة ووضع، وأن تلغيه ما دام مفتوحًا.

يستطيع **المالك** و**المسؤولون** و**المطوّرون** في مؤسستك تقديم الطلب. قدّمه
قبل الحملة بأيام لا في يومها.

قد نخفض حدًا أيضًا عندما يضر تكامل بالمنصة. ونخبرك بالبريد الإلكتروني عندما نفعل.

## حدود أخرى

هذه الحدود تحمي المنصة ولا تتغير من مؤسسة إلى أخرى:

* تسجيل الدخول والتسجيل وصفحة الدفع المستضافة وصفحة القسيمة محدودة لكل زائر؛
* تملك المؤسسة 16 نقطة استقبال webhook على الأكثر في كل وضع؛
* خمس عمليات تسليم webhook على الأكثر تكون جارية نحو نقطة الاستقبال الواحدة في الوقت نفسه؛
* تعيد القائمة 100 كائن على الأكثر في الطلب الواحد (`limit`)؛
* لا تتجاوز `Idempotency-Key` 255 حرفًا.
