POST /api/analyze
Request body
{
"prompt": "show me products under $30",
"fields": ["name", "price", "category", "stock"],
"collections": ["products", "orders", "customers"]
}
| Field | Type | Required | Notes |
|---|---|---|---|
prompt | string | yes | Plain-English question. Max 4000 characters. |
fields | string[] | no | Field names from the user's local schema, used to bias term resolution. Max 500 entries. No values. |
collections | string[] | no | Candidate collection names for routing. Max 100 entries. |
The engine is stateless — it never stores the prompt, never connects
to your database, and only uses fields / collections to improve
routing accuracy. Sending the raw schema is discouraged; sending data
values is forbidden by the contract (the API will accept them but they
have no effect and will appear in our access logs only as their length).
Response — 200 OK
{
"analysis": {
"intent": "find",
"operation": null,
"metric": null,
"groupBy": [],
"timeFrame": null,
"collection": ["products"],
"limit": null,
"sort": {},
"filters": { "price": { "$lt": 30 } },
"visualization": "TABLE",
"rawTokens": ["show", "product", "price", "under", "30"]
},
"tokens": ["show", "product", "price", "under", "30"],
"plan": "pro",
"planTier": "pro",
"usage": { "used": 12, "limit": 1000 },
"sector": {
"id": "ecommerce",
"label": "E-commerce / Retail",
"confidence": "high"
},
"message": "Prompt analyzed successfully"
}
analysis — the Analysis object
This is the structured intent the sidecar consumes. The exact shape is
defined in the internal packages/shared/src/contracts/analysis.js file
and is reproduced here:
| Field | Type | Notes |
|---|---|---|
intent | "find" | "aggregate" | High-level query shape. |
operation | "$sum" | "$avg" | "$min" | "$max" | "$count" | null | Aggregation op. null for plain find. |
metric | string | null | Field being aggregated (only set when operation != null). |
groupBy | string[] | Field names or time-unit tokens ("day", "week", …). |
timeFrame | string | { start, end } | null | Detected temporal window. Format depends on whether a range was named. |
collection | string[] | Candidate collection names, ranked by confidence. |
limit | number | null | Result limit, if the prompt named one. |
sort | { [field: string]: 1 | -1 } | Mongo-style sort spec. |
filters | { [field: string]: any } | Field → Mongo-style operator or literal. |
joins | object[] | Required lookups across collections. |
computedMetrics | object[] | Metrics requiring computation or formulas. |
subQueries | object[] | Subqueries required to satisfy the prompt. |
visualization | "BAR_CHART" | "LINE_CHART" | "PIE_CHART" | "TABLE" | false | Chart hint for the renderer. |
rawTokens | string[] | Preprocessed tokens, for debugging. |
Other response fields
| Field | Type | Notes |
|---|---|---|
tokens | string[] | Same as analysis.rawTokens. Kept at the top level for backward compatibility. |
plan | "free" | "pro" | "ultra" | "team" | Effective plan tier resolved by the portal (your key's plan or, if a team member, the team's). Server-stamped — clients cannot spoof this. |
planTier | same as plan | Alias kept for older clients. |
usage | { used: number, limit: number } | Usage in the current window as of after this call. limit: -1 is the JSON sentinel for "unlimited". |
sector | { id, label, confidence } | Sector pack auto-resolved from your schema fields (e.g. ecommerce, healthcare). Falls back to _generic. |
message | string | Human-readable status. Always "Prompt analyzed successfully" on 200. |
Response — errors
All error responses are JSON with at minimum { error, message }.
| HTTP | error | Meaning |
|---|---|---|
| 400 | "Bad request" | prompt missing or not a string; fields/collections not an array. |
| 400 | prompt_too_long | prompt > 4000 characters. Response includes { limit: 4000 }. |
| 400 | too_many_fields | fields > 500 entries. Response includes { limit: 500 }. |
| 400 | too_many_collections | collections > 100 entries. Response includes { limit: 100 }. |
| 401 | missing_api_key | No Authorization or X-Api-Key header. |
| 401 | invalid_api_key | Bearer token does not match any active key. |
| 401 | revoked_api_key | Key existed but has been revoked from the dashboard. |
| 429 | rate_limited | Either daily quota (from the portal — includes { used, limit }) or per-key burst limit (60 req / 60s default — includes { retryAfter }). |
| 500 | server_misconfigured | Operations issue on our end. Retry; if it persists, status.insightomate.ai will say so. |
| 500 | internal_error | Unhandled exception. Retry with backoff. |
| 502 | verify_upstream_error | The portal returned something unexpected. Transient; retry. |
| 504 | verify_timeout | The portal didn't answer the auth call in time. Transient; retry. |
Every response — including 2xx — carries:
X-Request-Id— opaque correlation id. Include this when reporting a bug.X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset— current per-key bucket state.
429 responses additionally include Retry-After in seconds.
Example — cURL
curl -X POST https://nlp-engine-prod.lunamic.co/api/analyze \
-H "Authorization: Bearer $IOM_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "show me products under $30",
"fields": ["name", "price", "category", "stock"],
"collections": ["products", "orders", "customers"]
}'
Example — Node
const res = await fetch("https://nlp-engine-prod.lunamic.co/api/analyze", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IOM_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "top 10 customers by revenue this quarter",
fields: ["id", "name", "total", "createdAt"],
collections: ["customers", "orders"],
}),
});
if (res.status === 429) {
const retry = Number(res.headers.get("Retry-After") || "1");
await new Promise((r) => setTimeout(r, retry * 1000));
// …retry
}
const { analysis, plan, usage } = await res.json();
// Hand `analysis` to your query generator. The sidecar's generator
// in this repo is one reference implementation.