> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apostra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Seller RFP Lifecycle

> Create, compose, inspect, revise, and evaluate seller RFP responses through the authenticated v3 MCP API.

The v3 seller RFP workflow turns one request into an immutable, auditable
request-and-response turn. It works through `/mcp/v3`; no Apostra page is
required. The authenticated account must be a Seller Account.

An **RFP** is the seller's durable commercial opportunity or exercise. An
**RFP turn** is one immutable request and its response. The response is the
seller proposal artifact inside that turn; clients do not save a second
seller-side Proposal object.

## Before creating an RFP

Response composition uses the Seller Account's own active Storefront products.
At least one product must have all of these catalog facts:

* a name and at least one priced option declaring a `guaranteed` or
  `non_guaranteed` delivery type;
* at least one channel, including a requested channel when the RFP specifies
  one, with a canonical format whose kind can execute in that channel. For a
  managed source that supplies no channel, the catalog infers a channel only
  when that canonical format maps to exactly one composer-compatible channel.
  Ambiguous formats, including common video and audio formats, stay
  unclassified; set `channels` explicitly with `save_wholesale_product`.
  A non-empty seller-authored channel list always takes precedence;
* an explicit positive fixed or floor price with pricing-option ID, pricing
  model, ISO currency, and a compatible delivery type; and
* at least one canonical URL-free creative format (`format_kind` plus `params`).

Missing product, price, or format facts are never replaced with an empty
offer. When no complete priced product exists, the turn finishes as
`needs_clarification` and identifies the missing catalog facts instead. When a
complete product exists but cannot execute the buyer's declared channel or
format, the turn finishes `ready` with a bounded `counter_pitch`: the decision
keeps the literal request separate from the substitute and lists every gap.

`pricingStatus` on a wholesale-product read is the source's coverage result,
not a guarantee that the response includes a quoteable option. Read
`composerCompleteness.missing` beside it to see the request-independent channel,
format, or priced-option fact that must be repaired. A particular RFP still
requires an option in its requested currency.
Composition scans at most 1,000 products and applies declared field,
collection, and artifact-size limits. Request prose may contain tabs and line
breaks. Unicode bidirectional controls, C1 controls, and unsafe C0 controls are
rejected in prose. Leading/trailing Unicode whitespace (including TAB/LF/CR) is
trimmed from structural identifiers and field names; the trimmed value must
contain a visible character and no C0/C1 controls, bidi controls, or
line/paragraph separators. A larger catalog or response also ends in explicit clarification;
Apostra never returns a partial proposal as `ready`.

The deterministic planner treats a channel label and a usable execution as
separate facts. For example, a product labelled `video` does not satisfy a
video request when its selected format options contain only `image`; it needs
`video_hosted` or `video_vast`. Explicit `formatKinds` are matched against the
same format kind that makes the declared channel executable, so unrelated
formats on one product cannot be combined into a false match. New typed
requests use `origin.channels`, `origin.preset.channels`, or
`request.dimensions.channels`. Bounded legacy `origin.preset.channel` and
`request.dimensions.channel` values remain accepted and normalized for
compatibility; `origin.channel` is not part of the public `save_rfp` input
schema. These structured declarations populate `decision.requested.channels`.
A channel mentioned in brief prose does not add or override buyer intent. Only structured
`dimensions.productCount` or `constraints.productCount` and
`dimensions.planRoles` or `constraints.planRoles` populate certified buyer
cardinality and roles; prose such as “one-line” or “anchor store” is not
published as authored plan intent. The planner does not certify buyer-authored
strategic roles from channel and format compatibility. It records structured
`planRoles` as requested intent. When the seller is otherwise responding with
a proposal, those roles add a `requested_role_evidence_unavailable` limitation
and yield a `counter_pitch`, even when the catalog otherwise supports the
request; an explicit seller pass remains a pass. Supported execution roles and
rationales are separate seller-derived facts. Measurement intent is certified
only from `request.constraints.measurementRequirements` (or its snake-case
alias), never inferred from the brief. Declared
`reporting_capabilities.available_metrics`
can verify a supported measurement; missing or incomplete reporting evidence
remains an explicit limitation. If the catalog cannot support the literal count, the response also
names that limitation and clearly labels the supported product mix as a
substitute.

