Insight Envelope

Every cached insight returns a typed value, computed_at, expires_at, and a 202 cache_warming contract for first computes.

The envelope

type InsightResponse<T> = {
  name: string                  // stable insight identifier
  value: T                      // ← typed per endpoint, NOT object
  computed_at: ISO8601
  expires_at: ISO8601 | null
  source: {
    kind: "live" | "cached"
    cache_runtime_ms?: number   // present when kind=cached
  }
}

Per-endpoint typing

Every cached-insight endpoint declares its own concrete T. The per-surface specs enumerate all 42 typed payload schemas across Templates, Teams, Campaigns, and Advanced families.

No value: object anywhere. The v1 CachedInsightsResponse envelope is gone. This is Finding 0 closed across Slices 8–13.

Loading state: 202 cache_warming

When a cached insight is computing for the first time, gondor returns 202 cache_warming with a Retry-After header — not the v1 204 No Content. Body:

{
  "status": "cache_warming",
  "retry_after_seconds": 30,
  "computed_at": null
}

Clients poll the same URL until they receive a 200 OK with the typed envelope. v2 does NOT keep the v1 "value: null while warming" pattern.

Cache invalidation

Insights expire per-endpoint (expires_at in the response). Forced recomputation is:

POST /v2/partner/insights/<insight-path>/recompute
Authorization: Bearer <token-with-partner:insights:recompute-scope>

→ 202 Accepted
{
  "recompute_id": "...",
  "poll_url": "/v2/partner/insights/recomputes/<recompute-id>",
  "estimated_completion_at": "2026-06-07T13:30:00Z"
}

Poll the poll_url until status is completed, then re-fetch the insight. Recompute IDs map to internal Oban job IDs; polling returns the live job state mapped to the spec status enum.

Example: objective completion rate, windowed

GET /v2/partner/insights/advanced/objective-completion-rate
  ?period=this_month
  &timezone=America/Chicago

→ 200 OK
{
  "name": "advanced.objective_completion_rate",
  "value": {
    "rate": 0.42,
    "completions": 1284,
    "eligible_contacts": 3057,
    "vs_previous_window": 0.08
  },
  "computed_at": "2026-06-07T12:00:00Z",
  "expires_at": "2026-06-07T13:00:00Z",
  "source": {
    "kind": "cached",
    "cache_runtime_ms": 1840
  }
}

The value field is typed per endpoint. Compare to v1's "value": { ... } with no schema — the v2 envelope guarantees you know the shape at compile time.