Planara Intelligence Layer
Platform Concepts

Widget API

Widget endpoints project typed slices of query responses for embedding in custom frontends and external tools.

What widget endpoints are

Widget endpoints return typed slices of a Planara query response. Instead of consuming the full QueryResponse object (which contains procedures, parts, safety warnings, specs, citations, and more), a consumer can request just the data it needs for a specific widget.

This matters because the product is the API. The reference frontend (frontend-v2) is one consumer. A customer's existing DMS, field app, or service portal is another. Widget endpoints let any consumer embed individual Planara capabilities without parsing the full response.

How they work

Widget consumption flow — query, stored response, widget endpoint projections, consumer applications

Widget endpoints are a projection layer on top of the core query pipeline. The flow is:

  1. Query first -- call POST /v1/query (or /v1/query/stream) with a session_id. The pipeline runs: retrieval, CKL enrichment, generation, validation. The full response is stored server-side (24h TTL).
  2. Project via widget endpoints -- call GET /v1/widgets/{namespace}/{widget}?session_id={id} to retrieve a typed slice of that stored response.

Widget endpoints do not re-run the pipeline. They read from the stored response and return the relevant fields. This makes them fast (sub-50ms) and safe to call multiple times.

For integration-derived widgets (queue, telemetry), no prior query is needed -- these endpoints dispatch directly to the configured data source.

Quick start

Interactive API docs

The API auto-generates an OpenAPI specification. Use these to explore endpoints, inspect request/response schemas, and test calls directly from the browser:

Minimal example

Three calls to go from question to rendered widgets:

Step 1 -- Submit a query

const sessionId = crypto.randomUUID();

const queryRes = await fetch("https://your-api-domain.planara.com/v1/query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    query: "How do I change the oil on this engine?",
    role: "technician",
    namespace: "your-namespace",
    session_id: sessionId,
  }),
});

const queryData = await queryRes.json();
// queryData contains the full QueryResponse -- but you don't need to parse it all.

Step 2 -- Get the assessment widget

const assessmentRes = await fetch(
  `https://your-api-domain.planara.com/v1/widgets/your-namespace/assessment?session_id=${sessionId}`
);
const assessment = await assessmentRes.json();
// { summary, confidence, confidence_badge, confidence_tier, query_type, active_skill, specifications }

Step 3 -- Get procedure steps

const procedureRes = await fetch(
  `https://your-api-domain.planara.com/v1/widgets/your-namespace/procedure?session_id=${sessionId}`
);
const procedure = await procedureRes.json();
// { procedures[], safety_warnings[] }

Widget endpoints are projections of the stored query response -- they return instantly (sub-50ms) and can be called independently or in parallel.

Streaming example

For chat UX, use the SSE stream endpoint to show progress and stream text as it generates:

const res = await fetch("https://your-api-domain.planara.com/v1/query/stream", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    query: "What are the torque specs for the cylinder head?",
    role: "technician",
    namespace: "your-namespace",
    session_id: crypto.randomUUID(),
  }),
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop(); // keep incomplete line in buffer

  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const event = JSON.parse(line.slice(6));

    switch (event.type) {
      case "thinking":
        // Pipeline started -- show a spinner
        break;
      case "retrieved":
        // Source content retrieved -- you can preview citations before generation finishes
        break;
      case "generating":
        // Answer generation started
        break;
      case "summary_token":
        // Partial summary text -- append to chat bubble for real-time typing effect
        appendToChat(event.token);
        break;
      case "complete":
        // Full structured response -- same shape as POST /v1/query response
        renderWidgets(event.data);
        break;
    }
  }
}

The summary_token events give you readable text as the answer is generated -- useful for a chat-style UX where users see the answer appear incrementally. The complete event delivers the full structured response with procedures, parts, citations, and everything else.

Page images

Citations reference specific pages from the source manuals. Render them with the page image endpoint:

// Thumbnail (300px wide, fast)
const thumbUrl = `https://your-api-domain.planara.com/v1/pages/your_doc_id/42`;

// Full resolution (original scan quality)
const fullUrl = `https://your-api-domain.planara.com/v1/pages/your_doc_id/42?quality=full`;

These are the original manual pages -- not AI-generated content. Use them alongside citations so technicians can verify information against the source material.

The page_images array in the citations widget response gives you the paths directly:

const citations = await citationsRes.json();
// citations.page_images = ["/v1/pages/your_doc_id/42", "/v1/pages/your_doc_id/43"]

Authentication

The API supports per-tenant API keys:

  • Keys are scoped per namespace -- a key for tenant-a cannot access tenant-b. The key's namespace overrides whatever namespace the request body specifies.
  • Keys are passed via the Authorization: Bearer <key> header.
  • Keys are generated and revoked in the backoffice, under the tenant's API keys page.

Whether unauthenticated requests are accepted is a per-deployment setting. Deployments serving real customer data require a key on every query; demo and evaluation deployments may run open with rate limiting. If a request is rejected with 401, include your API key in the Authorization header. Endpoint URLs and response shapes are identical with or without authentication.

