Rate limits and quotas: every number on the free plan

Updated 2026-10-30

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

ScopeFree plan
API key (REST and creator MCP together)30 requests per minute, 1,000 per day
Open forms per account3
Responses per account per calendar month (UTC), all sources100. 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 IP60 per minute
Human submit, per IP5 per minute and 30 per hour
Human submit, per form60 per minute
Fill token mint, per IP5 per minute and 30 per day
Fill token mint, per form200 per day
One fill token1 successful use, 60 minutes lifetime, 5 POST attempts, 30 GET reads per hour
POST /v1/fill/{token}, per IP10 per minute
Fill MCP calls, per IP20 per minute
Signup, per IP3 per hour
Signup, per email address3 per day
Abuse report form, per IP5 per hour
Webhooks per form3
Request body size64 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.

ScopeFree planPro plan
API key (REST and creator MCP together)30 per minute, 1,000 per day120 per minute, 20,000 per day
Open forms per account3Unlimited
Responses per account per calendar month (UTC), all sources10010,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:

HeaderMeaning
RateLimit-LimitHow many requests the current window allows.
RateLimit-RemainingHow many requests you have left in this window.
RateLimit-ResetSeconds until the window resets.

A 429 adds one more:

HeaderMeaning
Retry-AfterSeconds to wait before you send the request again.
http
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:

bash
curl https://aiformapi.com/v1/usage -H "Authorization: Bearer $AIFORMAPI_KEY"

GET /v1/usage returns your plan, your limits and your current usage.

json
{
  "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

  1. Read the Retry-After header on a 429 and wait that many seconds.
  2. 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.
  3. Stop after a few tries and report the error.
  4. Watch RateLimit-Remaining. Slow down before it reaches 0.
  5. 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

js
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

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"))