Skip to content

Search (GET — the snippet's path)

GET
/v1/query
curl --request GET \
--url 'https://api.proofql.dev/v1/query?q=dental%20implants&limit=3&mode=excerpts&include=text&fallback=none&min_rating=4&source=google&source=yelp&since=2025-01-01' \
--header 'Authorization: Bearer <token>' \
--header 'Cache-Control: no-cache' \
--header 'Origin: https://shop.example'

This is the form the snippet uses, and the one to use from a browser: GET /v1/query?key=pq_pk_…&q=… with no custom headers is a CORS simple request, so the browser sends it with no preflight, and responses are cacheable at the edge. The same request shape as POST /v1/query, mapped onto query parameters:

parameter body field
q q
limit limit
mode mode
include (repeatable, or comma-separated) include
fallback fallback
min_rating filters.min_rating
source (repeatable, or comma-separated) filters.source
since filters.since
metadata.<key> (one per key, e.g. metadata.location=north) filters.metadata.<key>
key authentication (publishable keys only), not part of the shape

Any other parameter, or q/limit/mode/fallback/min_rating/since given twice, is a 422 validation_failed.

See POST /v1/query for the semantics of q, the relevance floor, policy, score, highlight, include, fallback and match, badge, cached, and the cache.

key
string
/^pq_pk_(live|test)_[0-9A-Za-z]{32}$/

A publishable key, as an alternative to the Authorization header on GET (see the publishableKeyQuery security scheme). Not part of the request shape.

q
string
>= 1 characters <= 500 characters

The search text. Omit for the newest publishable reviews.

Example
dental implants
limit
integer
default: 5 >= 1 <= 20
Example
3
mode

excerpts: the best-matching slice per review. reviews: whole reviews, with review.text.

string
default: excerpts
Allowed values: excerpts reviews
include
Array<string>
<= 8 items
Allowed values: text

Optional response fields; repeat the parameter or comma-separate values. text adds review.text in mode=excerpts.

Example
?include=text
fallback

What to return when q is present and nothing clears the floor: none (default) an empty list; recent the newest publishable reviews under the same policy and filters, with match: fallback.

string
default: none
Allowed values: none recent

recent: when nothing clears the floor, return the newest publishable reviews with match: fallback. Default none.

min_rating
integer
>= 1 <= 5

Tightens the project’s min_rating for this request; cannot loosen it.

Example
4
source
Array<string>
<= 20 items

Repeat the parameter or comma-separate values; at most 20.

Example
?source=google&source=yelp
since
Any of:

YYYY-MM-DD (midnight UTC) or a full ISO 8601 timestamp with Z or offset.

string format: date

occurred_at >= since; YYYY-MM-DD or a full ISO 8601 timestamp.

Example
2025-01-01
Origin
string format: uri

Set by the browser. Required with a publishable key and must be one of the project’s allowed origins (exact scheme, host, and port), else 403 forbidden. Ignored for secret keys (echoed if present).

Example
https://shop.example
Cache-Control
string

no-cache skips the result-cache lookup (the response says x-cache: BYPASS). Costs one uncached query.

Example
no-cache

The matches in rank order — possibly none: below-floor candidates are dropped, never padded. match says what the list is: real matches, an honest fallback the caller asked for, nothing, or the newest reviews because no q was sent.

Media typeapplication/json
object
results
required

In rank order. Empty when nothing clears the floor and no fallback was requested. All matches or all fallback, never a mix.

Array<object>
object
score
required

Cosine similarity between the query and this excerpt, in [0, 1], already at or above the project’s relevance floor (default 0.66) — or, for an excerpt that matches the query’s words, at or above the word-match tier (the floor minus 0.13) — and comparable across queries. Not the fused rank the results are ordered by, so it may be non-monotone down the list. null when the request had no q and on fallback rows.

number | null
<= 1
matched
required

true for a real match (match: query); false on a fallback row and without q.

boolean
excerpt
required

A verbatim slice of the review’s text — the best-matching chunk.

string
excerpt_id
required

Stable id of the excerpt (chunk).

string format: uuid
highlight
required
One of:

Where excerpt sits in review.text, as UTF-16 code-unit offsets (the unit of String.prototype.slice), end exclusive: review.text.slice(start, end) === excerpt.

object
start
required
integer
end
required
integer
>= 1
review
required
object
id
required
string format: uuid
rating
required
integer | null
>= 1 <= 5
author_name
required
string | null
author_avatar_url
required
string | null
source
required

Where the review came from. custom is the escape hatch for anything not listed.

string
Allowed values: google yelp facebook trustpilot custom
occurred_at
required
string | null format: date-time
url
required
string | null
metadata
required

Flat string-to-string map the customer can filter on at query time. At most 32 entries; keys 1–64 characters, values up to 512. Nested objects, arrays, numbers, and booleans are rejected, not coerced.

object
<= 32 properties
key
additional properties
string
<= 512 characters
text

The whole review text. Present in mode=reviews, or in any mode with include: ["text"].

string
match
required

What results is: query real matches; fallback the newest reviews because nothing cleared the floor and fallback: recent was requested; none nothing cleared the floor and results is empty; recent no q was sent.

string
Allowed values: query fallback none recent
took_ms
required

Server time for this request, cached or not.

integer
cached
required

true when results were served from the result cache (x-cache: HIT).

boolean
badge
required

true means the snippet must render the “Reviews by ProofQL” badge (free tier). Mirrors the project’s plan on every response, cached or not.

boolean
Examples

`mode=excerpts` for `q=dental implants`

{
"results": [
{
"score": 0.83,
"matched": true,
"excerpt": "Dr. Patel did my implant and I honestly forgot it wasn't my own tooth within a week.",
"excerpt_id": "7c2e4b9a-1d3f-4a5b-8c6d-0e9f1a2b3c4d",
"highlight": {
"start": 26,
"end": 110
},
"review": {
"id": "2f1c9e5a-3b7d-4c1e-9f0a-6d2b8e4a1c3f",
"rating": 5,
"author_name": "Marcus T.",
"author_avatar_url": null,
"source": "google",
"occurred_at": "2026-03-14T18:20:00.000Z",
"url": "https://maps.google.com/?cid=123",
"metadata": {
"location": "north"
}
}
}
],
"match": "query",
"took_ms": 12,
"cached": false,
"badge": true
}
x-request-id
required
string
>= 1 characters <= 128 characters

This request’s id, on every response. Reuses the caller’s x-request-id when sent (up to 128 chars), else Cloudflare’s ray id, else a fresh UUID. Also inside every error envelope.

Cache-Control
required
string
Allowed values: private, max-age=0, must-revalidate no-store

private, max-age=0, must-revalidate on a successful GET /v1/query: the response varies by key, so a shared cache must never hold it, and a browser revalidates rather than reuses. no-store on POST, like every other response.

RateLimit-Policy
required
string
/^[0-9]+;w=[0-9]+$/

The key’s rate-limit policy as <limit>;w=<window seconds> (IETF draft ratelimit headers), e.g. 300;w=60 for a secret key, 120;w=60 for a publishable key. Present once the key is known.

RateLimit-Limit
required
integer
>= 1

The request limit per window for this key’s kind.

x-cache
required
string
Allowed values: HIT MISS BYPASS

Whether results came from the result cache: HIT (served from cache, cached: true), MISS (computed; stored when the cache accepts it), or BYPASS (the request sent Cache-Control: no-cache; computed).

Vary
required
string
Allowed value: Origin

Always Origin on /v1/query, so a shared cache never serves one origin’s CORS headers to another.

Access-Control-Allow-Origin
string

The request’s Origin, echoed when a publishable key’s project lists it or when the key is secret. Absent otherwise (the browser then blocks the response).

unauthorized: no Authorization header (and no ?key= where that is allowed), a header that is not Bearer <key>, a key that does not look like pq_(sk|pk)_(live|test)_…, a secret key in the URL, a ?key= on POST /v1/query, or an unknown or revoked key. Rate-limit headers are absent (the key was not resolved).

Media typeapplication/json
object
error
required
object
code
required

Stable error codes. Status by code: unauthorized 401, forbidden 403, not_found 404, payload_too_large 413, unsupported_media_type 415, validation_failed 422, review_limit_reached 422, rate_limited 429, query_quota_exceeded 429, internal 500, embedding_unavailable 503, service_unavailable 503.

string
Allowed values: unauthorized forbidden validation_failed review_limit_reached not_found payload_too_large unsupported_media_type rate_limited embedding_unavailable query_quota_exceeded service_unavailable internal
message
required

For humans; may change between releases. Switch on code.

string
doc_url
required

https://docs.proofql.dev/errors#<code>.

string format: uri
request_id
required

Same value as the x-request-id header.

string
details

Present for validation_failed only.

Array<object>
object
path
required

Dotted path to the offending field ("0.rating", "filters.since"); "" or "(body)" for the whole body.

string
message
required
string
error
required
object
code
Allowed value: unauthorized
Examples
Examplemissing
{
"error": {
"code": "unauthorized",
"message": "Missing Authorization header. Send `Authorization: Bearer <api key>`.",
"doc_url": "https://docs.proofql.dev/errors#unauthorized",
"request_id": "1b7c3d9e-2f4a-4b6c-8d0e-1f2a3b4c5d6e"
}
}
x-request-id
required
string
>= 1 characters <= 128 characters

This request’s id, on every response. Reuses the caller’s x-request-id when sent (up to 128 chars), else Cloudflare’s ray id, else a fresh UUID. Also inside every error envelope.

forbidden: a publishable key with no Origin header, or with an Origin that is not in the project’s allowed origins. The message names the origin and where to add it. (A publishable key never gets a 403 for the key kind here — both kinds may query.)

Media typeapplication/json
object
error
required
object
code
required

Stable error codes. Status by code: unauthorized 401, forbidden 403, not_found 404, payload_too_large 413, unsupported_media_type 415, validation_failed 422, review_limit_reached 422, rate_limited 429, query_quota_exceeded 429, internal 500, embedding_unavailable 503, service_unavailable 503.

string
Allowed values: unauthorized forbidden validation_failed review_limit_reached not_found payload_too_large unsupported_media_type rate_limited embedding_unavailable query_quota_exceeded service_unavailable internal
message
required

For humans; may change between releases. Switch on code.

string
doc_url
required

https://docs.proofql.dev/errors#<code>.

string format: uri
request_id
required

Same value as the x-request-id header.

string
details

Present for validation_failed only.

Array<object>
object
path
required

Dotted path to the offending field ("0.rating", "filters.since"); "" or "(body)" for the whole body.

string
message
required
string
error
required
object
code
Allowed value: forbidden
Example
{
"error": {
"code": "forbidden"
}
}
x-request-id
required
string
>= 1 characters <= 128 characters

This request’s id, on every response. Reuses the caller’s x-request-id when sent (up to 128 chars), else Cloudflare’s ray id, else a fresh UUID. Also inside every error envelope.

RateLimit-Policy
required
string
/^[0-9]+;w=[0-9]+$/

The key’s rate-limit policy as <limit>;w=<window seconds> (IETF draft ratelimit headers), e.g. 300;w=60 for a secret key, 120;w=60 for a publishable key. Present once the key is known.

RateLimit-Limit
required
integer
>= 1

The request limit per window for this key’s kind.

Vary
required
string
Allowed value: Origin

Always Origin on /v1/query, so a shared cache never serves one origin’s CORS headers to another.

validation_failed: the body or query string is not valid JSON, does not match the schema, or carries an unknown field. details lists every issue with a dotted path ("0.rating", "filters.limt"; "" or "(body)" for the body as a whole).

Media typeapplication/json
object
error
required
object
code
required

Stable error codes. Status by code: unauthorized 401, forbidden 403, not_found 404, payload_too_large 413, unsupported_media_type 415, validation_failed 422, review_limit_reached 422, rate_limited 429, query_quota_exceeded 429, internal 500, embedding_unavailable 503, service_unavailable 503.

string
Allowed values: unauthorized forbidden validation_failed review_limit_reached not_found payload_too_large unsupported_media_type rate_limited embedding_unavailable query_quota_exceeded service_unavailable internal
message
required

For humans; may change between releases. Switch on code.

string
doc_url
required

https://docs.proofql.dev/errors#<code>.

string format: uri
request_id
required

Same value as the x-request-id header.

string
details

Present for validation_failed only.

Array<object>
object
path
required

Dotted path to the offending field ("0.rating", "filters.since"); "" or "(body)" for the whole body.

string
message
required
string
error
required
object
code
Allowed value: validation_failed
details
required
array
Examples
ExampleunknownField
{
"error": {
"code": "validation_failed",
"message": "Invalid request: limt is not a recognized field.",
"doc_url": "https://docs.proofql.dev/errors#validation_failed",
"request_id": "1b7c3d9e-2f4a-4b6c-8d0e-1f2a3b4c5d6e",
"details": [
{
"path": "limt",
"message": "is not a recognized field"
}
]
}
}
x-request-id
required
string
>= 1 characters <= 128 characters

This request’s id, on every response. Reuses the caller’s x-request-id when sent (up to 128 chars), else Cloudflare’s ray id, else a fresh UUID. Also inside every error envelope.

RateLimit-Policy
required
string
/^[0-9]+;w=[0-9]+$/

The key’s rate-limit policy as <limit>;w=<window seconds> (IETF draft ratelimit headers), e.g. 300;w=60 for a secret key, 120;w=60 for a publishable key. Present once the key is known.

RateLimit-Limit
required
integer
>= 1

The request limit per window for this key’s kind.

rate_limited (over the key’s per-minute limit; Retry-After is seconds to the window end) or query_quota_exceeded (the project is at its monthly quota of uncached queries; Retry-After is seconds to the next UTC month). Cached answers are still served at quota.

Media typeapplication/json
object
error
required
object
code
required

Stable error codes. Status by code: unauthorized 401, forbidden 403, not_found 404, payload_too_large 413, unsupported_media_type 415, validation_failed 422, review_limit_reached 422, rate_limited 429, query_quota_exceeded 429, internal 500, embedding_unavailable 503, service_unavailable 503.

string
Allowed values: unauthorized forbidden validation_failed review_limit_reached not_found payload_too_large unsupported_media_type rate_limited embedding_unavailable query_quota_exceeded service_unavailable internal
message
required

For humans; may change between releases. Switch on code.

string
doc_url
required

https://docs.proofql.dev/errors#<code>.

string format: uri
request_id
required

Same value as the x-request-id header.

string
details

Present for validation_failed only.

Array<object>
object
path
required

Dotted path to the offending field ("0.rating", "filters.since"); "" or "(body)" for the whole body.

string
message
required
string
error
required
object
code
Allowed values: rate_limited query_quota_exceeded
Example
{
"error": {
"code": "rate_limited"
}
}
x-request-id
required
string
>= 1 characters <= 128 characters

This request’s id, on every response. Reuses the caller’s x-request-id when sent (up to 128 chars), else Cloudflare’s ray id, else a fresh UUID. Also inside every error envelope.

RateLimit-Policy
required
string
/^[0-9]+;w=[0-9]+$/

The key’s rate-limit policy as <limit>;w=<window seconds> (IETF draft ratelimit headers), e.g. 300;w=60 for a secret key, 120;w=60 for a publishable key. Present once the key is known.

RateLimit-Limit
required
integer
>= 1

The request limit per window for this key’s kind.

Retry-After
required
integer
>= 1

Whole seconds to wait. For rate_limited, until the current window ends; for query_quota_exceeded, until the next UTC month begins.

internal: something unexpected. The message carries the request id and nothing about the cause.

Media typeapplication/json
object
error
required
object
code
required

Stable error codes. Status by code: unauthorized 401, forbidden 403, not_found 404, payload_too_large 413, unsupported_media_type 415, validation_failed 422, review_limit_reached 422, rate_limited 429, query_quota_exceeded 429, internal 500, embedding_unavailable 503, service_unavailable 503.

string
Allowed values: unauthorized forbidden validation_failed review_limit_reached not_found payload_too_large unsupported_media_type rate_limited embedding_unavailable query_quota_exceeded service_unavailable internal
message
required

For humans; may change between releases. Switch on code.

string
doc_url
required

https://docs.proofql.dev/errors#<code>.

string format: uri
request_id
required

Same value as the x-request-id header.

string
details

Present for validation_failed only.

Array<object>
object
path
required

Dotted path to the offending field ("0.rating", "filters.since"); "" or "(body)" for the whole body.

string
message
required
string
error
required
object
code
Allowed value: internal
Example
{
"error": {
"code": "internal"
}
}
x-request-id
required
string
>= 1 characters <= 128 characters

This request’s id, on every response. Reuses the caller’s x-request-id when sent (up to 128 chars), else Cloudflare’s ray id, else a fresh UUID. Also inside every error envelope.

Retryable, never a degraded answer: embedding_unavailable (q could not be embedded; no full-text-only fallback) or service_unavailable (the database refused or timed out a connection, Retry-After: 1; or a daily platform allowance is spent, Retry-After until it renews).

Media typeapplication/json
object
error
required
object
code
required

Stable error codes. Status by code: unauthorized 401, forbidden 403, not_found 404, payload_too_large 413, unsupported_media_type 415, validation_failed 422, review_limit_reached 422, rate_limited 429, query_quota_exceeded 429, internal 500, embedding_unavailable 503, service_unavailable 503.

string
Allowed values: unauthorized forbidden validation_failed review_limit_reached not_found payload_too_large unsupported_media_type rate_limited embedding_unavailable query_quota_exceeded service_unavailable internal
message
required

For humans; may change between releases. Switch on code.

string
doc_url
required

https://docs.proofql.dev/errors#<code>.

string format: uri
request_id
required

Same value as the x-request-id header.

string
details

Present for validation_failed only.

Array<object>
object
path
required

Dotted path to the offending field ("0.rating", "filters.since"); "" or "(body)" for the whole body.

string
message
required
string
error
required
object
code
Allowed values: embedding_unavailable service_unavailable
Example
{
"error": {
"code": "embedding_unavailable"
}
}
x-request-id
required
string
>= 1 characters <= 128 characters

This request’s id, on every response. Reuses the caller’s x-request-id when sent (up to 128 chars), else Cloudflare’s ray id, else a fresh UUID. Also inside every error envelope.

RateLimit-Policy
string
/^[0-9]+;w=[0-9]+$/

Present on embedding_unavailable (the key was resolved); absent when the database was unreachable before auth.

RateLimit-Limit
string
/^[0-9]+$/

As RateLimit-Policy.

Retry-After
integer
>= 1

Present on service_unavailable only.

Vary
string

Origin, on a response that set a CORS allow-origin.