Endpoint catalog

Intelligence-derived widgets

These project from a stored query response. All require session_id as a query parameter.

EndpointReturnsFields
GET /v1/widgets/{ns}/assessmentAI assessmentsummary, confidence, confidence_tier, query_type, active_skill, specifications
GET /v1/widgets/{ns}/procedureProcedure stepsprocedures[], safety_warnings[]
GET /v1/widgets/{ns}/safetySafety warningssafety_warnings[]
GET /v1/widgets/{ns}/partsParts listparts[]
GET /v1/widgets/{ns}/specificationsSpecificationsspecifications{}
GET /v1/widgets/{ns}/citationsCitationscitations[], citation_map[], page_images[]

Integration-derived widgets

These dispatch to the configured data source (mock, integration, or none). No session_id required.

EndpointReturnsData source
GET /v1/widgets/{ns}/queueWork order queueMock or DMS integration
GET /v1/widgets/{ns}/telemetryEngine telemetryMock or telematics integration

Diagram highlight endpoint

In addition to the standard widget projections, the API provides a diagram highlighting endpoint:

EndpointReturnsFields
GET /v1/diagrams/{ns}/{doc_id}/{page}/highlightHighlighted diagram imagePNG bytes with component bounding boxes drawn on the original page image

Query parameters:

ParameterDescription
componentsComma-separated component names to highlight (e.g., thermostat,water pump)
qualitythumb (default, 300px wide) or full (original resolution)

The highlight endpoint reads pre-computed diagram analysis data (generated by the diagram analysis preprocessing step) and draws semi-transparent bounding boxes around matching components. If no analysis exists for the page, or no components match, the original page image is returned as a fallback.

This endpoint is used by the technician frontend's procedure widget -- when a procedure step references a component that has been analyzed, the diagram appears with the relevant part highlighted in sky blue. See Diagram Highlighting for the full workflow.

Response shapes

Assessment

{
  "summary": "This engine requires 7.4 quarts of manufacturer-recommended oil...",
  "confidence": 0.85,
  "confidence_badge": "verified",
  "confidence_tier": {
    "tier": "verified",
    "label": "From the manual",
    "description": "Answer sourced directly from manufacturer documentation"
  },
  "query_type": "procedural",
  "active_skill": "maintenance",
  "specifications": {
    "Oil Capacity": "7.4 quarts (7.0 L)",
    "Oil Type": "10W-30 (manufacturer recommended)"
  }
}

Confidence tier

Every response includes a confidence_tier object with a qualitative assessment of sourcing quality. Use this instead of the raw confidence float for user-facing display.

TierLabelInternal scoreMeaning
verified"From the manual">= 0.70Strong match to manufacturer documentation. Fully grounded and cited.
informed"Based on related info"0.35 -- 0.69Drawn from related sections. May involve inference across multiple sources.
uncertain"Verify before acting"< 0.35Limited documentation found. Answer should be verified with additional sources.

The raw confidence float (0--1) and legacy confidence_badge string are still included for backward compatibility and internal analytics. For tech-facing UI, use confidence_tier.label and confidence_tier.tier to drive display.

Procedure

{
  "procedures": [
    {
      "step": 1,
      "action": "Remove the oil drain plug and drain the oil completely.",
      "detail": "Place a drain pan beneath the engine...",
      "safety_refs": [0],
      "ckl_refs": []
    }
  ],
  "safety_warnings": [
    {
      "index": 0,
      "severity": "WARNING",
      "text": "Engine oil is hot. Allow engine to cool before draining.",
      "source": "your_doc_id",
      "page": 42
    }
  ]
}

Safety

{
  "safety_warnings": [
    {
      "index": 0,
      "severity": "DANGER",
      "text": "Disconnect the battery before performing electrical work.",
      "source": "your_doc_id",
      "page": 15
    }
  ]
}

Parts

{
  "parts": [
    {
      "name": "Oil Filter",
      "part_number": "PART-13440-04",
      "quantity": 1,
      "verified": true
    }
  ]
}

Specifications

{
  "specifications": {
    "Spark Plug Gap": "0.9-1.0 mm (0.035-0.039 in)",
    "Ignition Timing": "BTDC 0° at 1000 rpm"
  }
}

Citations

{
  "citations": [
    {
      "source": "your_doc_id",
      "page": 42,
      "section": "Periodic Maintenance",
      "text": "Change engine oil every 100 hours..."
    }
  ],
  "citation_map": [
    { "index": 1, "source": "your_doc_id", "page": 42 }
  ],
  "page_images": ["/v1/pages/your_doc_id/42"]
}

Queue

{
  "jobs": [
    {
      "id": "WO-2024-0042",
      "customer_name": "Bay Marine Services",
      "equipment": "Model A",
      "complaint": "Engine overheating at WOT",
      "status": "in_progress",
      "priority": "high"
    }
  ]
}

Telemetry