When both camel-case and snake-case list aliases are supplied, format kinds,
plan roles, and measurement requirements are merged and deduplicated. Product
count aliases are scalar synonyms: every supplied count across `dimensions`
and `constraints` must match. Conflicting counts are treated as invalid
cardinality intent and produce clarification rather than silently choosing one.

The channel-to-format compatibility vocabulary is closed and deterministic:

| Channel                  | Compatible canonical `format_kind` values                                                                                   |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `display`                | `image`, `html5`, `display_tag`, `image_carousel`, `native_in_feed`, `sponsored_placement`, `responsive_creative`, `custom` |
| `olv`                    | `video_hosted`, `video_vast`, `custom`                                                                                      |
| `social`                 | `image`, `image_carousel`, `video_hosted`, `native_in_feed`, `custom`                                                       |
| `search`                 | `sponsored_placement`, `responsive_creative`, `custom`                                                                      |
| `ctv`                    | `image`, `native_in_feed`, `sponsored_placement`, `video_hosted`, `video_vast`, `custom`                                    |
| `linear_tv`              | `sponsored_placement`, `video_hosted`, `video_vast`, `custom`                                                               |
| `radio`                  | `audio_hosted`, `audio_daast`, `sponsored_placement`, `custom`                                                              |
| `streaming_audio`        | `audio_hosted`, `audio_daast`, `sponsored_placement`, `custom`                                                              |
| `podcast`                | `audio_hosted`, `audio_daast`, `sponsored_placement`, `custom`                                                              |
| `dooh`                   | `image`, `video_hosted`, `video_vast`, `sponsored_placement`, `custom`                                                      |
| `ooh`                    | `image`, `custom`                                                                                                           |
| `print`                  | `image`, `custom`                                                                                                           |
| `cinema`                 | `video_hosted`, `video_vast`, `custom`                                                                                      |
| `email`                  | `image`, `html5`, `responsive_creative`, `custom`                                                                           |
| `gaming`                 | `image`, `html5`, `video_hosted`, `native_in_feed`, `sponsored_placement`, `custom`                                         |
| `retail_media`           | `sponsored_placement`, `native_in_feed`, `image`, `video_hosted`, `custom`                                                  |
| `influencer`             | `image`, `image_carousel`, `video_hosted`, `native_in_feed`, `custom`                                                       |
| `affiliate`              | `sponsored_placement`, `native_in_feed`, `custom`                                                                           |
| `product_placement`      | `sponsored_placement`, `custom`                                                                                             |
| `sponsored_intelligence` | `agent_placement`, `sponsored_placement`, `custom`                                                                          |

Supported structured measurement requirements and their accepted catalog
metrics are:

| Requirement               | Accepted `available_metrics` values                                                      |
| ------------------------- | ---------------------------------------------------------------------------------------- |
| `brand_lift`              | `brand_lift`, `brand_lift_study`                                                         |
| `closed_loop_attribution` | `units_sold`, `incremental_sales_lift`, `new_to_brand_units`, `foot_traffic`             |
| `sales_attribution`       | `units_sold`, `incremental_sales_lift`, `conversion_value`, `roas`, `new_to_brand_units` |

When multiple measurement requirements are supplied, every distinct requirement
is preserved and verified independently; no requirement subsumes another.

A ready semantic document exposes this result at `decision`: `disposition` is
`pitch` or `counter_pitch`; `requested` records channels, format kinds, product
count, and buyer-authored roles; `supported.roles` binds each verified execution
role to a product and readable rationale without claiming that catalog-derived
roles are buyer-authored; and `limitations` lists the exact
gaps. Commercial allocations also carry that `role` and `rationale`. A
single-product allocation explains why the only supported product receives
100%, rather than presenting an unexplained default split. Missing budget or
request-required flight, audience, or creative inputs produces a bounded
counter or clarification, never an apparently complete approval request.
When the buyer requests an exact demographic age range that the seller's
catalog cannot execute, the decision is likewise a `counter_pitch`: it keeps
the available proposal, names that age targeting was not applied, and lists
the exact supported ranges (or states that none are declared).
In `semantic_document_json`, `commercial.pricing.currency` is nullable only for
an explicit product-free pass without a buyer budget; `totalBudget` is omitted
in that case. `buyer_proposal_json` emits no budget guidance when no total is
available. Its published Proposal shape and field set are unchanged: allocation
`productId` contains the source-qualified `wh:` wholesale product identifier and
`pricingOptionId` contains the corresponding round-trippable `whpo:` wholesale
pricing-option identifier. Buyer integrations must preserve both values exactly
when calling `create_media_buy`.

