Rate limits and quotas: every number on the free plan
The free plan allows 30 requests per minute and 1,000 per day for each API key. It allows 3 open forms and 100 responses a month. Public pages and fill endpoints have their own limits by IP address. Limited responses carry RateLimit-* headers. A 429 adds Retry-After.
A rate limit slows you down for a short time. You get 429 rate_limited, wait, and try again. A quota is a monthly or plan allowance. When you hit it you get 403 quota_exceeded, and waiting a few seconds does not help.
The full table
| Scope | Free plan |
|---|---|
| API key (REST and creator MCP together) | 30 requests per minute, 1,000 per day |
| Open forms per account | 3 |
| Responses per account per calendar month (UTC), all sources | 100. After that, new submits get 403 quota_exceeded and the form page says "This form is full for this month". |
Public form schema, GET /v1/public/forms/{slug}, per IP | 60 per minute |
| Human submit, per IP | 5 per minute and 30 per hour |
| Human submit, per form | 60 per minute |
| Fill token mint, per IP | 5 per minute and 30 per day |
| Fill token mint, per form | 200 per day |
| One fill token | 1 successful use, 60 minutes lifetime, 5 POST attempts, 30 GET reads per hour |
POST /v1/fill/{token}, per IP | 10 per minute |
| Fill MCP calls, per IP | 20 per minute |
| Signup, per IP | 3 per hour |
| Signup, per email address | 3 per day |
| Abuse report form, per IP | 5 per hour |
| Webhooks per form | 3 |
| Request body size | 64 KB |
Pro plan (coming soon)
Pro costs $19 per month. It is not on sale yet and there is no billing. It changes the plan limits only. Every other row in the table above stays the same.
| Scope | Free plan | Pro plan |
|---|---|---|
| API key (REST and creator MCP together) | 30 per minute, 1,000 per day | 120 per minute, 20,000 per day |
| Open forms per account | 3 | Unlimited |
| Responses per account per calendar month (UTC), all sources | 100 | 10,000 |
See Pricing. We give 30 days notice by email before a plan change.
We store IP addresses only as a salted SHA-256 hash. The limits above work on that hash.
Headers
Every response from a rate-limited endpoint has 3 headers:
| Header | Meaning |
|---|---|
RateLimit-Limit | How many requests the current window allows. |
RateLimit-Remaining | How many requests you have left in this window. |
RateLimit-Reset | Seconds until the window resets. |
A 429 adds one more:
| Header | Meaning |
|---|---|
Retry-After | Seconds to wait before you send the request again. |
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
RateLimit-Limit: 30
RateLimit-Remaining: 0
RateLimit-Reset: 41
Retry-After: 41
{"error": {"code": "rate_limited", "message": "Too many requests. Try again in 41 seconds."}}Check your usage at any time:
curl https://aiformapi.com/v1/usage -H "Authorization: Bearer $AIFORMAPI_KEY"GET /v1/usage returns your plan, your limits and your current usage.
{
"plan": "free",
"limits": {
"requests_per_minute": 30,
"requests_per_day": 1000,
"open_forms": 3,
"responses_per_month": 100
},
"usage": {
"open_forms": 1,
"responses_this_month": 12,
"month": "2026-10",
"resets_at": "2026-11-01T00:00:00.000Z"
}
}How to back off
- Read the
Retry-Afterheader on a 429 and wait that many seconds. - If there is no header, wait 1 second, then 2, then 4, up to a ceiling. Add a little random time so many clients do not retry at the same moment.
- Stop after a few tries and report the error.
- Watch
RateLimit-Remaining. Slow down before it reaches 0. - Replace polling with a webhook. A webhook costs you no requests.
Do not retry 403 quota_exceeded, 422 errors or 410 errors. The same request will fail again.
If you see 503 rate_limiter_unavailable, we could not check the limit and chose to refuse the request rather than guess. Wait a moment and try again.
Retry code
JavaScript
async function apiWithRetry(url, options = {}, maxTries = 5) {
for (let attempt = 1; attempt <= maxTries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429 && res.status !== 503) return res;
if (attempt === maxTries) return res;
const header = Number(res.headers.get("Retry-After"));
const backoff = Number.isFinite(header) && header > 0
? header
: Math.min(2 ** (attempt - 1), 30) + Math.random();
await new Promise((resolve) => setTimeout(resolve, backoff * 1000));
}
}
const res = await apiWithRetry("https://aiformapi.com/v1/forms", {
headers: { Authorization: `Bearer ${process.env.AIFORMAPI_KEY}` },
});
console.log(res.status, res.headers.get("RateLimit-Remaining"));Python
import os
import random
import time
import requests
def api_with_retry(method, url, max_tries=5, **kwargs):
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {os.environ['AIFORMAPI_KEY']}"
for attempt in range(1, max_tries + 1):
r = requests.request(method, url, headers=headers, timeout=30, **kwargs)
if r.status_code not in (429, 503) or attempt == max_tries:
return r
retry_after = r.headers.get("Retry-After")
if retry_after and retry_after.isdigit():
wait = int(retry_after)
else:
wait = min(2 ** (attempt - 1), 30) + random.random()
time.sleep(wait)
r = api_with_retry("GET", "https://aiformapi.com/v1/forms")
print(r.status_code, r.headers.get("RateLimit-Remaining"))Related
- Errors lists every code.
- Agent fill explains the token rules.
- Responses shows how to fetch only new rows with
since.