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/queryAuth
Two checks:
- Origin must match a registered domain (same as
/token). X-Spelo-Signatureheader 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/queryOrigin: https://emberandoak.comContent-Type: application/jsonX-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.
| Field | Type | Notes |
|---|---|---|
collection | string | Must match a key in the site’s adapter config |
query | string (optional) | Free-text search |
filters | array (optional) | Structured filters |
sort_by | string (optional) | Must be in the collection’s filterable_fields / searchable_fields / display_fields |
sort_direction | 'asc' | 'desc' | Default 'asc' |
limit | number (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
# 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 bodyBODY='{"collection":"menu_items","query":"vegan","limit":5}'SIGNATURE=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SIGNING_SECRET" | sed 's/^.* //')
# 3) Call /querycurl -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
| HTTP | Code | Cause |
|---|---|---|
| 400 | invalid_request | Missing collection or malformed body |
| 401 | invalid_signature | HMAC mismatch — signing secret wrong or expired |
| 403 | origin_not_allowed | Bad Origin |
| 422 | validation_failed | Unknown field, unsupported operator, or out-of-range value |
| 422 | adapter_error | Your adapter returned success: false; details.error has the message |
| 429 | rate_limited | 300/min per site |
| 504 | adapter_timeout | Your backend took longer than 10 seconds |
Rate limits
| Scope | Limit | Window |
|---|---|---|
| Per site_id | 300 | 1 minute |
| Burst | 30 | 1 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
- Sites API — configuring
filterable_fieldsetc. - Connect your data — adapter setup
- Token endpoint — how the signing secret is issued