The composer also reads seller-approved Material-owned positioning and
case-study candidates from each Material's current source revision. A candidate
is eligible only after an explicit `save_material` accept or correct decision
and only when its market, locale, channel, format, property, and visibility
scope matches the RFP. Advertiser-confidential or otherwise advertiser-scoped
candidates remain excluded until their eligibility can be bound to server-owned
authorization rather than an advertiser label supplied in the RFP. Its exact Material revision and evidence
locator are carried into the semantic document. Pending, rejected, withdrawn,
stale-revision, mismatched, or missing-digest candidates are not supplied to
the composer.

Those evidence rules also apply when live narrative generation is unavailable.
For a pitch or counter, the deterministic response cites eligible accepted
positioning directly in its thesis and retains an eligible accepted case study
in its proof section. Both carry the same Material ID, source revision,
candidate ID, locator, and content digest. An explicit pass remains
product-free and uses its validated seller-authored policy reason instead of
repurposing catalog or Material evidence as the seller's decision basis.
If no eligible Material positioning or proof story remains after relevance,
confidentiality, and required provenance checks, the semantic document records
a bounded warning for each missing class instead
of silently implying that seller proof was considered or available. Excluded
evidence and its contents are never named by that warning.

Deterministic proposal copy is localized from `request.constraints.locale`.
`nl-NL` produces Dutch headings, proposal language metadata, commercial
phrases, pass explanations, and next-step prompts; `en-US` and an omitted
locale produce English. Other valid BCP-47 locales currently fall back to
English, and the semantic document reports `en-US` as the language actually
emitted instead of labelling English copy as the requested language. Currency,
unit prices, allocation percentages, and rendered tables use that emitted
locale. Buyer-facing next-step prose and labels describe the concrete decision
needed; the machine-readable CTA target retains its established v3 operation
reference for agents. A representation uses the immutable semantic document's
language; requesting another supported language is rejected because rendering
does not translate the already-composed response. Append a turn with the new
request locale when a translated response is needed. Previously stored
`story-first-v3` responses remain readable: JSON preserves the exact immutable
document and its original locale, while human artifacts render that recipe's
legacy English copy with truthful `en-US` language metadata.

`request.budget` is an exact currency amount, not an arbitrary decimal. Its
`currency` must match the uppercase ISO 4217 contract published by the
`save_rfp` input schema and be supported by its runtime refinement; `amount`
must remain between the schema's published minimum and maximum. Decimal precision follows that currency's minor unit: for
example USD accepts two decimal places, BHD accepts three, and JPY accepts none.
An amount that cannot be represented in whole minor units is rejected before
the RFP is saved. An empty budget object is accepted as an omitted budget for
legacy clients; a non-empty budget must include both amount and currency. Zero
is accepted so an incomplete brief can be recorded, but
composition returns actionable `needs_clarification`; it never consumes worker
retries or emits zero-value allocation lines.

`request.flight` may contain only the bounded facts currently known. A partial
start or end is preserved rather than rejected, and legacy ISO datetime values
are canonicalized to their `YYYY-MM-DD` calendar date. Composition asks for any
flight fact the request explicitly marks as required instead of rejecting the
turn at save time.

The legacy `request.constraints.mustInclude` list remains supported for
compatibility. It accepts at most 16 non-empty, control-safe planning-text
items, each up to 160 Unicode code points after trimming.

Structural identifiers and field names use the same trimming rule: leading and
trailing Unicode whitespace (including TAB/LF/CR) is trimmed, then the value
must contain a visible character and no C0/C1 controls, bidi controls, or
line/paragraph separators. Idempotency uses the trimmed identifier, so padded
and unpadded forms identify the same save request.

## Use a Quick RFP preset

Quick identifies the RFP origin; it does not change the request purpose. Its
production cache is used only when `origin.kind: "quick"`, `purpose: "live"`, and a non-empty typed
`origin.preset` are all present. Supported cache dimensions are buyer,
advertiser, category, location, market, channels, objective, advertiser class,
and budget band. Strings and channel collections are bounded by the published
`save_rfp` schema. Existing bounded legacy preset records (including
`preset.id`) and omitted presets remain accepted; they compose through the
established lifecycle without populating or reading the production cache.
Quick draft and evaluation requests are likewise accepted and bypass caching.

