Skip to content

Overview

Semantic search over a business's reviews, served at the edge.

ProofQL turns a business’s reviews into a semantically searchable corpus so a website can show the reviews relevant to each page. This document covers the public /v1 API: pushing reviews in, managing them, and querying them.

Conventions

  • JSON everywhere (Content-Type: application/json). Field names are snake_case. Timestamps are ISO 8601 / RFC 3339 in UTC. Ids are UUIDs. A request body with any other Content-Type (or none) is a 415 unsupported_media_type, refused before it is read.
  • Versioned by path prefix (/v1).
  • Every response carries an x-request-id header. Quote it in a support request; it is also inside every error envelope as error.request_id. Send your own (up to 128 characters of A-Z a-z 0-9 . _ : -) to correlate with your logs; anything else is replaced with a fresh id.
  • Every response also carries X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, and Cache-Control: no-store — except a successful GET /v1/query, which is Cache-Control: private, max-age=0, must-revalidate: it varies by key, so a shared cache must not hold it; the result cache is server-side (x-cache).
  • Unknown fields anywhere — body or query string — are a 422 validation_failed naming the field, never silently ignored.

Errors

Every non-2xx response is one envelope:

{ "error": { "code": "validation_failed", "message": "…", "doc_url": "https://docs.proofql.dev/errors#validation_failed",
             "request_id": "…", "details": [{ "path": "0.rating", "message": "…" }] } }

code is a short, stable string to switch on (the full list is the ErrorCode schema); message is for a human and may change; details is present only for validation failures. A request for a route that does not exist is a 404 not_found in the same envelope.

Keys

Two kinds, both per project, both prefixed so a leaked key’s blast radius is obvious:

Prefix Kind Can Lives
pq_sk_live_… / pq_sk_test_… Secret Everything: ingest, manage, query Servers only
pq_pk_live_… / pq_pk_test_… Publishable /v1/query only, CORS-restricted to the project’s allowed origins Browsers, the snippet

Send either kind as Authorization: Bearer <key>. On GET /v1/query a publishable key may instead ride in the URL as ?key=pq_pk_… (the snippet’s form); on POST /v1/query a ?key= is a 401 unauthorized, and secret keys are never accepted from the URL on any method (URLs land in logs and referrers). A publishable key on a secret-only route is a 403 forbidden. A test key reads and writes only test rows of its project; a live key only live rows.

Rate limits

Every authenticated request is counted against its key: 300 requests per 60 s for a secret key, 120 per 60 s for a publishable key. Every authenticated response advertises the limit in RateLimit-Policy and RateLimit-Limit (IETF draft ratelimit headers); a refused request is a 429 rate_limited with Retry-After. Separately, /v1/query is subject to the project’s monthly quota of uncached queries (free tier: 50,000); at the quota a cache miss is a 429 query_quota_exceeded with Retry-After set to the seconds until the next UTC month, while cached answers keep being served.

CORS

A publishable key is public by design, so the page’s origin is what scopes it: a /v1/query request with a publishable key must carry an Origin header that is in the project’s allowed origins, compared as a whole origin (scheme, host, port; no wildcards), or it is a 403 forbidden. OPTIONS /v1/query answers browser preflights. The snippet’s form — GET /v1/query?key=pq_pk_…&q=… with no custom headers — is a CORS simple request, so browsers send it without any preflight.

A secret key, Authorization: Bearer pq_sk_live_…. Servers only. Grants everything: ingest, manage, query.

Security scheme type: http

Bearer format: pq_sk_{live|test}_<32 base62 chars>

A publishable key in the Authorization header. Query only; the request must carry an Origin from the project’s allowed origins.

Security scheme type: http

Bearer format: pq_pk_{live|test}_<32 base62 chars>

A publishable key in the URL (?key=pq_pk_live_…) — the snippet’s form, so the browser’s GET needs no custom header and no preflight. GET /v1/query only: on POST /v1/query any ?key= is a 401 pointing at the Authorization header. A secret key here is a 401. On GET the Authorization header wins when both are present.

Security scheme type: apiKey

Query parameter name: key