openapi: 3.1.0
info:
  title: YouSaid Product API
  version: 1.0.0
  description: |
    Versioned server-to-server API for signals, threads, features, and the in-app inbox.
    Authenticate with a product-scoped service API key (`Authorization: Bearer yswld_...`) created by a
    workspace owner. Each endpoint requires a scope. Every response carries `x-request-id` and
    `x-ratelimit-limit`, `x-ratelimit-remaining`, `x-ratelimit-reset` (unix seconds) for the key's
    120 requests/minute window. Errors use a typed envelope. The Rooms API is described separately in
    /openapi-rooms-v1.5.yaml.

    Outbound webhooks: deliveries are POSTed as JSON `{ id, type, createdAt, organizationId, productId, data }`
    with headers `yousaid-signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>`,
    `yousaid-event-id`, `yousaid-event-type`, `yousaid-delivery-id`, and `yousaid-delivery-attempt`.
    Reject timestamps older than 5 minutes. Failed deliveries retry with exponential backoff (30s doubling,
    capped at 1h, 8 attempts) and can be replayed from the workspace. Event types include signal.created,
    thread.replied, release.published, feature.voted, and room.* lifecycle events.

    Proof (v1.6): `/api/v1/proof/*` uses proof scopes (`yousaid:proof:read`, `yousaid:proof:read_private`,
    `yousaid:proof:write`, `yousaid:proof:publish`, `yousaid:proof:export`, `yousaid:proof:submissions:write`). A service key acts
    as a non-human integration: approving, rejecting, managing consent, revoking directly and exporting raw originals are
    human-only and answer HUMAN_APPROVAL_REQUIRED or FORBIDDEN. Every mutation accepts an `Idempotency-Key` header
    (8 to 200 characters, kept 24 hours): a retry with the same key and body replays the stored response with
    `idempotent-replayed: true`; the same key with a different body returns IDEMPOTENCY_KEY_REUSED. Proof webhooks:
    proof.opportunity.created, proof.response.submitted, proof.consent.granted, proof.consent.revoked,
    proof.item.approved, proof.asset.ready, proof.placement.published, proof.health.warning,
    proof.brand_mention.created and proof.request.opted_out. Their `data` is the minimal ProofWebhookData schema with a
    `fetchUrl`; the envelope id is `evt_<ledger event id>` and is stable across sweeps and replays.
servers:
  - url: https://yousaid.dev
security:
  - serviceApiKey: []
