Simulix

API · post-hoc

Reading

Post-hoc analysis of a completed simulation. A separate, smaller model reads the transcripts after the calibrated run and returns an 8-block payload: synthesis, patterns, reservations, attractions, intensity, quotes, segment intensity, and across-runs.

Overview

Every simulation completes with the canonical

verdict
+
by_segment
aggregates on the existing
/results
endpoint. Reading is the layer that sits beneath: post-hoc enrichment of the same transcripts, rendered as observational prose blocks under a separate
post_hoc
envelope.

Reading is generated asynchronously after the sim completes. Typical latency: 30-60 seconds. Per-sim cost: ~$0.05 (subject to the per-tier monthly ceiling). Conceptually separate from the calibrated verdict above — see /docs/concepts/post-hoc-reading for the model and the stability contract.

Authentication

Bearer token via the

Authorization
header. Both endpoints require the
insights:read
scope. Keys minted before v1.0.7b that carry
simulations:read
are automatically granted
insights:read
by the cascade migration — existing integrations keep working without a key rotation. New keys can grant
insights:read
alone for a least-privilege reporting backend.

Preview signal

Until v1.0.8, every

/insights
response carries an HTTP header:

X-Simulix-Preview: insights

The header is a signal — not a warning — that the payload shape may evolve once production telemetry beds in. Field names + the eight block names + the

post_hoc
envelope key are already stable per the three-tier contract documented at /docs/concepts/post-hoc-reading. The header is removed in v1.0.8.

GET /v1/simulations/{id}/insights

Read the Reading payload for one simulation. Always returns 200 regardless of lifecycle state; branch on

insights_status
to know what is available.

Response — completed

{
  "data": {
    "simulation_id": "0190a8b4-1c34-7d2a-9e87-2c4f3b5a6d8e",
    "insights_status": "completed",
    "block_status": {
      "synthesis": "completed",
      "patterns": "completed",
      "reservations": "completed",
      "attractions": "completed",
      "intensity": "completed",
      "quotes": "completed",
      "segment_intensity": "completed",
      "across_runs": "not_applicable"
    },
    "calibration_boundary": {
      "kind": "post_hoc_uncalibrated",
      "disclaimer": "These passages were read after the run completed by a separate small model. They are not part of the calibrated verdict above.",
      "model_tier": "cloudflare-llama-mixed-8b-70b"
    },
    "post_hoc": {
      "synthesis": { "text": "Respondents weighed the price against routine cost, with most settling on a maybe.", "confidence": "soft" },
      "patterns": { "patterns": [ /* up to 6 InsightPattern objects */ ], "method_note": "..." },
      "reservations": { "reservations": [ /* up to 5 InsightReservation objects */ ] },
      "attractions": { "attractions": [ /* up to 5 InsightAttraction objects */ ] },
      "intensity": { "distribution": { "1": 12, "2": 28, "3": 81, "4": 47, "5": 32 }, "median": 3.2, "note": "..." },
      "quotes": { "yes": { /* InsightQuote */ }, "no": { /* InsightQuote */ } },
      "segment_intensity": { "by_segment": [ /* SegmentIntensityRow per segment */ ] },
      "across_runs": null
    },
    "generated_at": "2026-05-21T12:34:56Z",
    "cost_usd_estimate": 0.087
  },
  "error": null,
  "meta": {}
}

Response — running

{
  "data": {
    "simulation_id": "0190a8b4-...",
    "insights_status": "running",
    "block_status": null,
    "calibration_boundary": {
      "kind": "post_hoc_uncalibrated",
      "disclaimer": "...",
      "model_tier": "cloudflare-llama-mixed-8b-70b"
    },
    "post_hoc": null,
    "generated_at": null,
    "cost_usd_estimate": null,
    "error_message": null
  },
  "error": null,
  "meta": { "next_poll_after_seconds": 5 }
}

Includes

Retry-After: 5
header. Resume polling at the suggested cadence.

Response — not_started (legacy / opt-out)

