Rate limits
Requests are counted per client (each API key, each partner application) in fixed windows of one minute.
| Client | Limit |
|---|---|
| API key | 600 requests per minute |
| Partner application | Set per application (600 by default, adjustable on request) |
Every response to an API key or a partner application carries the state of the current window (IETF RateLimit headers):
HTTP/1.1 200 OKRateLimit-Limit: 600RateLimit-Remaining: 587RateLimit-Reset: 42| Header | Meaning |
|---|---|
RateLimit-Limit |
Requests allowed in the window. |
RateLimit-Remaining |
Requests left in the current window. |
RateLimit-Reset |
Seconds until the window resets. |
When the limit is exceeded
Section titled “When the limit is exceeded”The API answers 429 Too Many Requests with the rate_limited code and a Retry-After header (seconds):
HTTP/1.1 429 Too Many RequestsRetry-After: 17RateLimit-Limit: 600RateLimit-Remaining: 0RateLimit-Reset: 17
{ "error": { "code": "rate_limited", "message": "Too many requests, retry later", "details": { "limit": 600, "retryAfterSeconds": 17 } } }Wait Retry-After seconds, then retry. Refused requests are counted in the usage statistics of the client but perform no action.
curl -s --retry 3 --retry-delay 0 "$MLT_API_URL/v1/contacts?entityId=$ENTITY_ID" \ -H "Authorization: Bearer $MLT_TOKEN"# curl honors Retry-After on 429 when --retry is set.async function withRetry(input: string, init: RequestInit = {}, attempts = 3): Promise<Response> { for (let attempt = 1; ; attempt++) { const response = await fetch(input, init); if (response.status !== 429 || attempt === attempts) return response; const seconds = Number(response.headers.get('Retry-After') ?? '1'); await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); }}import time
def with_retry(method: str, url: str, attempts: int = 3, **kwargs) -> requests.Response: for attempt in range(1, attempts + 1): response = requests.request(method, url, **kwargs) if response.status_code != 429 or attempt == attempts: return response time.sleep(int(response.headers.get("Retry-After", "1")))use GuzzleHttp\HandlerStack;use GuzzleHttp\Middleware;use Psr\Http\Message\ResponseInterface;
$stack = HandlerStack::create();$stack->push(Middleware::retry( fn (int $retries, $request, ?ResponseInterface $response) => $retries < 3 && $response?->getStatusCode() === 429, fn (int $retries, ?ResponseInterface $response) => 1000 * (int) ($response?->getHeaderLine('Retry-After') ?: 1),));$http = new GuzzleHttp\Client(['handler' => $stack]);Good practices
Section titled “Good practices”- Reuse access tokens: the token endpoint of the identity provider is not meant to be called per request.
- Prefer webhooks to polling for status changes.
- Use
pageSize=100when you read whole lists. - Spread batch jobs (imports, nightly syncs) over time instead of bursting.
Business quotas (e.g. the number of signature requests or registered mails per period, set by your plan) are distinct from rate limits: they answer 403 quota_exceeded, see the options endpoints of each feature.