---
title: "Get Marketing Failures"
method: GET
path: "/internal/reliability/marketing/failures"
tags: ["internal"]
---

# Get Marketing Failures

`GET /internal/reliability/marketing/failures`

Week-over-week marketing analysis failure rates for the requested scope.

Scope widens or narrows with the params:
- no `company_id`           -> all companies, breakdown by company
- `company_id`              -> that company, breakdown by marketing team
- `company_id` + `team_id`  -> that team, no further breakdown

## Query parameters

- `weeks` integer
- `sla_hours` integer
- `company_id` string, uuid, nullable
- `team_id` string, uuid, nullable

## Response `200`

Successful Response

- MarketingFailuresResponse — Failure trend + next-level breakdown for marketing analyses. A single response drives the failure chart (`weekly`), the KPI cards (`totals`), and the drill-down table (`breakdown`) for whatever scope was requested, plus citation coverage (`citations` totals + `citation_weekly` trend), the risk-action funnel (`risk_actions` totals + `risk_action_weekly` trend -- action rate and flag rate) for the risks those runs surfaced, run-latency stats (`run_latency` totals + `run_latency_weekly` trend -- queue wait, prep + analysis, and end-to-end together), and feedback signal & coverage (`feedback_signal` totals + `feedback_signal_weekly` trend -- validated rate, coverage gap, outdated share).
  - `weekly` WeeklyFailurePoint[], required
    - `week_start` string, date, required
    - `total` integer, required
    - `failed` integer, required
    - `failure_pct` number, required
  - `breakdown` EntityFailureRow[], required
    - `id` string, uuid, nullable, required
    - `name` string, required
    - `total` integer, required
    - `failed` integer, required
    - `failure_pct` number, required
    - `total_risks` integer, required
    - `actioned` integer, required
    - `actioned_pct` number, required
    - `flagged` integer, required
    - `flag_rate_pct` number, required
    - `with_citations` integer, required
    - `with_citations_pct` number, required
  - `totals` FailureTotals, required — Window-wide totals for the current scope (KPI cards).
    - `total` integer, required
    - `failed` integer, required
    - `failure_pct` number, required
  - `citations` CitationCoverage, required — How many risks in the current scope carry at least one citation. A risk either has citation(s) (page + clause evidence) or it does not, so `with_citations + without_citations == total_risks`. Counts every risk surfaced by the marketing runs in the window (including ones later superseded by a re-run).
    - `total_risks` integer, required
    - `with_citations` integer, required
    - `without_citations` integer, required
    - `with_citations_pct` number, required
  - `citation_weekly` WeeklyCitationPoint[], required
    - `week_start` string, date, required
    - `total_risks` integer, required
    - `with_citations` integer, required
    - `with_citations_pct` number, required
  - `risk_actions` RiskActionCoverage, required — The risk-action funnel for the current scope: of the marketing risks surfaced, how many a reviewer acted on, and of those how many were flagged. A funnel over risks: `total_risks -> actioned -> flagged`. - `actioned` = risks a reviewer decided on (status flag / allow / reject); risks not yet reviewed (status NULL) are excluded. - `flagged` = the subset of actioned risks with status flag. - `actioned_pct = actioned / total_risks` -- the action rate: share of surfaced risks a reviewer engaged with at all. - `flag_rate_pct = flagged / actioned` -- the flag rate: share of reviewed risks flagged, matching the marketing analytics definition. So `flagged <= actioned <= total_risks`.
    - `total_risks` integer, required
    - `actioned` integer, required
    - `actioned_pct` number, required
    - `flagged` integer, required
    - `flag_rate_pct` number, required
  - `risk_action_weekly` WeeklyRiskActionPoint[], required
    - `week_start` string, date, required
    - `total_risks` integer, required
    - `actioned` integer, required
    - `actioned_pct` number, required
    - `flagged` integer, required
    - `flag_rate_pct` number, required
  - `run_latency` RunLatencyTotals, required — Marketing run-latency stats for the scope, split queue vs analysis. Three latencies over the same set of runs, so they decompose one journey: - `queue_*` = `analysis_started_at - run_time` -- created until a worker actually began. This is dispatch/queue delay, not work. - `analysis_*` = `analysis_completed_at - analysis_started_at` -- **file prep plus analysis**, not the analysis alone. The worker stamps `analysis_started_at` before it blocks waiting for uploads to finish processing, so this span can contain minutes of file-pipeline wait. A spike here does not on its own implicate the analysis or the model. - `end_to_end_*` = `analysis_completed_at - run_time` -- what the person who submitted the run waited, the two above together. **Three statistics per latency, because one is not enough.** P50 and P95 describe the bulk and the tail; `queue_max_seconds` catches what percentiles structurally cannot. A handful of runs stalled for hours out of a hundred normal ones does not reach the 95th percentile, so P95 can sit flat across a week containing a multi-hour stall while the max moves by orders of magnitude. Conversely the max is one row and says nothing about how widespread a delay is -- `queued_over_threshold` (runs whose queue wait exceeded `queue_threshold_seconds`) is the breadth signal. Read them together; each is blind to a different failure. **`queued_over_threshold` is a count and must stay one.** Every other metric on the reliability page carries a companion percentage, so the pull to normalise this one is strong -- but dividing it by `runs` inverts the ranking whenever volume moves. On the incident week, breaches rose in absolute terms while the *rate* fell below the neighbouring quiet weeks, purely because run volume roughly doubled. As a rate the worst week reads as the best. Report the count; put the denominator beside it if context is needed. Maxima are per-column and so may come from three different runs: unlike the percentiles they do **not** decompose, and `queue_max + analysis_max` is no particular run's end-to-end. Use the slowest-runs drill-down to see one run's real split. **`runs` is the measurement denominator, and it is not the number of runs that happened.** Every figure here is over successful runs carrying both analysis timestamps in a consistent order. Both timestamps are written in one update when results land, so a run that failed or is still queued carries neither -- it contributes nothing, and its wait is not measurable at all. These are survivor latencies and the true tail is worse than shown. Compare `runs` against the same week's `WeeklyFailurePoint.total` to see how much of the week was measurable. Every latency field is null when `runs` is zero (no measurement), never zero.
    - `runs` integer, required
    - `queue_p50_seconds` number, nullable, required
    - `queue_p95_seconds` number, nullable, required
    - `queue_max_seconds` number, nullable, required
    - `analysis_p50_seconds` number, nullable, required
    - `analysis_p95_seconds` number, nullable, required
    - `analysis_max_seconds` number, nullable, required
    - `end_to_end_p50_seconds` number, nullable, required
    - `end_to_end_p95_seconds` number, nullable, required
    - `end_to_end_max_seconds` number, nullable, required
    - `queued_over_threshold` integer, nullable, required
    - `queue_threshold_seconds` integer, required
    - `excluded_inconsistent` integer, required
  - `run_latency_weekly` WeeklyRunLatencyPoint[], required
    - `week_start` string, date, required
    - `runs` integer, required
    - `queue_p50_seconds` number, nullable, required
    - `queue_p95_seconds` number, nullable, required
    - `queue_max_seconds` number, nullable, required
    - `analysis_p50_seconds` number, nullable, required
    - `analysis_p95_seconds` number, nullable, required
    - `analysis_max_seconds` number, nullable, required
    - `end_to_end_p50_seconds` number, nullable, required
    - `end_to_end_p95_seconds` number, nullable, required
    - `end_to_end_max_seconds` number, nullable, required
    - `queued_over_threshold` integer, nullable, required
  - `feedback_signal` FeedbackSignalCoverage, required — Feedback signal & coverage for the current scope — a lens on Kobalt's own risk output, distinct from the review-quality funnel above. "Validated"/"actioned"/"outdated" are measured over Kobalt-generated risks only (`created_by_user_id IS NULL`); the coverage gap is measured over all risks so it captures reviewer-added feedback Kobalt missed. - `actioned` = Kobalt risks a reviewer dispositioned: `status` set (flagged/allowed/dismissed) OR outdated (`made_obsolete_by NOT NULL`). - `flagged` / `allowed` / `dismissed` / `outdated` partition `actioned` (they sum to it): outdated takes precedence, so `flagged`/`allowed`/ `dismissed` are that status AND *not* outdated, and `outdated` is every outdated risk regardless of status. - `validated` = flagged + outdated; `validated_pct = validated / actioned` — of the risks a reviewer acted on, the share kept as valid. - `outdated_share_pct = outdated / validated` — of validated feedback, the share that was outdated rather than flagged. - `uncaptured` = reviewer-added risks (`created_by_user_id NOT NULL`); `coverage_gap_pct = uncaptured / all_risks` — feedback Kobalt did not capture. So `flagged + allowed + dismissed + outdated == actioned`, `validated == flagged + outdated`, `outdated <= validated <= actioned <= ai_risks`, and `ai_risks + uncaptured == all_risks`. `dismiss_reasons` splits `dismissed` by reviewer-stated reason (a derived classification, no schema change); `over_flag_rate_pct = over_flagged / actioned` — the share of dispositioned risks the reviewer marked as an over-flag (false positive).
    - `ai_risks` integer, required
    - `actioned` integer, required
    - `validated` integer, required
    - `validated_pct` number, required
    - `flagged` integer, required
    - `outdated` integer, required
    - `allowed` integer, required
    - `dismissed` integer, required
    - `dismiss_reasons` DismissReasonBreakdown, required — How dismissed marketing risks split by reviewer-stated reason. A derived classification over the existing ``dismiss_reason`` column (no schema change; see ``backend.defs.runs.DISMISS_REASON_BUCKET``). Measured over Kobalt-generated, non-outdated risks a reviewer dismissed (``status`` in reject/dismissed) — the same population as ``FeedbackSignalCoverage.dismissed``. - ``over_flagged`` = the analysis over-flagged (reasons: incorrect / out_of_scope / already_met) — false positives to tighten rules on. - ``accepted`` = a real finding the reviewer accepted as fine (reasons: valid_ok / pre_approved) — adjust severity/wording, not the trigger. - ``other`` = everything else: the ``other`` reason, a ``dismissed`` with no reason, and legacy ``reject`` (which never carried a reason). - ``by_reason`` = per-reason counts keyed by the wire value (e.g. ``"incorrect"``), plus a ``"reject"`` key for dismissals with no structured reason (legacy ``reject`` / a ``dismissed`` left unspecified). So ``over_flagged + accepted + other == sum(by_reason.values()) == FeedbackSignalCoverage.dismissed``.
      - `over_flagged` integer, required
      - `accepted` integer, required
      - `other` integer, required
      - `by_reason` object, required
    - `over_flag_rate_pct` number, required
    - `outdated_share_pct` number, required
    - `all_risks` integer, required
    - `uncaptured` integer, required
    - `coverage_gap_pct` number, required
  - `feedback_signal_weekly` WeeklyFeedbackSignalPoint[], required
    - `week_start` string, date, required
    - `ai_risks` integer, required
    - `actioned` integer, required
    - `validated` integer, required
    - `validated_pct` number, required
    - `flagged` integer, required
    - `outdated` integer, required
    - `allowed` integer, required
    - `dismissed` integer, required
    - `dismissed_over_flagged` integer, required
    - `dismissed_accepted` integer, required
    - `dismissed_other` integer, required
    - `over_flag_rate_pct` number, required
    - `outdated_share_pct` number, required
    - `all_risks` integer, required
    - `uncaptured` integer, required
    - `coverage_gap_pct` number, required
  - `breakdown_level` 'company' | 'team' | 'none', required
  - `weeks` integer, required
  - `sla_hours` integer, required

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/kobaltlabs/apis/fastapi.md) · [All operations](https://skmtc.dev/kobaltlabs/apis/fastapi/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/kobaltlabs/fastapi/revisions/3ff4c0bd276b/schema)
