Limits
The free tier is meant to be enough for a small business for good. Paid removes the badge and raises the numbers. The table is rendered at build time from the same plan table the API enforces (PLANS in @proofql/core), so what you read here is what the service does.
| Limit | Free | Paid |
|---|---|---|
| Projects | 1 | 50 |
| Reviews per project | 5,000 | 100,000 |
| Queries per month (cached hits are free) | 50,000 | 2,000,000 |
| Rate limit, secret key | 300 / min | 1,000 / min |
| Rate limit, publishable key | 120 / min | 600 / min |
| Snippet badge | Shown | Removable |
Billing is not open yet (M3): the Paid column is the shape of the plan, not a price list, and its numbers are ceilings against runaway scripts rather than product promises. Pricing will be at https://proofql.dev/pricing. Sources (CSV, the push API, Google) and key kinds (live and test) are the same on every plan; Paid adds priority polling for Google.
At the review cap a POST /v1/reviews is a 422 review_limit_reached and nothing from that batch is written. At the query quota a cache miss on /v1/query is a 429 query_quota_exceeded with Retry-After set to the seconds until the next UTC month, while cached answers keep being served. The month is a calendar month in UTC.
Queries are counted per project; a cache hit (cached: true, x-cache: HIT) is not counted. Identical requests from the snippet across your pages hit the same cache entry, so a site that asks the same handful of questions uses a small fraction of the quota however much traffic it gets.
Snippet badge
Section titled “Snippet badge”- Free: every query response says
badge: true, so the snippet shows the badge under the list. - Paid: every query response says
badge: false, so the snippet renders no badge (your own markup may still credit ProofQL).
The badge is derived from the plan on every request; it is never a per-project setting, so it changes the moment the plan does.
Rate limits
Section titled “Rate limits”Counted per key, every authenticated request:
| Key kind | Free | Paid |
|---|---|---|
Secret (pq_sk_…) | 300 requests per 60 s | 1,000 requests per 60 s |
Publishable (pq_pk_…) | 120 requests per 60 s | 600 requests per 60 s |
Over the limit the request is a 429 rate_limited with Retry-After. Every authenticated response carries RateLimit-Policy and RateLimit-Limit (IETF draft ratelimit headers), so a client can read its own limit instead of hard-coding this table.
Request limits
Section titled “Request limits”These are not product limits; they exist so a single malformed or hostile request cannot push megabytes into the database. Real reviews are nowhere near them.
Request bodies are capped per route, checked from Content-Length and again while the body streams. Like the plan table, this one is rendered at build time from the constants the API enforces (REQUEST_BODY_LIMITS in @proofql/core). A body with any Content-Type other than application/json is a 415 unsupported_media_type before it is read.
| Request body | Limit | Over the limit |
|---|---|---|
POST /v1/reviews body | 1 MiB | 413 payload_too_large |
PATCH /v1/reviews/{id} body | 64 KiB | 413 payload_too_large |
/v1/query body | 16 KiB | 413 payload_too_large |
Everything else is a size on a field:
| Limit | Over the limit | |
|---|---|---|
Reviews per POST /v1/reviews |
100 | 422 validation_failed |
Review text |
20,000 characters | 422 validation_failed |
external_id |
512 characters | 422 |
author_name |
256 characters | 422 |
metadata |
32 entries; keys 64 characters, values 512 | 422 |
/v1/query q |
500 characters | 422 |
/v1/query limit |
1 to 20 (default 5) | 422; the snippet clamps data-limit |
GET /v1/reviews limit |
1 to 100 (default 20) | 422 |
| Allowed origins | exact scheme, host, and port; no wildcards | an unlisted origin is 403 forbidden |
Response-time expectations
Section titled “Response-time expectations”A query is one embedding call plus one SQL statement over the project’s vectors (an exact scan, which for a tenant under ~50,000 vectors is single-digit milliseconds). took_ms in every response is the server’s own measurement; cached answers return in the time of a KV read.