---
title: "Validate Studio Opportunity"
method: POST
path: "/playground/opportunities/validate"
tags: ["AI_PLAYGROUND", "OPPORTUNITIES"]
---

# Validate Studio Opportunity

`POST /playground/opportunities/validate`

Pre-submit smoke test for a Studio → User opportunity draft (AI-7498).

Runs the full publish gates WITHOUT inserting: the primary resource must
verify against tenant inventory, the description must be a description
rather than a placeholder, and the estimated savings must be bounded by the
resource's actual last-30-day spend — cross-checked against the anchoring
analysis turn when the session has one, and against the resource's measured
spend when it does not.

Blocking failures come back as HTTP 422 with the same per-field
``{code, message}`` shape as submit, so the form reuses its error parser. A
pass returns the non-blocking warnings plus ``savings``: the looked-up
monthly spend, which evidence source was used, and the claim as a share of
spend. NO ClickHouse insert, no side effects.

Submit runs the identical gate, so a client cannot bypass these checks by
skipping validate.

## Query parameters

- `navigationSource` string, nullable

## Request body

- SubmitStudioOpportunityRequest — Body for the explicit publish path (``POST /studio/opportunities/submit``). The agent no longer auto-publishes: it emits an editable ``OpportunityFinding`` draft, the user corrects it, and this request publishes it. ``turn_uuid`` is optional — a Mode B (direct-add) finding has no analysis turn to anchor to.
  - `session_id` string, required
  - `turn_uuid` string, nullable
  - `finding` OpportunityFinding, required — A draft finding — the fields a real opportunity carries. Mirrors the tissues cost_savings_ops shape a published opportunity actually stores: a title, a description, the target resource(s), a monthly savings estimate, an effort level, and the Issue/Goal/Context ``agent_instructions`` markdown shown in the opportunity detail's "Agent Instructions" tab.
    - `opportunity_id` string
    - `title` string, required
    - `description` string
    - `resource_name` string
    - `resources` FindingResource[]
      - `resource_name` string, required
      - `resource_type` string, required
      - `resource_uri` string
      - `resource_id` string
      - `workspace_id` string
      - `instance_id` integer, nullable
      - `verified` boolean
    - `estimated_savings_usd_per_month` number
    - `effort` 'low' | 'medium' | 'high'
    - `agent_instructions` string
  - `allow_update` boolean

## Response `200`

Successful Response

- ValidateStudioOpportunityResponse — 200 body for the pre-submit smoke test (``POST /studio/opportunities/validate``). Runs the publish gates (the primary resource must verify against tenant inventory), the description checks, and the savings-plausibility checks WITHOUT inserting anything. Blocking failures come back as a 422 with the same ``{errors: [{code, message}]}`` shape as submit; a pass returns this body. ``errors`` is always empty on a 200; ``warnings`` carry the non-blocking findings, and ``savings`` reports the basis of the verdict.
  - `valid` boolean
  - `errors` StudioOpportunityValidationErrorItem[]
    - `code` string, required
    - `message` string, required
  - `warnings` StudioOpportunityValidationErrorItem[]
    - `code` string, required
    - `message` string, required
  - `savings` SavingsValidationReport — What the savings claim was actually judged against, so the form can show the user the basis for the verdict instead of a bare pass/fail. ``evidence_source`` is ``session`` when the anchoring analysis turn carried text to check the claim against, ``resource`` when it did not and the resource's measured spend was the only basis, and ``none`` when neither was available (the claim could not be bounded at all).
    - `evidence_source` string
    - `claimed_savings_usd_per_month` number
    - `resource_monthly_spend_usd` number, nullable
    - `spend_source` string
    - `spend_window_days` integer
    - `spend_days_observed` integer
    - `monthly_run_rate_usd` number, nullable
    - `savings_pct_of_spend` number, nullable
    - `session_max_dollar_figure` number, nullable

## Other responses

- `422` — Validation Error

## Changes

- **2026-09-11** `70e0a3e5cb0d` — 1 info
  - added the optional property `savings` to the response with the `200` status

[Change history](https://skmtc.dev/myaltimate/apis/fastapi/changes/playground/opportunities/validate/post.md)

---

[API](https://skmtc.dev/myaltimate/apis/fastapi.md) · [All operations](https://skmtc.dev/myaltimate/apis/fastapi/llms.txt) · [OpenAPI document](https://skmtc.dev/myaltimate/apis/fastapi/revisions/eb8306fa77dd?raw)