```json theme={null}
{
  "action": "create",
  "clientRequestId": "quick:retail-us-video:v1",
  "origin": {
    "kind": "quick",
    "presetId": "curated-retail-us-video",
    "preset": {
      "category": "retail",
      "location": "US",
      "channels": ["video"],
      "objective": "launch",
      "advertiserClass": "brand",
      "budgetBand": "50k_250k"
    }
  },
  "purpose": "live",
  "request": {
    "brief": "Launch a retail video package.",
    "budget": { "amount": 125000, "currency": "USD" },
    "flight": { "startDate": "2026-09-01", "endDate": "2026-09-30" },
    "dimensions": { "audience": "retail decision makers" },
    "constraints": { "inventory": "brand_safe" }
  },
  "strategy": { "posture": "hold_value" }
}
```

The cache stores a validated reusable selection/story skeleton and the
truth-gated seller narrative produced by the first viable live composition. A
cache hit skips both the full skeleton build and the narrative model call; it
never stores buyer-authored brief prose for reuse.
Every response still rescans and reloads current products, price options,
product eligibility, and availability, then applies exact budget and currency
facts. Audience, flight, constraints, and overrides remain recorded request
context; the current planner checks required-input presence but does not claim
that those values filtered inventory or overlaid catalog facts. A
dependency digest binds the referenced Material revision, the complete set of
current accepted Material positioning and case-study candidates, Playbook and
acceptance-policy content, catalog and pricing versions
and content, response recipe, model inputs, response-validation contract, and
renderer contract. A changed dependency replaces the entry for that preset
rather than serving stale facts.

The terminal publication transaction takes a Seller Account customer-scoped
dependency fence, re-reads the authoritative catalog, policy, Playbook,
Product Marketing, and Material snapshot, and publishes the semantic document,
receipts, and any cache update atomically. Dependency writers take the
exclusive side of the same customer fence; unrelated Seller Accounts do not
share a platform-wide lock. If the snapshot changed during composition, the
attempt publishes neither a ready response nor a cache entry and retries from
current facts.

Cache hits do not change buyer-facing response shape, representation, or
provenance. Certified plan intent is pre-release: its decision and role fields
are not yet available to Seller Accounts. The `seller-rfp-plan-intent` flag
remains disabled until the follow-up trigger-enforcement migration ships, so
new turns retain the legacy response contract. The pre-release capability adds
no entitlement, package, or billing meter.

Read `receipt.cacheMode` on `get({"kind":"rfp_turn"})`:

* `miss_full_compose` created or replaced the skeleton;
* `hit_hydrated` reused a truth-gated narrative for the same request and
  deterministic decision basis;
* `hit_customized` reused only source-authenticated catalog or Material claims
  whose composite references and decision basis still match; and
* `bypassed` composed normally and names the reason, such as a non-Quick origin,
  any non-live purpose, a legacy or omitted preset, the 500-key Seller Account
  cache limit, or another request currently populating that Seller Account's
  cache.

For cache hits, `cachedSkeletonDigest` identifies the loaded shared skeleton
and `currentSkeletonDigest` identifies the request-specific overlay. The
buyer's requested product count is `null` when no cardinality was authored;
seller-selected plan size remains separate.

Decision claims carry evidence for the source that actually established each
claim. A seller-authored pass reason cites its `strategy.passReason` request
locator. Missing-input limitations cite the exact request path. Catalog,
format, role, and measurement limitations cite the current catalog revision;
the renderer never presents a generic policy or catalog citation for a reason
that source did not produce.

An explicit seller `pass` does not require a viable product, currency, or
`strategy.passReason`. Every explicit pass records
`explicit_strategy_posture` as immutable provenance. For an enrolled Seller
Account, a supplied reason produces the v2 pass document and is recorded as its
seller-authored policy basis; an explicit pass without a reason—or any explicit
pass composed while the exposure flag is off or unavailable—retains the
deployed v1 compatibility document. A planner-driven catalog-gap pass is a v2
decision without seller-posture provenance. Catalog or request gaps remain
separate rendered limitations.

