POST /api/analyze

Request body

{
  "prompt": "show me products under $30",
  "fields": ["name", "price", "category", "stock"],
  "collections": ["products", "orders", "customers"]
}
FieldTypeRequiredNotes
promptstringyesPlain-English question. Max 4000 characters.
fieldsstring[]noField names from the user's local schema, used to bias term resolution. Max 500 entries. No values.
collectionsstring[]noCandidate 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:

FieldTypeNotes
intent"find" | "aggregate"High-level query shape.
operation"$sum" | "$avg" | "$min" | "$max" | "$count" | nullAggregation op. null for plain find.
metricstring | nullField being aggregated (only set when operation != null).
groupBystring[]Field names or time-unit tokens ("day", "week", …).
timeFramestring | { start, end } | nullDetected temporal window. Format depends on whether a range was named.
collectionstring[]Candidate collection names, ranked by confidence.
limitnumber | nullResult 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.
joinsobject[]Required lookups across collections.
computedMetricsobject[]Metrics requiring computation or formulas.
subQueriesobject[]Subqueries required to satisfy the prompt.
visualization"BAR_CHART" | "LINE_CHART" | "PIE_CHART" | "TABLE" | falseChart hint for the renderer.
rawTokensstring[]Preprocessed tokens, for debugging.

Other response fields

FieldTypeNotes
tokensstring[]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.
planTiersame as planAlias 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.
messagestringHuman-readable status. Always "Prompt analyzed successfully" on 200.

Response — errors

All error responses are JSON with at minimum { error, message }.

HTTPerrorMeaning
400"Bad request"prompt missing or not a string; fields/collections not an array.
400prompt_too_longprompt > 4000 characters. Response includes { limit: 4000 }.
400too_many_fieldsfields > 500 entries. Response includes { limit: 500 }.
400too_many_collectionscollections > 100 entries. Response includes { limit: 100 }.
401missing_api_keyNo Authorization or X-Api-Key header.
401invalid_api_keyBearer token does not match any active key.
401revoked_api_keyKey existed but has been revoked from the dashboard.
429rate_limitedEither daily quota (from the portal — includes { used, limit }) or per-key burst limit (60 req / 60s default — includes { retryAfter }).
500server_misconfiguredOperations issue on our end. Retry; if it persists, status.insightomate.ai will say so.
500internal_errorUnhandled exception. Retry with backoff.
502verify_upstream_errorThe portal returned something unexpected. Transient; retry.
504verify_timeoutThe 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.
Last updated: Jul 30 2026