{
  "data": {
    "simulation_id": "0190a8b4-...",
    "insights_status": "not_started",
    "post_hoc": null
  },
  "error": null,
  "meta": {
    "generate_url": "https://api.simulix.com/v1/simulations/0190a8b4-.../insights/generate"
  }
}

Returned for sims that completed before v1.0.7b shipped, or for sims minted with

generate_insights: false
. POST to
meta.generate_url
to queue retroactive Reading.

Field reference

FieldTypeNull?Notes
simulation_idstring (uuid)noID of the parent simulation
insights_statusenumnonot_started | queued | running | completed | failed | disabled
block_statusobject | nullyesPer-block lifecycle map; null until the pipeline finishes
calibration_boundaryobject | nullyesDisclaimer string + model tier; render verbatim in your own UI
post_hocobject | nullyesThe 8-block payload; null while not-yet-completed or on failure
generated_atstring (ISO-8601) | nullyesCompletion timestamp; null until the row finalizes
cost_usd_estimatenumber | nullyesPer-sim Reading cost in USD; null until completion
error_messagestring | nullyesPopulated when insights_status is failed or disabled

POST /v1/simulations/{id}/insights/generate

Queue retroactive Reading generation for a completed simulation that has no row yet (legacy sim, or one that opted out at create time). Body is empty.

Response — 202

{
  "data": {
    "simulation_id": "0190a8b4-...",
    "insights_status": "queued",
    "poll_url": "https://api.simulix.com/v1/simulations/0190a8b4-.../insights",
    "estimated_completion_time_seconds": 30
  },
  "error": null,
  "meta": {}
}

insights_status on /v1/simulations/{id}

The status endpoint carries an additive optional

insights_status
field so a polling integrator can piggyback on the same call to know when to fetch the Reading payload. Existing SDKs that don't read the field keep working unchanged.

{
  "data": {
    "id": "0190a8b4-...",
    "workflow_id": "cdc_nhis_adult_smoking",
    "status": "completed",
    "results_available": true,
    "results_url": "https://api.simulix.com/v1/simulations/0190a8b4-.../results",
    "insights_status": "running"
  }
}

Webhook events

When a simulation was created with a

callback_url
, two new event types fire as the Reading pipeline finalizes. Same HMAC signing scheme as
simulation.completed
(see /docs/webhooks).

simulation.insights.completed

{
  "id": "evt_a1b2c3d4e5f6...",
  "type": "simulation.insights.completed",
  "created": 1716295496,
  "data": {
    "simulation_id": "0190a8b4-...",
    "insights_status": "completed",
    "block_status": {
      "synthesis": "completed",
      "patterns": "completed",
      "reservations": "completed",
      "attractions": "completed",
      "intensity": "completed",
      "quotes": "completed",
      "segment_intensity": "completed",
      "across_runs": "not_applicable"
    },
    "cost_usd_estimate": 0.087,
    "generated_at": "2026-05-21T12:34:56Z",
    "insights_url": "/v1/simulations/0190a8b4-.../insights"
  }
}

simulation.insights.failed

{
  "id": "evt_a1b2c3d4e5f6...",
  "type": "simulation.insights.failed",
  "created": 1716295496,
  "data": {
    "simulation_id": "0190a8b4-...",
    "insights_status": "failed",
    "error_message": "orchestrator_crash: cloudflare 503 after 3 retries",
    "generated_at": "2026-05-21T12:34:56Z",
    "insights_url": "/v1/simulations/0190a8b4-.../insights"
  }
}

No webhook fires when a sim is created with

generate_insights: false
— the row inserts directly as
status="disabled"
and there is no terminal event to notify on.

Errors

EndpointStatusError codeWhen
GET /insights404simulation_not_foundSim does not exist or belongs to another org
GET /insights403forbidden_missing_scopeKey lacks insights:read scope
POST /insights/generate409simulation_not_completeSim has not finished yet
POST /insights/generate409insights_already_generatedRow exists at status='completed'; GET it instead
POST /insights/generate422insights_disabledOrg has insights_enabled=false
POST /insights/generate429insights_cost_cap_reachedOrg over monthly Reading ceiling

Next