Rate Limits & Retries
Published limits
| Limit | Value |
|---|---|
| Standard rate | 10,000 requests/hour per key |
| Burst | 60 requests/second |
| Webhook deliveries | Unlimited |
Higher limits are available on request for Enterprise customers at no additional charge — contact [email protected] if your integration's normal operation approaches the standard ceiling.
Limits are enforced per API key, not per IP — an integration server sitting behind a shared corporate NAT doesn't share its limit with anyone else's key on the same network.
The 429 response
Exceeding the limit returns a 429 with a standard Retry-After header and a matching value in
the body, in case a proxy between you and OwlMar strips response headers:
429 Too Many Requests
Retry-After: 47
{ "error": "Rate limit exceeded", "code": "RATE_LIMITED", "meta": { "retryAfterMs": 47000 } }Backoff pattern
Respect Retry-After rather than retrying immediately, and use exponential backoff with jitter
as a fallback for any other transient failure (503 UPSTREAM_UNAVAILABLE, network timeouts):
async function fetchWithBackoff(url, options, maxAttempts = 5) {for (let attempt = 0; attempt < maxAttempts; attempt++) {const res = await fetch(url, options);if (res.status !== 429) return res;const retryAfterHeader = res.headers.get('Retry-After');const waitMs = retryAfterHeader? Number(retryAfterHeader) * 1000: Math.min(2 ** attempt * 1000, 30000) + Math.random() * 250;await new Promise((resolve) => setTimeout(resolve, waitMs));}throw new Error('Exceeded max retry attempts on 429 RATE_LIMITED');}
import timeimport randomimport requestsdef get_with_backoff(url, headers, max_attempts=5):for attempt in range(max_attempts):resp = requests.get(url, headers=headers)if resp.status_code != 429:resp.raise_for_status()return resp.json()retry_after = resp.headers.get("Retry-After")wait_s = float(retry_after) if retry_after else min(2 ** attempt, 30) + random.random() * 0.25time.sleep(wait_s)raise RuntimeError("exceeded max retries on 429 RATE_LIMITED")
A tight retry loop with no backoff makes a transient rate limit worse, not better — every immediate retry counts against the same hourly window it just tripped. Always back off.
Combine backoff with an Idempotency-Key on any write you retry — backoff alone protects your rate limit; idempotency protects your data from being created twice.
