Skip to content

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.

LimitFreePaid
Projects150
Reviews per project5,000100,000
Queries per month (cached hits are free)50,0002,000,000
Rate limit, secret key300 / min1,000 / min
Rate limit, publishable key120 / min600 / min
Snippet badgeShownRemovable

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.

  • 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.

Counted per key, every authenticated request:

Key kindFreePaid
Secret (pq_sk_…)300 requests per 60 s1,000 requests per 60 s
Publishable (pq_pk_…)120 requests per 60 s600 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.

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 bodyLimitOver the limit
POST /v1/reviews body1 MiB413 payload_too_large
PATCH /v1/reviews/{id} body64 KiB413 payload_too_large
/v1/query body16 KiB413 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

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.