{
  "enabled": true,
  "data": {
    "engine_id": "6CF-1042857",
    "events": [
      { "code": "P0217", "description": "Engine Coolant Over Temperature", "timestamp": "2026-03-22T14:30:00Z" }
    ],
    "fields": {
      "rpm": 5200,
      "coolant_temp_f": 185,
      "oil_pressure_psi": 45
    }
  }
}

Chat card types

Chat responses compose structured cards inline with text. Each card type renders a specific data slice from the query response.

Seven chat card types — safety, spec, parts, page preview, new assessment, follow-up questions, service timeline

Intelligence-derived vs integration-derived

The distinction matters for how data flows and how the Dashboard Configuration controls each widget.

Intelligence-derived widgets (assessment, procedure, safety, parts, specifications, citations):

  • Data comes from the intelligence pipeline -- AI-generated, structured, cited
  • Require a prior POST /v1/query call with a session_id
  • Data source is always intelligence in the dashboard config
  • Response is cached for 24 hours

Integration-derived widgets (queue, telemetry):

  • Data comes from external systems via the adapter pattern
  • Do not require a prior query
  • Data source is mock, integration, or none in the dashboard config
  • When set to integration, the endpoint dispatches to the configured adapter (e.g., Lightspeed for DMS, Siren Marine for telemetry)

Caching behavior

All widget endpoints return Cache-Control: private, max-age=300 headers (5 minutes). This means:

  • Browsers and CDNs will cache the response for 5 minutes
  • The private directive prevents shared caches (CDNs) from storing responses meant for a specific user
  • After 5 minutes, the next request will hit the server and read from the stored response

The underlying query responses in the database have a 24-hour TTL. After that, the widget endpoint returns 404 and the consumer must re-query.

Additional request parameters

skip_cache

Pass skip_cache: true on the POST /v1/query request body to bypass the retrieval cache and force a fresh pipeline run. Useful when you've just ingested new documents and want to verify the updated content is being retrieved.

const res = await fetch("https://your-api-domain.planara.com/v1/query", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    query: "What are the torque specs for the cylinder head?",
    role: "technician",
    namespace: "your-namespace",
    session_id: crypto.randomUUID(),
    skip_cache: true,
  }),
});

Additional response fields

follow_up_questions

Every query response includes a follow_up_questions array -- AI-generated clarifying or next-step questions based on the current response. The technician frontend renders these as tappable chips below the chat response.

{
  "follow_up_questions": [
    "What torque wrench setting should I use?",
    "Do I need to replace the gasket when re-torquing?",
    "What is the re-torque interval after break-in?"
  ]
}

Use these to drive conversational flow. Each follow-up can be submitted as a new query with the same session_id to maintain conversation context.

response_type

Every query response carries a response_type discriminator that tells the consumer which rendering mode to use. Most responses are oem_answer — the default, full cited answer. The values a consumer should handle:

ValueMeaningRendering guidance
oem_answerFull cited answer from the equipment documentation (default)Standard answer rendering
ckl_answerAnswer drawn from the industry compliance layer (CKL) rather than the equipment manualLabel as general industry knowledge
needs_clarificationThe question is ambiguous — for example, between two near-identical systems or machines. The response is a question, not an answerRender the clarification question and options as tappable choices. Never render as an answer card
knowledge_gapThe documentation does not cover the question. The summary says so explicitly, keeps what was understood, points to the closest documented information, and gives a concrete next stepSuppress procedure, parts, and follow-up chip rendering — do not imply an answer exists
refusalOut-of-scope question, declinedShort message, no answer card
continuationContinuation of the previous answer, no new retrievalInline with the prior answer
inventoryListing of the available documentationRender as a list
inline_chatConversational aside (greeting, small talk)Plain chat bubble
ack / navigationAcknowledgment or UI-navigation responseMinimal or no rendering
safety_emergency / safety_hazardLife-safety or hazardous-equipment-state override — safety guidance replaces the normal answerRender the safety warnings prominently, before anything else

Two of these are honesty guarantees, enforced server-side: a clarifying question is always typed needs_clarification (never served as a confident oem_answer), and a knowledge_gap response always states the gap plainly rather than restating the question — a bare restatement is never the answer.

clarification

When response_type is needs_clarification, the response includes a clarification object with the question to display and selectable options:

{
  "response_type": "needs_clarification",
  "summary": "Are you asking about the engine-driven pump, or the electric transfer pump?",
  "clarification": {
    "question": "Are you asking about the engine-driven pump, or the electric transfer pump?",
    "options": [
      { "id": "doc-id-a", "label": "Engine-driven pump" },
      { "id": "doc-id-b", "label": "Electric transfer pump" }
    ]
  }
}

Render the options as buttons. Submitting a selection (a new query with the same session_id) re-asks the original question scoped to that system. The option labels are also mirrored into follow_up_questions, so a consumer that only renders follow-up chips still presents the choices. clarification is null on every other response type.

Error handling

StatusMeaning
200Widget data returned
404No stored response found for this session -- either the session expired (24h TTL) or no query was made
429Rate limited (inherits from global rate limiting)
500Server error