`presetId` labels a preset as curated; normalized dimensions and the resolved
response locale determine its cache key, so cached English narrative can never
be selected for a Dutch response. Dutch platform narrative is deterministic
and does not populate `cachedNarrative`. Unnamed combinations populate lazily from real demand. Apostra
retains at most 500 normalized preset keys per Seller Account and does not
prebuild their Cartesian product.

The same turn receipt includes bounded composition evidence at
`receipt.composer`:

* `schemaVersion` is `rfp-composer-execution-v3`;
* `mode` is `model`, `cache`, or `fallback`;
* `outcome` is `composed` for a live model result, `cache_hit` for reuse, or a
  bounded fallback category. Dutch responses use `localized_deterministic` and
  do not invoke or reuse a narrative model;
* `provider` and `model` identify the live composer only when `mode` is
  `model`; both are `null` for cache and fallback.
* `attempted` says whether a model invocation was attempted for this turn;
  `attemptProvider` and `attemptModel` identify that invocation when the
  provider reported a bounded identity. A rejected or ungrounded paid result
  therefore remains `mode: "fallback"` without being mislabeled as a no-call
  fallback.
* `deadlineMs` is the model budget granted to this turn: up to 90 seconds, always
  bounded by the remaining 120-second composition attempt. A `deadline`
  outcome means the model did not answer inside that budget and the turn fell
  back to the deterministic response.
* `groundingDetail`, when present for an `ungrounded` fallback, is a bounded,
  counts-only diagnosis of the model claims, citation-grounding and entailment
  drops, and their closed reason histogram. It makes a rejected pitch
  diagnosable from the turn receipt without exposing model prose or buyer data.
* `entailmentFailure`, when present, records the bounded verifier failure kind
  and whether a required endorsed response received the one permitted retry
  after a transient `model_error`.
  A recovered proposal retains this receipt evidence; if that retry also
  failed, `retryKind` records its bounded failure kind. `retrySkipped:
  "no_time"` records the distinct case where a retryable fault had no compose
  time left for another verifier call. It never includes model prose or buyer
  data.

`receipt.modelVersion` continues to identify the content's model lineage. On a
cache hit it can name the model that produced the cached narrative without
claiming that a model ran for the current turn; use `receipt.composer.mode` to
distinguish those cases.

## Create and poll a turn

Call `save_rfp` with `action: "create"`. `clientRequestId` is the idempotency
key: retrying the exact request returns the same RFP and turn, while reusing the
key with a changed payload returns an idempotency conflict.

```json theme={null}
{
  "action": "create",
  "clientRequestId": "rfp:autumn-video:v1",
  "origin": {
    "kind": "manual",
    "buyer": "Northwind Media",
    "advertiser": "Contoso",
    "channels": ["video"]
  },
  "purpose": "draft",
  "request": {
    "brief": "Reach high-intent retail audiences with premium video.",
    "budget": { "amount": 125000, "currency": "USD" },
    "constraints": { "mustInclude": ["brand_safe"] }
  },
  "strategy": { "posture": "hold_value" }
}
```

The save returns immediately with `rfpId`, `turnId`, `responseState`, and a
safe `get` call. Poll the exact turn:

```json theme={null}
{ "kind": "rfp_turn", "id": "TURN_ID" }
```

The observable lifecycle is:

`queued → processing → ready | passed | needs_clarification | failed`

* `ready` contains an immutable `proposal-semantic-document-v1` response with
  products, prices, budget allocations, canonical formats, seller claims and
  their source citations, visual reuse decisions, and composition receipts.
* `passed` is a deliberate, evaluated decision not to propose, not an absent
  response. For an enrolled Seller Account, a pass with `strategy.passReason`
  or a planner-driven catalog-gap pass contains an immutable
  `seller-rfp-pass-v2` response. An explicit reasonless pass, or any
  unenrolled explicit pass even when it has a reason, can contain
  `seller-rfp-pass-v1`. Both remain readable. The response includes
  a product-free semantic document with what the seller heard, the grounded
  reason or posture-only compatibility basis for passing, and the condition for
  a useful new turn. Catalog and request gaps appear as separate, rendered
  decision limitations. An explicit
  `strategy.posture: "pass"` records `explicit_strategy_posture` as immutable
  decision provenance on both the response and turn; it never invents a
  catalog incompatibility or emits offer lines and terms disguised as a pass.
