Skip to content

Rate Limit

Overview

The Appmax API enforces request limits at two levels to ensure stability and availability for all integrators. Your request must pass both levels to be processed.

Control levels

Level 1 — Per-credential limit

Applied per merchant client_id. Controls request rate and total volume.

MetricLimitDescription
Burst50 requestsMaximum simultaneous requests (instant peak)
Rate5 requests/secondSustained request rate
Monthly quota100,000 requests/monthTotal requests per month (resets on the 1st)

Level 2 — Per-route limit

Applied per email + source IP. Controls individual route usage.

Route typeLimitWindow
Transactional routes (default)60 requests1 minute
Sensitive operations (login, credentials)5 requests1 minute

TIP

In practice, you can burst up to 50 requests instantly, then maintain a sustained rate of 5 requests per second without being throttled. If the limit is exceeded, the request is rejected with 429.

Monthly quota

In addition to the per-second rate limit, there is a monthly limit of 100,000 requests per client_id:

  • The counter is incremented on each request
  • Automatically resets on the first day of each month

WARNING

When the monthly quota is exceeded, all requests are blocked with 429 until the next month.

INFO

These limits exist to ensure platform security and stability, but they are fully flexible. If your integration needs higher limits, don't hesitate to reach out to our team — we'll adjust them to fit your needs.

Rate limit response

When the limit is exceeded, the API returns HTTP 429 status with informational headers:

http
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Retry-After: 45
json
{
  "message": "Too many requests",
  "retryAfter": "45(s)"
}
HeaderDescription
X-RateLimit-LimitMaximum number of requests allowed in the window
X-RateLimit-RemainingRequests remaining in the current window
Retry-AfterSeconds until you can retry

Best practices

  1. Implement retry with exponential backoff. When receiving 429, wait for the time indicated in the Retry-After header. If no header is present, use exponential backoff (1s, 2s, 4s, 8s...).

  2. Use queues for batch operations. If you need to create many orders or customers, queue the requests and process them respecting the 5 req/s rate.

  3. Cache the Bearer token. The token lasts 1 hour. Reuse it instead of generating a new one per request — authentication also consumes quota.

  4. Monitor rate limit headers. Use X-RateLimit-Remaining to adjust your speed before hitting the limit.

  5. Group operations when possible. Prefer creating customer + order in quick sequence rather than multiple distributed calls.

Retry with backoff example

go
func requestWithRetry(client *http.Client, req *http.Request) (*http.Response, error) {
	maxRetries := 3

	for attempt := 0; attempt <= maxRetries; attempt++ {
		resp, err := client.Do(req)
		if err != nil {
			return nil, err
		}

		if resp.StatusCode != http.StatusTooManyRequests {
			return resp, nil
		}
		resp.Body.Close()

		retryAfter := resp.Header.Get("Retry-After")
		wait, _ := strconv.Atoi(retryAfter)
		if wait == 0 {
			wait = 1 << attempt // backoff: 1s, 2s, 4s
		}

		log.Printf("Rate limited, retrying in %ds (attempt %d/%d)", wait, attempt+1, maxRetries)
		time.Sleep(time.Duration(wait) * time.Second)
	}

	return nil, fmt.Errorf("rate limit exceeded after %d retries", maxRetries)
}
javascript
async function requestWithRetry(url, options, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    const retryAfter = response.headers.get('Retry-After');
    const wait = retryAfter ? parseInt(retryAfter) : Math.pow(2, attempt);

    console.log(`Rate limited, retrying in ${wait}s (attempt ${attempt + 1}/${maxRetries})`);
    await new Promise(resolve => setTimeout(resolve, wait * 1000));
  }

  throw new Error(`Rate limit exceeded after ${maxRetries} retries`);
}
python
import time
import requests

def request_with_retry(method, url, max_retries=3, **kwargs):
    for attempt in range(max_retries + 1):
        response = requests.request(method, url, **kwargs)

        if response.status_code != 429:
            return response

        retry_after = response.headers.get('Retry-After')
        wait = int(retry_after) if retry_after else 2 ** attempt

        print(f"Rate limited, retrying in {wait}s (attempt {attempt + 1}/{max_retries})")
        time.sleep(wait)

    raise Exception(f"Rate limit exceeded after {max_retries} retries")
php
function requestWithRetry(string $method, string $url, array $options, int $maxRetries = 3): Response
{
    $client = new \GuzzleHttp\Client();

    for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
        $response = $client->request($method, $url, $options + [
            'http_errors' => false,
        ]);

        if ($response->getStatusCode() !== 429) {
            return $response;
        }

        $retryAfter = $response->getHeader('Retry-After')[0] ?? null;
        $wait = $retryAfter ? (int) $retryAfter : pow(2, $attempt);

        Log::warning("Rate limited, retrying in {$wait}s (attempt " . ($attempt + 1) . "/{$maxRetries})");
        sleep($wait);
    }

    throw new \Exception("Rate limit exceeded after {$maxRetries} retries");
}