paths:
  /api/v1/signals:
    get:
      operationId: listSignals
      summary: List signals for the key's product, newest first
      x-required-scope: yousaid:signals:read
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/Cursor" }
        - { name: category, in: query, schema: { type: string, enum: [support, bug, feature, ux, review] } }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - { name: endUserId, in: query, schema: { type: string } }
      responses:
        "200": { description: Signal page, headers: { $ref: "#/components/headers/RateLimit" }, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Signal" } }, nextCursor: { type: [string, "null"] } } } } } }
        default: { $ref: "#/components/responses/Error" }
    post:
      operationId: createSignal
      summary: Capture a signal on behalf of an end user (idempotent by idempotencyKey)
      x-required-scope: yousaid:signals:write
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CreateSignal" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/SignalReceipt" } } } } } }
        "200": { description: Replayed with the same idempotencyKey }
        default: { $ref: "#/components/responses/Error" }
  /api/v1/signals/{id}:
    get:
      operationId: getSignal
      x-required-scope: yousaid:signals:read
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200": { description: Signal with linked features, content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/Signal" } } } } } }
        default: { $ref: "#/components/responses/Error" }
  /api/v1/threads:
    get:
      operationId: listThreads
      x-required-scope: yousaid:threads:read
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/Cursor" }
        - { name: state, in: query, schema: { type: string, enum: [open, pending, resolved, closed] } }
        - { name: owner, in: query, description: "Teammate id or `unassigned`", schema: { type: string } }
        - { name: endUserId, in: query, schema: { type: string } }
      responses:
        "200": { description: Thread page, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Thread" } }, nextCursor: { type: [string, "null"] } } } } } }
        default: { $ref: "#/components/responses/Error" }
  /api/v1/threads/{id}:
    parameters: [{ $ref: "#/components/parameters/Id" }]
    get:
      operationId: getThread
      summary: Thread with customer-visible messages (internal notes are never returned)
      x-required-scope: yousaid:threads:read
      responses:
        "200": { description: Thread, content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/Thread" } } } } } }
        default: { $ref: "#/components/responses/Error" }
    patch:
      operationId: updateThread
      summary: "Change state or assignment (ownerId null unassigns)"
      x-required-scope: yousaid:threads:write
      requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: false, properties: { state: { type: string, enum: [open, pending, resolved, closed] }, ownerId: { type: [string, "null"] } } } } } }
      responses:
        "200": { description: Updated }
        default: { $ref: "#/components/responses/Error" }
  /api/v1/features:
    get:
      operationId: listFeatures
      summary: Features ordered by evidence-weighted score
      x-required-scope: yousaid:features:read
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { name: state, in: query, schema: { type: string, enum: [discovery, planned, building, released, declined] } }
        - { name: publicBoard, in: query, schema: { type: string, enum: ["true", "false"] } }
      responses:
        "200": { description: Features, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/Feature" } } } } } } }
        default: { $ref: "#/components/responses/Error" }
  /api/v1/features/{id}:
    get:
      operationId: getFeature
      x-required-scope: yousaid:features:read
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200": { description: Feature with linked evidence }
        default: { $ref: "#/components/responses/Error" }
  /api/v1/features/{id}/votes:
    parameters: [{ $ref: "#/components/parameters/Id" }]
    post:
      operationId: voteFeature
      summary: "Record an end user vote (idempotent); voting also follows unless follow=false"
      x-required-scope: yousaid:features:write
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/Vote" } } } }
      responses:
        "200": { description: Vote recorded with the new score }
        default: { $ref: "#/components/responses/Error" }
    delete:
      operationId: unvoteFeature
      x-required-scope: yousaid:features:write
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/Vote" } } } }
      responses:
        "200": { description: Vote withdrawn }
        default: { $ref: "#/components/responses/Error" }
  /api/v1/inbox:
    get:
      operationId: listInbox
      summary: An end user's in-app inbox
      description: Call with a signed end-user identity token, or with a service API key (`yousaid:inbox:read`) plus `endUserId`.
      security: [{ serviceApiKey: [] }, { bearerIdentity: [] }]
      parameters:
        - { name: endUserId, in: query, description: Required with an API key, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100 } }
        - { name: before, in: query, description: nextCursor from the previous page, schema: { type: string } }
      responses:
        "200": { description: Inbox page, content: { application/json: { schema: { $ref: "#/components/schemas/InboxPage" } } } }
  /api/v1/inbox/read:
    post:
      operationId: markInboxRead
      summary: "Mark items (or everything) read. End-user identity only."
      security: [{ bearerIdentity: [] }]
      requestBody: { content: { application/json: { schema: { type: object, properties: { ids: { type: array, maxItems: 200, items: { type: string } } } } } } }
      responses:
        "200": { description: Updated unread count, content: { application/json: { schema: { type: object, properties: { unread: { type: integer }, marked: { type: integer } } } } } }
  /api/v1/inbox/stream:
    get:
      operationId: streamInbox
      summary: "Server-Sent Events: ready, item, unread"
      security: [{ bearerIdentity: [] }]
      responses:
        "200": { description: text/event-stream, content: { text/event-stream: { schema: { type: string } } } }
  /api/v1/proof/items:
    get:
      operationId: listProofItems
      tags: [Proof]
      summary: List proof items in the key's product, newest first
      x-required-scope: yousaid:proof:read
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/Cursor" }
        - { name: status, in: query, description: Comma-separated statuses, schema: { type: string, example: "approved,published" } }
        - { name: type, in: query, schema: { type: string, enum: [text, audio, video, image, review, case_study, release_story] } }
        - { $ref: "#/components/parameters/ProofItemExpand" }
      responses:
        "200": { description: "Page of items. With expand, each item carries `expanded` and the page size is capped at 50.", headers: { $ref: "#/components/headers/RateLimit" }, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { allOf: [{ $ref: "#/components/schemas/ProofItem" }, { type: object, properties: { expanded: { $ref: "#/components/schemas/ProofItemExpansions" } } }] } }, nextCursor: { type: [string, "null"] } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/items/{id}:
    get:
      operationId: getProofItem
      tags: [Proof]
      summary: Item, approved public version and per-channel eligibility (decided by the eligibility evaluator)
      x-required-scope: yousaid:proof:read
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/ProofItemExpand" }]
      responses:
        "200": { description: Item, content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { item: { $ref: "#/components/schemas/ProofItem" }, publicVersion: { oneOf: [{ $ref: "#/components/schemas/ProofVersion" }, { type: "null" }] }, eligibleChannels: { type: array, items: { type: string } }, eligibility: { type: array, items: { $ref: "#/components/schemas/ChannelEligibility" } }, expanded: { $ref: "#/components/schemas/ProofItemExpansions" } } } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/items/{id}/versions:
    get:
      operationId: listProofVersions
      tags: [Proof]
      summary: Versions oldest first. Non-approved content (including the original) requires yousaid:proof:read_private.
      x-required-scope: yousaid:proof:read
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200": { description: Versions, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ProofVersion" } } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
    post:
      operationId: createProofVersion
      tags: [Proof]
      summary: Create a draft derivative version with a semantic diff. The original is immutable.
      x-required-scope: yousaid:proof:write
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: false, required: [content, changeReason], properties: { content: { $ref: "#/components/schemas/ProofContent" }, changeReason: { type: string, minLength: 3, maxLength: 1000 }, parentVersionId: { type: string }, modelId: { type: string }, locale: { type: string } } } } } }
      responses:
        "201": { description: Draft version, content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/ProofVersion" } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/items/{id}/request-approval:
    post:
      operationId: requestProofApproval
      tags: [Proof]
      summary: Queue a version for human review. Requesting is not approving.
      x-required-scope: yousaid:proof:write
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [versionId], properties: { versionId: { type: string } } } } } }
      responses:
        "200": { description: Item and version now awaiting internal review }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/items/{id}/approve:
    post:
      operationId: approveProofItem
      tags: [Proof]
      summary: Human-only. Service keys always receive 403 HUMAN_APPROVAL_REQUIRED; approve in the workspace.
      x-required-scope: yousaid:proof:write
      x-human-only: true
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "403": { description: HUMAN_APPROVAL_REQUIRED }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/items/{id}/reject:
    post:
      operationId: rejectProofItem
      tags: [Proof]
      summary: Human-only. Service keys always receive 403 HUMAN_APPROVAL_REQUIRED.
      x-required-scope: yousaid:proof:write
      x-human-only: true
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "403": { description: HUMAN_APPROVAL_REQUIRED }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/items/{id}/consent:
    get:
      operationId: getProofConsent
      tags: [Proof]
      summary: Consent grants for the item (scope, state, receipt hash). Read-only; evidence such as IP addresses is never returned.
      x-required-scope: yousaid:proof:read
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200": { description: Consent status, content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { proofItemId: { type: string }, status: { type: string }, grants: { type: array, items: { $ref: "#/components/schemas/ConsentSummary" } } } } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/items/{id}/revoke:
    post:
      operationId: requestProofRevocation
      tags: [Proof]
      summary: File a revocation request. Service keys cannot revoke directly; a human with consent authority completes it (202).
      x-required-scope: yousaid:proof:write
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { content: { application/json: { schema: { type: object, properties: { requestText: { type: string, maxLength: 2000 } } } } } }
      responses:
        "202": { description: Revocation request filed, content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/RevocationResult" } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/items/{id}/summarize:
    post:
      operationId: summarizeProofItem
      tags: [Proof AI]
      summary: "@proof AI summary via SansaAI. Numbers not in the source are rejected. 503 AI_UNAVAILABLE / 429 AI_BUDGET_EXCEEDED when AI cannot run; nothing is fabricated."
      x-required-scope: yousaid:proof:write
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200": { description: "AI summary (labeled). Caveats always state any incentive, the verification state and the consent limits; `validation` says what was removed or added. `needsHumanReview` is true when the model has not been evaluated yet.", content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/ProofItemSummary" } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/consents/{id}/revoke:
    post:
      operationId: requestConsentRevocation
      tags: [Proof]
      summary: File a revocation request for the grant's item (202 for service keys).
      x-required-scope: yousaid:proof:write
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "202": { description: Revocation request filed, content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/RevocationResult" } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/placements:
    get:
      operationId: listProofPlacements
      tags: [Proof]
      x-required-scope: yousaid:proof:read
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/Cursor" }
        - { name: state, in: query, schema: { type: string, enum: [draft, active, paused, archived] } }
      responses:
        "200": { description: Placements, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ProofPlacement" } }, nextCursor: { type: [string, "null"] } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
    post:
      operationId: createProofPlacement
      tags: [Proof]
      summary: Create a placement in the key's product (the product always comes from the key).
      x-required-scope: yousaid:proof:publish
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [name, placementType, channel], properties: { name: { type: string }, placementType: { type: string, enum: [wall, widget, slot, case_study, campaign, sales_asset] }, channel: { type: string, enum: [website, in_app, email, sales, social_organic, paid_ads, case_study, press] }, hostContext: { type: string, enum: [yousaid_native, static_export, third_party] }, locale: { type: string }, geography: { type: string }, eligibilityPolicy: { type: object }, renderConfig: { type: object } } } } } }
      responses:
        "201": { description: Placement, content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/ProofPlacement" } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/placements/{id}:
    get:
      operationId: getProofPlacement
      tags: [Proof]
      x-required-scope: yousaid:proof:read
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/ProofPlacementExpand" }]
      responses:
        "200": { description: Placement, content: { application/json: { schema: { type: object, properties: { data: { allOf: [{ $ref: "#/components/schemas/ProofPlacement" }, { type: object, properties: { expanded: { type: object, properties: { items: { type: object, description: "First 50 rows of GET /api/v1/proof/placements/{id}/items (same shape).", properties: { data: { type: array, items: { $ref: "#/components/schemas/ProofPlacementItem" } }, hasMore: { type: boolean } } } } } } }] } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
    patch:
      operationId: updateProofPlacement
      tags: [Proof]
      summary: Rename, pause, reactivate or change render config. Bumps the render cache version.
      x-required-scope: yousaid:proof:publish
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: false, properties: { name: { type: string }, state: { type: string, enum: [active, paused, archived] }, renderConfig: { type: object }, eligibilityPolicy: { type: object } } } } } }
      responses:
        "200": { description: Updated placement }
        default: { $ref: "#/components/responses/ProofError" }
    delete:
      operationId: archiveProofPlacement
      tags: [Proof]
      summary: Archive the placement (history is kept).
      x-required-scope: yousaid:proof:publish
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "200": { description: Archived placement }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/placements/{id}/items:
    get:
      operationId: listProofPlacementItems
      tags: [Proof]
      summary: Placed items with the live eligibility decision for each
      x-required-scope: yousaid:proof:read
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200": { description: Placement items }
        default: { $ref: "#/components/responses/ProofError" }
    post:
      operationId: addProofPlacementItem
      tags: [Proof]
      summary: Place an approved version. Whether it renders is decided by eligibility at every manifest build.
      x-required-scope: yousaid:proof:publish
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: false, required: [proofItemId], properties: { proofItemId: { type: string }, versionId: { type: string }, position: { type: integer } } } } } }
      responses:
        "201": { description: Placed, with the current eligibility decision }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/placements/{id}/items/{itemId}:
    delete:
      operationId: removeProofPlacementItem
      tags: [Proof]
      x-required-scope: yousaid:proof:publish
      parameters: [{ $ref: "#/components/parameters/Id" }, { name: itemId, in: path, required: true, schema: { type: string } }, { $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "200": { description: Removed }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/placements/{id}/publish:
    post:
      operationId: publishProofPlacement
      tags: [Proof]
      summary: Activate the placement. Items without matching consent are excluded at render time and counted. Emits proof.placement.published. With dryRun=true nothing is written and the same counts are returned.
      x-required-scope: yousaid:proof:publish
      parameters: [{ $ref: "#/components/parameters/Id" }, { $ref: "#/components/parameters/IdempotencyKey" }, { name: dryRun, in: query, schema: { type: boolean, default: false }, description: "Run every permission and eligibility check without publishing." }]
      responses:
        "200": { description: Published, content: { application/json: { schema: { type: object, properties: { data: { type: object, properties: { placement: { $ref: "#/components/schemas/ProofPlacement" }, eligibleCount: { type: integer }, excludedCount: { type: integer } } } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/events:
    get:
      operationId: listProofEvents
      tags: [Proof]
      summary: Trust Ledger events for the key's product, oldest first
      x-required-scope: yousaid:proof:read
      parameters:
        - { $ref: "#/components/parameters/Limit" }
        - { $ref: "#/components/parameters/Cursor" }
        - { name: proofItemId, in: query, schema: { type: string } }
        - { name: eventType, in: query, schema: { type: string, example: proof.item.approved } }
        - { name: eventId, in: query, schema: { type: string } }
      responses:
        "200": { description: Events, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ProofEvent" } }, nextCursor: { type: [string, "null"] } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/exports:
    post:
      operationId: createProofExport
      tags: [Proof]
      summary: Approved-only export (items, approved versions, consent summaries, placements) with a sha256 digest. Raw originals are exported by a workspace owner.
      x-required-scope: yousaid:proof:export
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "201": { description: Export bundle }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/requests:
    post:
      operationId: createProofRequest
      tags: [Proof]
      summary: Create a DRAFT proof request. Sending is customer-facing and needs human approval.
      x-required-scope: yousaid:proof:write
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: false, required: [channel], properties: { channel: { type: string, enum: [link, email, in_app, room, support, survey, api] }, recipientId: { type: string }, opportunityId: { type: string }, formId: { type: string }, message: { type: string, maxLength: 4000 } } } } } }
      responses:
        "201": { description: Draft request }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/requests/draft:
    post:
      operationId: draftProofRequestMessage
      tags: [Proof AI]
      summary: "@proof drafts an ask via SansaAI. Never sends. Drafts with review gating, sentiment-conditioned rewards or invented numbers are discarded (422)."
      x-required-scope: yousaid:proof:write
      requestBody: { required: true, content: { application/json: { schema: { type: object, properties: { proofItemId: { type: string }, recipientName: { type: string }, context: { type: string }, channel: { type: string, enum: [email, in_app, link, room] }, tone: { type: string, enum: [warm, brief, formal] }, incentiveDisclosure: { type: string } } } } } }
      responses:
        "200": { description: "Labeled AI draft with policy checks. `needsHumanReview` is true when the model has not been evaluated yet.", content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/ProofAskDraft" } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/submissions:
    post:
      operationId: createProofSubmission
      tags: [Proof collection]
      summary: Headless capture of a customer submission. Always stored; an exhausted allowance flags it and pauses only costly processing.
      description: Provided by the collection service. Listed here so the Proof API is documented in one place.
      x-required-scope: yousaid:proof:submissions:write
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      responses:
        "201": { description: Submission stored }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/assets/generate:
    post:
      operationId: generateProofAsset
      tags: [Proof Studio]
      summary: Deterministic quote card (SVG) from an eligible approved version. 422 NOT_ELIGIBLE with the evaluator's reasons otherwise.
      x-required-scope: yousaid:proof:write
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: false, required: [proofItemId], properties: { proofItemId: { type: string }, versionId: { type: string }, format: { type: string, enum: [square, portrait, landscape, story] }, channel: { type: string, enum: [social_organic, website, email, sales, in_app, press, case_study, paid_ads] }, excerpt: { type: string }, theme: { type: object } } } } } }
      responses:
        "201": { description: Draft asset, content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/ProofAsset" } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/assets/{id}:
    get:
      operationId: getProofAsset
      tags: [Proof Studio]
      summary: Asset with lineage (source items, versions, grant, template) and approval state
      x-required-scope: yousaid:proof:read
      parameters: [{ $ref: "#/components/parameters/Id" }]
      responses:
        "200": { description: Asset, content: { application/json: { schema: { type: object, properties: { data: { $ref: "#/components/schemas/ProofAsset" } } } } } }
        default: { $ref: "#/components/responses/ProofError" }
  /api/v1/proof/case-studies:
    post:
      operationId: compileProofCaseStudy
      tags: [Proof AI]
      summary: Compile a source-cited case study draft via SansaAI. Uncited statements, unknown citations, invented numbers and altered quotes are rejected; the draft needs human and customer approval.
      x-required-scope: yousaid:proof:write
      parameters: [{ $ref: "#/components/parameters/IdempotencyKey" }]
      requestBody: { required: true, content: { application/json: { schema: { type: object, additionalProperties: false, required: [proofItemIds], properties: { proofItemIds: { type: array, minItems: 1, maxItems: 10, items: { type: string } }, template: { type: string }, length: { type: string, enum: [short, standard, deep] }, goal: { type: string } } } } } }
      responses:
        "201": { description: Draft case-study asset plus the rejected statements }
        default: { $ref: "#/components/responses/ProofError" }
components:
  securitySchemes:
    serviceApiKey: { type: http, scheme: bearer, bearerFormat: yswld_ }
    bearerIdentity: { type: http, scheme: bearer, bearerFormat: JWT, description: "Signed end-user identity (HS256, kid = product key). State-changing requests consume the token nonce, so mint one per write." }
  parameters:
    Id: { name: id, in: path, required: true, schema: { type: string } }
    Limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
    Cursor: { name: cursor, in: query, schema: { type: string } }
    IdempotencyKey: { name: Idempotency-Key, in: header, required: false, schema: { type: string, minLength: 8, maxLength: 200 }, description: Replays the stored response for 24 hours when the same key and body are retried. }
    ProofItemExpand:
      name: expand
      in: query
      required: false
      style: form
      explode: false
      description: "Inline related data that otherwise comes from a sub-resource, with the same shape, scopes and permissions. `versions` = GET /items/{id}/versions (latest 10, oldest first; unapproved versions need yousaid:proof:read_private), `consent` = GET /items/{id}/consent (latest 20 grants). At most 2 relations; an unknown, repeated or extra relation answers 400 VALIDATION_FAILED. On the list, each relation is one query for the whole page and the page size is capped at 50."
      schema: { type: array, maxItems: 2, uniqueItems: true, items: { type: string, enum: [versions, consent] } }
    ProofPlacementExpand:
      name: expand
      in: query
      required: false
      style: form
      explode: false
      description: "`items` = GET /placements/{id}/items (first 50 rows, same shape). Any other value answers 400 VALIDATION_FAILED."
      schema: { type: array, maxItems: 2, uniqueItems: true, items: { type: string, enum: [items] } }
  headers:
    RateLimit:
      x-ratelimit-limit: { schema: { type: integer } }
      x-ratelimit-remaining: { schema: { type: integer } }
      x-ratelimit-reset: { schema: { type: integer, description: Unix seconds when the window resets } }
  responses:
    Error:
      description: Typed error
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                required: [code, message, requestId]
                properties:
                  code: { type: string, enum: [INVALID_API_KEY, KEY_AUDIENCE_MISMATCH, ENVIRONMENT_SCOPE_MISMATCH, PRODUCT_SCOPE_REQUIRED, PRODUCT_SCOPE_UNAVAILABLE, INSUFFICIENT_SCOPE, RATE_LIMITED, VALIDATION_FAILED, NOT_FOUND, CONFLICT, QUOTA_EXCEEDED, SERVICE_UNAVAILABLE] }
                  message: { type: string }
                  requestId: { type: string }
                  details: {}
    ProofError:
      description: Typed error (Proof)
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                required: [code, message, requestId]
                properties:
                  code: { type: string, enum: [INVALID_API_KEY, KEY_AUDIENCE_MISMATCH, ENVIRONMENT_SCOPE_MISMATCH, PRODUCT_SCOPE_REQUIRED, PRODUCT_SCOPE_UNAVAILABLE, INSUFFICIENT_SCOPE, RATE_LIMITED, VALIDATION_FAILED, FORBIDDEN, HUMAN_APPROVAL_REQUIRED, NOT_FOUND, CONFLICT, REVOKED_FINAL, CONSENT_INVALID, NOT_ELIGIBLE, SOURCE_POLICY_BLOCKED, CITATION_VALIDATION_FAILED, RECOVERY_WINDOW_CLOSED, IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_IN_PROGRESS, QUOTA_EXCEEDED, AI_BUDGET_EXCEEDED, AI_UNAVAILABLE, SERVICE_UNAVAILABLE] }
                  message: { type: string }
                  requestId: { type: string }
                  details: {}
  schemas:
    Signal:
      type: object
      properties: { id: { type: string }, threadId: { type: string }, endUserId: { type: string }, category: { type: string }, title: { type: string }, body: { type: string }, urgency: { type: string }, sentiment: { type: [string, "null"] }, summary: { type: [string, "null"] }, analysisState: { type: string }, createdAt: { type: string, format: date-time }, updatedAt: { type: string, format: date-time } }
    CreateSignal:
      type: object
      additionalProperties: false
      required: [endUserId, title, body, idempotencyKey]
      properties: { endUserId: { type: string }, email: { type: string, format: email }, name: { type: string }, kind: { type: string, enum: [support, bug, feature, ux, review] }, title: { type: string, minLength: 3, maxLength: 180 }, body: { type: string, minLength: 3, maxLength: 10000 }, idempotencyKey: { type: string, minLength: 8, maxLength: 180 }, context: { type: object } }
    SignalReceipt:
      type: object
      properties: { id: { type: string }, threadId: { type: string }, state: { type: string }, replayed: { type: boolean } }
    Thread:
      type: object
      properties: { id: { type: string }, endUserId: { type: string }, subject: { type: string }, state: { type: string }, ownerId: { type: [string, "null"] }, nextUpdateAt: { type: [string, "null"] }, createdAt: { type: string }, updatedAt: { type: string } }
    Feature:
      type: object
      properties: { id: { type: string }, title: { type: string }, problem: { type: string }, state: { type: string }, score: { type: number }, scoreBreakdown: { type: object }, voteCount: { type: integer }, followerCount: { type: integer }, signalCount: { type: integer }, publicBoard: { type: boolean } }
    Vote:
      type: object
      additionalProperties: false
      required: [endUserId]
      properties: { endUserId: { type: string }, follow: { type: boolean } }
    InboxPage:
      type: object
      properties:
        unread: { type: integer }
        nextCursor: { type: [string, "null"] }
        items: { type: array, items: { type: object, properties: { id: { type: string }, threadId: { type: string }, subject: { type: string }, preview: { type: string }, createdAt: { type: string }, readAt: { type: [string, "null"] } } } }
    ProofItem:
      type: object
      properties: { id: { type: string }, productId: { type: [string, "null"] }, type: { type: string }, status: { type: string }, statusReason: { type: [string, "null"] }, customerPersonId: { type: [string, "null"] }, originalVersionId: { type: [string, "null"] }, approvedPublicVersionId: { type: [string, "null"] }, overLimit: { type: boolean }, overLimitReasons: { type: array, items: { type: string } }, createdAt: { type: string }, updatedAt: { type: string } }
    ProofContent:
      type: object
      additionalProperties: false
      properties: { text: { type: string }, title: { type: string }, rating: { type: number }, mediaAssetIds: { type: array, items: { type: string } }, attribution: { type: object, additionalProperties: { type: string } }, answers: { type: object } }
    ProofVersion:
      type: object
      properties: { id: { type: string }, proofItemId: { type: string }, versionNumber: { type: integer }, isOriginal: { type: boolean }, content: { $ref: "#/components/schemas/ProofContent" }, contentHash: { type: string }, createdByType: { type: string }, modelId: { type: [string, "null"] }, changeReason: { type: [string, "null"] }, semanticDiff: { type: object }, approvalState: { type: string }, createdAt: { type: string } }
    ChannelEligibility:
      type: object
      properties: { channel: { type: string }, eligible: { type: boolean }, reasons: { type: array, items: { type: string } }, grantId: { type: [string, "null"] }, identityFields: { type: array, items: { type: string } }, disclosure: { type: [string, "null"] }, labels: { type: array, items: { type: string } } }
    ConsentSummary:
      type: object
      properties: { id: { type: string }, approvedVersionId: { type: string }, state: { type: string }, displayMode: { type: string }, identityFields: { type: array, items: { type: string } }, channels: { type: array, items: { type: string } }, products: { type: object }, media: { type: array, items: { type: string } }, paidUseAllowed: { type: boolean }, translationAllowed: { type: boolean }, geography: { type: array, items: { type: string } }, validUntil: { type: [string, "null"] }, customerApprovedWording: { type: boolean }, receiptHash: { type: [string, "null"] }, revokedAt: { type: [string, "null"] } }
    ProofItemExpansions:
      type: object
      description: Present only when expand is sent. Each relation carries the sub-resource's `data` and `hasMore` when the per-item cap cut it.
      properties:
        versions: { type: object, properties: { data: { type: array, items: { $ref: "#/components/schemas/ProofVersion" } }, hasMore: { type: boolean } } }
        consent: { type: object, properties: { data: { type: object, properties: { proofItemId: { type: string }, status: { type: string }, grants: { type: array, items: { $ref: "#/components/schemas/ConsentSummary" } } } }, hasMore: { type: boolean } } }
    ProofPlacementItem:
      type: object
      properties: { id: { type: string }, proofItemId: { type: string }, versionId: { type: string }, position: { type: integer }, state: { type: string }, removedReason: { type: [string, "null"] }, eligible: { type: boolean }, reasons: { type: array, items: { type: string } }, disclosure: { type: [string, "null"] } }
    RevocationResult:
      type: object
      properties: { requestOnly: { type: boolean }, revocation: { type: object, properties: { id: { type: string }, resolutionState: { type: string }, immediatePolicyAction: { type: string }, slaDeadlineAt: { type: string } } } }
    ProofPlacement:
      type: object
      properties: { id: { type: string }, productId: { type: [string, "null"] }, name: { type: string }, placementType: { type: string }, channel: { type: string }, hostContext: { type: string }, state: { type: string }, renderCacheVersion: { type: integer }, renderConfig: { type: object }, eligibilityPolicy: { type: object } }
    ProofEvent:
      type: object
      properties: { id: { type: string }, eventType: { type: string }, productId: { type: [string, "null"] }, placementId: { type: [string, "null"] }, proofItemId: { type: [string, "null"] }, assetId: { type: [string, "null"] }, actor: { type: object, properties: { type: { type: string }, id: { type: string } } }, timestamp: { type: string }, properties: { type: object } }
    ProofAsset:
      type: object
      properties: { id: { type: string }, assetType: { type: string, enum: [quote_card, case_study] }, sourceProofIds: { type: array, items: { type: string } }, sourceVersionIds: { type: array, items: { type: string } }, templateId: { type: [string, "null"] }, brandKitVersion: { type: [string, "null"] }, approvalState: { type: string }, channelEligibility: { type: array, items: { type: string } }, files: { type: array, items: { type: string } }, content: { type: object } }
    ProofWebhookData:
      type: object
      description: "`data` of a proof.* webhook. Minimal by design: fetch details from fetchUrl with a proof-scoped key. Deduplicate on the envelope `id` (evt_<ledger id>)."
      properties: { eventVersion: { type: integer, example: 1 }, proofEventId: { type: string }, ledgerEventType: { type: string }, occurredAt: { type: string }, emittedAt: { type: string }, actor: { type: object, properties: { type: { type: string }, id: { type: string } } }, productId: { type: [string, "null"] }, proofItemId: { type: [string, "null"] }, placementId: { type: [string, "null"] }, assetId: { type: [string, "null"] }, fetchUrl: { type: string }, properties: { type: object } }
    ProofAiModel:
      type: object
      description: "Which model answered. `sansa/auto` is a router; `resolved` is the concrete model when SansaAI reports it. A model outside the eval baseline is flagged, never blocked."
      properties: { provider: { type: string, example: sansaai }, name: { type: string, example: sansa/auto }, resolved: { type: [string, "null"] }, evaluated: { type: boolean }, needsHumanReview: { type: boolean }, reviewReason: { type: [string, "null"], enum: [model_unevaluated, resolved_model_unknown, null] }, reviewMessage: { type: [string, "null"], example: "Made by a model we haven't evaluated yet; review before approving." } }
    ProofSummaryValidation:
      type: object
      description: "Caveat checks on the summary. passed: returned as the model wrote it; revised: statements removed or required caveats added; fallback: no model sentence survived and a fixed summary is returned."
      properties: { status: { type: string, enum: [passed, revised, fallback] }, issues: { type: array, items: { type: object, properties: { location: { type: string }, code: { type: string, enum: [missing_incentive_disclosure, incentive_misstated, missing_verification_state, verification_overclaim, missing_consent_limits, use_outside_consent, identity_beyond_consent, invented_number] }, detail: { type: string }, text: { type: string } } } }, rejected: { type: array, items: { type: string } }, added: { type: array, items: { type: string } } }
    ProofItemSummary:
      type: object
      properties: { summary: { type: string }, sentiment: { type: string, enum: [positive, mixed, neutral, negative] }, caveats: { type: array, items: { type: string } }, sourceVersionId: { type: string }, label: { type: string }, actionLevel: { type: string, enum: [read] }, validation: { $ref: "#/components/schemas/ProofSummaryValidation" }, needsHumanReview: { type: boolean }, reviewMessage: { type: [string, "null"] }, program: { type: object, properties: { id: { type: string }, version: { type: string } } }, model: { $ref: "#/components/schemas/ProofAiModel" }, operationId: { type: string } }
    ProofAskDraft:
      type: object
      properties: { draft: { type: object, properties: { subject: { type: string }, body: { type: string } } }, label: { type: string }, actionLevel: { type: string, enum: [draft] }, requiresApproval: { type: string }, needsHumanReview: { type: boolean }, reviewMessage: { type: [string, "null"] }, policyChecks: { type: array, items: { type: object, properties: { name: { type: string }, status: { type: string, enum: [pass, fail] } } } }, program: { type: object, properties: { id: { type: string }, version: { type: string } } }, model: { $ref: "#/components/schemas/ProofAiModel" }, operationId: { type: string } }