* `needs_clarification` also contains an immutable semantic document. It
  contains no proposed products or budget allocations and lists the questions
  and catalog facts that must be resolved. Correct the product catalog or
  request, then append a new turn.
* `failed` contains a stable reason and safe retry guidance. Do not treat it as
  a successful response or keep polling it as though it were processing.

Each composition attempt is lease- and generation-fenced. A reclaimed or late
worker cannot overwrite the artifact committed by the current attempt.

## Read why a recorded response could not answer the brief

Proposal Pass shows a **Why this response** section when the immutable turn
retains an explanation. It describes the recorded decision, rather than making
a new claim about the seller or the current catalogue.

The explanation can say that the request needed more detail or approval, that
the seller declined under a recorded policy, that the recorded selection found
no available fit, or that a run or source failed. It can also say that a
complete recorded catalogue did not declare the requested channel or format.
That last result is limited to the recorded catalogue and request. It does not
prove that the seller has no broader capability. A request can also be outside
the seller's recorded offering, which records the decision for this turn rather
than a general market claim.

Some turns cannot support a stronger answer. Historical, unknown, truncated,
or malformed receipts, and incomplete catalogue evidence, are labelled
incomplete. Proposal Pass does not turn those records into an absence claim or
an offer of a substitute. A partial explanation may still show a confirmed
request limitation beside an incomplete catalogue limitation.

When a complete, authoritative receipt identifies one exact product with only
a price configuration gap, Proposal Pass can show **Open source diagnostics**.
This action is available only to the authenticated seller for that exact turn
and recorded source. It opens the existing source diagnostics Page for that
source; it does not open another source, grant broader account access, or retry
the response. The Page checks the seller, turn, and source again before it
opens. If the host cannot open it, Proposal Pass keeps the recorded explanation
visible and lets the seller retry the same action.

## Continue the lifecycle

`save_rfp` uses one closed action union. Fields from another action are
rejected rather than ignored.

`create` and `append_turn` may include `requiredLibraryUnitIds`: one to eight
exact `{ materialId, unitId, renditionRevision }` identities from reusable,
price-free Library units currently offered to the authenticated seller. This is
a seller requirement, not a strategy hint or a citation: every listed unit is
placed verbatim in the response, including when deterministic composition is
used. A unit that is not currently eligible is rejected before the turn is
queued. The receipt records each selected Library unit and whether it was
`seller_required` or `model_selected`.

For an evaluation turn, `append_turn` also snapshots the most recent saved
`feedback.note` from its exact parent. The child uses that note only as bounded
writing guidance: it may change wording, emphasis, or the order of supported
products, but it cannot add or change products, prices, sources, or claims.
The buyer-authored `request` remains unchanged, the note is never a citable
fact, and feedback saved after the append cannot change the child. Live RFP
turns do not inherit feedback this way.

| Action                   | Required values                                                                                                    | Effect                                                                                            |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| `create`                 | `clientRequestId`, `origin`, `purpose`, `request`; optional `requiredEndorsedPairId`, `requiredLibraryUnitIds`     | Creates the RFP and first turn, then queues composition                                           |
| `append_turn`            | `rfpId`, `parentTurnId`, `clientRequestId`, `request`; optional `requiredEndorsedPairId`, `requiredLibraryUnitIds` | Adds an immutable revision turn and queues composition                                            |
| `record_feedback`        | `rfpId`, `turnId`, `clientRequestId`, `feedback` and/or typed `grade`, `ledBy`, `commentary`                       | Appends bounded feedback without rewriting the response                                           |
| `request_representation` | `rfpId`, `turnId`, `clientRequestId`, `representation`                                                             | Requests an idempotent representation of the immutable response                                   |
| `cancel_representation`  | `rfpId`, `turnId`, `representationId`, `clientRequestId`                                                           | Cancels a queued or processing render without changing the source document                        |
| `release_turn`           | `rfpId`, `turnId`, `clientRequestId`                                                                               | Marks an eligible live response for delivery                                                      |
| `record_outcome`         | `rfpId`, `turnId`, `clientRequestId`, non-empty `outcome`                                                          | Appends the observed commercial result and finality                                               |
| `attach_response`        | `rfpId`, `materialId`, `clientRequestId`                                                                           | Attaches an uploaded or historical-proposal Material response when no composed response is paired |
| `endorse`                | `rfpId`, `clientRequestId`, optional `commentary`                                                                  | Endorses the paired response and stores its non-text response shape                               |
| `unendorse`              | `rfpId`, `clientRequestId`                                                                                         | Removes the endorsement and stored response shape                                                 |

