Skip to content
GitHub
Get started →

Query endpoint

When the AI calls the search_database function, the widget translates the call into an HTTP POST to this endpoint. We decrypt your data connection credentials, call your adapter’s search(), and return the result.

Endpoint

POST /v1/:siteId/query

Auth

Two checks:

  1. Origin must match a registered domain (same as /token).
  2. X-Spelo-Signature header must validate as HMAC-SHA256 of the body with the session’s signing secret (issued from /token).

An attacker without the signing secret cannot call /query even if they have the site_id.

Request

POST /v1/ab1c2d3e/query
Origin: https://emberandoak.com
Content-Type: application/json
X-Spelo-Signature: sha256=a7f...
X-Spelo-Session: sess_xyz
{
"collection": "menu_items",
"query": "vegan",
"filters": [
{ "field": "price", "operator": "lte", "value": 20 }
],
"sort_by": "price",
"sort_direction": "asc",
"limit": 5
}

Body shape matches SearchParams in packages/shared/src/types.ts.

FieldTypeNotes
collectionstringMust match a key in the site’s adapter config
querystring (optional)Free-text search
filtersarray (optional)Structured filters
sort_bystring (optional)Must be in the collection’s filterable_fields / searchable_fields / display_fields
sort_direction'asc' | 'desc'Default 'asc'
limitnumber (optional)Max 10

Filter operators

type FilterOperator =
| 'eq' | 'neq'
| 'gt' | 'gte' | 'lt' | 'lte'
| 'contains'
| 'in'

See adapter-specific pages for how each one translates to the backend.

Response 200

{
"success": true,
"data": {
"success": true,
"total": 12,
"returned": 5,
"items": [
{ "id": "m1", "name": "Farro Bowl", "price": 18, "vegan": true },
...
]
}
}

data.success: false indicates an adapter-level error (e.g. DB connection died mid-query). data.error has the message.

Example curl

Terminal window
# 1) Get a token (needs a valid Origin)
TOKEN_RESPONSE=$(curl -sS -X POST "https://api.spelo.ai/v1/ab1c2d3e/token" \
-H "Origin: https://emberandoak.com" \
-H "Content-Type: application/json" \
-d '{}')
SIGNING_SECRET=$(echo "$TOKEN_RESPONSE" | jq -r '.data.signing_secret')
SESSION_ID=$(echo "$TOKEN_RESPONSE" | jq -r '.data.session_id')
# 2) Sign the query body
BODY='{"collection":"menu_items","query":"vegan","limit":5}'
SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | sed 's/^.* //')
# 3) Call /query
curl -X POST "https://api.spelo.ai/v1/ab1c2d3e/query" \
-H "Origin: https://emberandoak.com" \
-H "Content-Type: application/json" \
-H "X-Spelo-Signature: sha256=${SIGNATURE}" \
-H "X-Spelo-Session: ${SESSION_ID}" \
-d "$BODY"

In practice you never do this from outside the widget — the widget handles the signing automatically. The example is for debugging.

Identifier whitelist

Every field name in the request must exist in the collection’s config under filterable_fields / searchable_fields / display_fields. Anything else is rejected with 422 validation_failed:

{
"success": false,
"error": {
"code": "validation_failed",
"message": "Field 'password' is not filterable for collection 'menu_items'"
}
}

This is our strongest defense against the AI generating an unauthorized query via hallucinated field names.

Errors

HTTPCodeCause
400invalid_requestMissing collection or malformed body
401invalid_signatureHMAC mismatch — signing secret wrong or expired
403origin_not_allowedBad Origin
422validation_failedUnknown field, unsupported operator, or out-of-range value
422adapter_errorYour adapter returned success: false; details.error has the message
429rate_limited300/min per site
504adapter_timeoutYour backend took longer than 10 seconds

Rate limits

ScopeLimitWindow
Per site_id3001 minute
Burst301 second

Timeouts

  • Adapter calls time out at 10 seconds.
  • If your backend is slow, the AI gets an error and tells the user “I couldn’t find that right now, give me a moment” — often retrying.

Logging

Every /query call is logged (request body, response size, latency) and available in Dashboard → Analytics → Query log. Actual row contents are NOT logged (for privacy and size).

See also