`record_outcome.result` remains a nonempty free-form string so existing
integrations can preserve their source vocabulary. Aggregate reporting uses the
documented closed taxonomy in [RFP performance](/v2/setup/v3/rfp-performance);
an unknown recorded value is retained but makes classified commercial metrics
unavailable with `unknown_outcome`.

The certified `seller_proposal_v1` profile supports
`seller_response_json`, `buyer_proposal_json`, `semantic_document_json`,
`html`, `pdf`, and `pptx`. Each file is generated from the turn's immutable
document and preserves the same products, prices, budget allocations, seller
claims, and source citations. Layout differs by format. HTML declares its
language, title, heading order, table headers, image alternative text, and
tested color contrast. PDF is paginated and embeds a deterministic subset of
the certified Noto Sans font for its supported Latin, Greek, and Cyrillic
glyphs. A PDF request containing a glyph outside that font, including CJK,
right-to-left scripts, or emoji, fails explicitly instead of replacing the
character. PPTX text remains editable; long text is continued across slides
according to the fixed text-box height. Unsupported profiles, formats, glyphs,
or layout sizes fail explicitly; they are never substituted or truncated.

Set `representation.audience` to `seller_preview` only for a preview you
review yourself. Seller previews include the seller-only **Shaped by**
provenance line when the agent followed an endorsed pair. The default,
`buyer_delivery`, is the required audience for any file you send to a buyer
and never includes that provenance. `buyer_proposal_json` is always clamped to
`buyer_delivery`.

Representation states are `queued`, `processing`, `ready`, `failed`, and
`canceled`. Temporary render failures retry automatically. A canceled or
superseded attempt cannot replace a newer file. A `ready` representation
returns its checksum, commercial digest, validation receipt, expiry, and a
short-lived download URL authorized for the current Seller Account. The URL is
limited to the exact stored file version, expires no later than the artifact's
retention expiry, and does not make the file public. An artifact at or past its
retention expiry receives no download URL. Signing also requires more than
three minutes of retention to remain, so private-storage validation cannot
consume the URL's usable lifetime.

### Rendering continues after the request returns

Creating a turn or requesting a representation records durable work before the
API responds. The composition and rendering workers continue that queued work
after the request that started it has completed. Poll the exact turn or
representation for its current state rather than treating a completed request
as a completed file.

This does not make rendering unbounded or irreversible. You can explicitly
cancel a `queued` or `processing` representation, and terminal `failed` or
`canceled` states remain visible for diagnosis. A later worker attempt cannot
replace a newer attempt for the same representation.

Newly rendered files return a `proposal-representation-validation-v2` receipt.
It binds `rfpId`, `turnId`, `representationId`, and the originating
`clientRequestId` alongside the document, commercial, artifact, format, and
render-option digests. Use those correlation fields to match a file to the
exact `request_representation` call that created it. Historical
`proposal-representation-validation-v1` receipts remain readable as immutable
legacy evidence, but they do not contain the four request-correlation fields
and must not be treated as a v2 receipt with stripped fields. Both
`request_representation` and `record_feedback` echo `clientRequestId` in their
fresh and idempotent-replay responses. When an equivalent representation is
reused, `request_representation` also returns
`originatingClientRequestId`; it identifies the original request whose
immutable validation receipt is returned, while `clientRequestId` identifies
the current call.

The receipt's existing `options` record includes `audience`. This remains v2:
render options are an extensible record, and a historical receipt without an
audience is interpreted fail-closed as `buyer_delivery`.

A visual is included only when its reuse rights, confidentiality, advertiser
restrictions, expiry, file type, checksum, and allowed `proposal` use all
permit it. The response lists every omitted or unavailable visual and the
fallback used.

Requests that exceed a certified limit fail explicitly; they are never
returned as partial files.

| Limit             | Maximum |
| ----------------- | ------: |
| Semantic document |  128 KB |
| Rendered file     |   10 MB |
| Sections          |      16 |
| Story blocks      |     256 |
| Visual references |      32 |
| One input visual  |    5 MB |
| All input visuals |   20 MB |

## Proposal Pass, the portable turn workbench

Proposal Pass is the portable MCP App a seller opens to work one RFP turn —
in Murph, Claude, ChatGPT, or another conforming MCP Apps host. It is a thin
client of the same `save_rfp`/`get` contract documented above: it introduces
no calculation, no hidden lifecycle step, and no capability the API itself
does not already offer.

Open it from a v3 Seller Account with the exact immutable pair returned by
`save_rfp` or `get`:

```json theme={null}
{
  "rfpId": "rfp_01...",
  "turnId": "turn_01..."
}
```

Call `open_proposal_pass` with that object. The launch result contains only
those ids; after the fixed Proposal Pass resource opens, its app-only tools
load the private turn. A missing turn, a turn belonging to another RFP or
Seller Account, or an unauthorized account returns `NOT_FOUND` or
`WRONG_ACCOUNT` without opening the Page. The legacy v2 Storefront form,
`open_proposal_pass({"runId": ...})`, is unchanged and is not the v3 contract.

Draft and evaluation turns can be opened deliberately by exact id for review.
That does not promote them into the live Demand Inbox or include them in live
RFP performance metrics; those populations remain live-only.

From an open turn, Proposal Pass can:

* record `record_feedback` on the turn;
* `append_turn` an evaluation rerun that snapshots the latest saved note as
  non-citable writing guidance, opening a new immutable turn without changing
  the buyer brief — the parent turn is never edited in place;
* `request_representation` for `seller_response_json`, `html`, `pdf`, or
  `pptx` as either a seller preview or buyer delivery, and watch it move
  through `queued → processing → ready | failed`; and
* `cancel_representation` while it is still `queued`.

For an evaluation, **Save coaching and run test again** performs the first two
actions in sequence, opens the child turn, and hydrates its exact parent for the
before-and-after comparison. If feedback saves but the rerun cannot start, the
page keeps the correction and offers a safe retry with the same idempotency
keys, including after the widget reloads, so it does not create duplicate
feedback or child turns.

`release_turn` and `record_outcome` are deliberately not available from
Proposal Pass. Both are consequential, human-facing commitments the widget
does not yet have a reviewed confirmation flow for; use `save_rfp` directly
for those two actions.

Feedback and representations render in the same bounded pages the exact-turn
read returns (see [Search and audit](#search-and-audit) below): the widget
shows the default page and a "Show more" control follows `feedback.next`/
`representations.next` for the rest, so a turn with more than one page of
either never hides feedback or representations past the default page.

Preview and buyer-delivery download read the representation's short-lived signed URL fresh
at the moment of the request, hand it directly to the host's own
link-opening capability to open in a new browser tab, and hold it nowhere
else: the URL is never written into the widget's saved state, never
rendered, never logged, and never visible to the model driving the
conversation the seller is in. Every other format downloads the same way.
An expired or not-yet-ready representation offers no open action. Because
`request_representation` is idempotent per format-and-audience tuple for a
given turn, a representation that has already been requested cannot be re-requested to force a
fresh attempt — a failed, canceled, or expired representation stays in
that terminal state.

## Search and audit

Use `search({"kind":"rfp"})` for opportunities and
`search({"kind":"rfp_turn"})` for immutable turns. RFP filters include state,
origin, purpose, buyer, advertiser, category, market, channel, posture,
Playbook version, cache mode, and outcome. Turn filters add RFP ID, response and
evaluation state, recipe/model/judge version, and feedback status.

Use `get({"kind":"rfp","id":"RFP_ID"})` for the opportunity summary,
latest and released turns, bounded turn history, outcome summary, and links.
Use `get({"kind":"rfp_turn","id":"TURN_ID"})` for the exact request,
response, receipts, diagnostics, feedback, outcome, and representation status.

Search results are summaries; they do not repeat the complete request or
response. Feedback bodies and representation statuses remain on the authorized
exact-turn read in bounded pages. Follow `feedback.next` and
`representations.next` until they are `null`. Feedback continuations carry
`feedbackOffset`; representation pages carry an opaque `representationCursor`.
Pass the supplied argument back to `get` rather than synthesizing one. The
representation cursor pins a snapshot, so rows appended while paging do not
produce skips or duplicates.
