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

# Preview a spec change against past feedback

> Replays a spec change over past cases with feedback: it spends engine calls, stores a run, and burns the held-out cases it lists. Not a dry run of evaluate. Replays a local spec (with its weights pin and cut points) and its base over the decision's reviewed and seeded cases in the key's environment (a test key previews against test cases only), and returns fixes, breaks, the action mix at the cut points and an evidence label. Writes an eval run only: never an evaluation, never traffic, never acceptance. A preview does not register the candidate: before evaluating by its structure hash, register the spec with POST /v1/decisions/{decision_id}/structures (or send the spec inline on evaluate once). Holdout cases it lists individually (fixes and breaks) are burned (they count as training from then on); unlisted holdout cases are only scored. `evidence.by_split.holdout.holdout_uses` counts preview runs that scored held-out cases, not burns. The base, in order: the base of `suggestion_id` when it is sent (then `base` is ignored), else `base`, else the version serving primary traffic in the key's environment, else the latest version seeded there, else none. With `suggestion_id`, `hashes_match` says whether the spec is that suggestion's candidate; another environment's suggestion is 404. The evidence replays only cases with an outcome from the owner, reviewers or seeds; the action mix replays a sample of the environment's recent primary traffic. For features the candidate changes, `before` is a fresh re-run of the base feature (the fair baseline), so engine nondeterminism can move it between previews; unchanged features replay cached answers when available. `noise_rate` measures outcome flips from that re-run against the cached baseline, not score drift. Holdout burns and use counts are per environment, and a test key's preview neither reads nor fills the shared cache of engine answers. A decision nothing was registered, evaluated or seeded under is 404.



## OpenAPI

````yaml /openapi.json post /v1/decisions/{decision_id}/previews
openapi: 3.1.0
info:
  title: Moonbounce Decisions API
  version: 0.0.0
  description: >-
    Moonbounce decisions: evaluate items against a decision spec (features
    combined by aggregators), record feedback and seed examples, review a ranked
    queue, and improve the spec through suggestions that ship as code diffs,
    each with a preview (backtest) and an evidence label.


    **Authentication.** Send an API key as `Authorization: Bearer <key>` on
    every operation except `GET /health` and `POST /v1/specs/hash`. Call the API
    from a server, never a browser: a key must not reach one, and the API sends
    no CORS headers. A key belongs to one organization and one environment:
    production, or test for a key with the `env:test` permission. Test traffic
    never counts as production traffic.


    **Requests.** JSON bodies (`Content-Type: application/json`) of at most 1
    MB, parsed strictly: duplicate keys, lone surrogates, integers outside
    ±(2^53 - 1), `__proto__` keys, U+0000 and nesting deeper than 64 are a 400.


    **Retries.** Send an `Idempotency-Key` on every POST under /v1 that takes
    one (all but `POST /v1/specs/hash`). Keep it for the request's retries. The
    same key with the same request returns the first stored result, marked
    `Idempotent-Replayed: true`, for 24 hours (410 if that result was since
    deleted); with another request it is 422 IDEMPOTENCY_KEY_REUSED. Keys are
    scoped to the organization and environment across every operation, so use a
    fresh key per request. A request that failed before its result was stored
    runs again when retried. 429 and 503 under /v1 may carry `Retry-After`
    (seconds); a daily QUOTA_EXCEEDED cap instead names its reset time.


    **Errors.** Every error under /v1 is `{"error": {"type", "message",
    "reason"?, "param"?, "details"?}}` (ErrorBody); `GET /health` answers its
    own shape. Every response carries a `Request-Id` header. Rate limits apply
    per key and per client IP, counted per Cloudflare location.
servers:
  - url: https://api.moonbounce.ai
    description: Production
security: []
paths:
  /v1/decisions/{decision_id}/previews:
    post:
      tags:
        - suggestions
      summary: Preview a spec change against past feedback
      description: >-
        Replays a spec change over past cases with feedback: it spends engine
        calls, stores a run, and burns the held-out cases it lists. Not a dry
        run of evaluate. Replays a local spec (with its weights pin and cut
        points) and its base over the decision's reviewed and seeded cases in
        the key's environment (a test key previews against test cases only), and
        returns fixes, breaks, the action mix at the cut points and an evidence
        label. Writes an eval run only: never an evaluation, never traffic,
        never acceptance. A preview does not register the candidate: before
        evaluating by its structure hash, register the spec with POST
        /v1/decisions/{decision_id}/structures (or send the spec inline on
        evaluate once). Holdout cases it lists individually (fixes and breaks)
        are burned (they count as training from then on); unlisted holdout cases
        are only scored. `evidence.by_split.holdout.holdout_uses` counts preview
        runs that scored held-out cases, not burns. The base, in order: the base
        of `suggestion_id` when it is sent (then `base` is ignored), else
        `base`, else the version serving primary traffic in the key's
        environment, else the latest version seeded there, else none. With
        `suggestion_id`, `hashes_match` says whether the spec is that
        suggestion's candidate; another environment's suggestion is 404. The
        evidence replays only cases with an outcome from the owner, reviewers or
        seeds; the action mix replays a sample of the environment's recent
        primary traffic. For features the candidate changes, `before` is a fresh
        re-run of the base feature (the fair baseline), so engine nondeterminism
        can move it between previews; unchanged features replay cached answers
        when available. `noise_rate` measures outcome flips from that re-run
        against the cached baseline, not score drift. Holdout burns and use
        counts are per environment, and a test key's preview neither reads nor
        fills the shared cache of engine answers. A decision nothing was
        registered, evaluated or seeded under is 404.
      operationId: preview
      parameters:
        - schema:
            $ref: '#/components/schemas/DecisionId'
          required: true
          name: decision_id
          in: path
        - schema:
            type: string
            minLength: 1
            maxLength: 255
            pattern: ^[\x20-\x7e]+$
            description: >-
              Makes a retry safe. For 24 hours, the same key with the same
              request returns the first stored result (with Idempotent-Replayed:
              true); the same key with a different request is a 422
              IDEMPOTENCY_KEY_REUSED. Keys are scoped to the organization and
              environment across every operation, so use a fresh key per
              request. A request that failed before its result was stored runs
              again when retried with its key.
          required: false
          description: >-
            Makes a retry safe. For 24 hours, the same key with the same request
            returns the first stored result (with Idempotent-Replayed: true);
            the same key with a different request is a 422
            IDEMPOTENCY_KEY_REUSED. Keys are scoped to the organization and
            environment across every operation, so use a fresh key per request.
            A request that failed before its result was stored runs again when
            retried with its key.
          name: idempotency-key
          in: header
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreviewRequest'
      responses:
        '200':
          description: The preview
          headers:
            Idempotent-Replayed:
              schema:
                type: string
                enum:
                  - 'true'
                description: >-
                  Present and `true` when this is the stored result of an
                  earlier request with the same Idempotency-Key.
              required: false
              description: >-
                Present and `true` when this is the stored result of an earlier
                request with the same Idempotency-Key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Preview'
        '400':
          description: 'Invalid request: `param` names the field at fault when one applies'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '401':
          description: Missing or invalid API key (reason KEY_INVALID)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '403':
          description: The key may not do this
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '404':
          description: Not found, or not in this key's organization and environment
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '409':
          description: Conflict with the current state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '410':
          description: >-
            The Idempotency-Key's first result was deleted, so it cannot be
            replayed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '422':
          description: >-
            The request is well formed but cannot be processed (for example
            reason IDEMPOTENCY_KEY_REUSED)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '429':
          description: >-
            RATE_LIMITED: retry after Retry-After seconds; QUOTA_EXCEEDED:
            details.reset_at gives the daily reset time (no Retry-After), or
            details.too_large asks for a smaller request
          headers:
            Retry-After:
              schema:
                type: string
                description: Seconds to wait before retrying, when waiting helps.
              required: false
              description: Seconds to wait before retrying, when waiting helps.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '502':
          description: >-
            An engine failed to answer a feature the output needs (type
            engine_failed, reason ENGINE_FAILED)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '503':
          description: >-
            Temporarily unavailable, with Retry-After (for example reason
            AUTH_UNAVAILABLE, ENGINE_UNAVAILABLE or STORE_UNAVAILABLE): retry
            with the same Idempotency-Key
          headers:
            Retry-After:
              schema:
                type: string
                description: Seconds to wait before retrying, when waiting helps.
              required: true
              description: Seconds to wait before retrying, when waiting helps.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
      security:
        - bearerAuth:
            - decisions:develop
components:
  schemas:
    DecisionId:
      type: string
      pattern: ^[a-z][a-z0-9_.-]{0,127}$
    PreviewRequest:
      type: object
      properties:
        spec:
          $ref: '#/components/schemas/Spec'
        weights:
          $ref: '#/components/schemas/WeightsRef'
        outcomes:
          $ref: '#/components/schemas/Outcomes'
        suggestion_id:
          $ref: '#/components/schemas/SugId'
        base:
          type: object
          properties:
            structure:
              $ref: '#/components/schemas/VersionHash'
            weights:
              $ref: '#/components/schemas/VersionHash'
          required:
            - structure
            - weights
          additionalProperties: false
      required:
        - spec
        - weights
        - outcomes
      additionalProperties: false
    Preview:
      type: object
      properties:
        preview_id:
          $ref: '#/components/schemas/RunId'
        decision_id:
          $ref: '#/components/schemas/DecisionId'
        environment:
          $ref: '#/components/schemas/Environment'
        base:
          anyOf:
            - $ref: '#/components/schemas/VersionRef'
            - type: 'null'
        candidate:
          $ref: '#/components/schemas/VersionRef'
        hashes_match:
          anyOf:
            - type: object
              properties:
                structure:
                  type: boolean
                weights:
                  type: boolean
              required:
                - structure
                - weights
              additionalProperties: false
            - type: 'null'
        evidence:
          $ref: '#/components/schemas/Evidence'
        cost:
          $ref: '#/components/schemas/PreviewCost'
      required:
        - preview_id
        - decision_id
        - environment
        - base
        - candidate
        - hashes_match
        - evidence
        - cost
      additionalProperties: false
    ErrorBody:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              $ref: '#/components/schemas/ErrorType'
            reason:
              type: string
              pattern: ^[A-Z][A-Z0-9_]+$
              description: >-
                Set where a caller can act on it, one of the reasons in the
                ErrorReason schema. New reasons may be added: treat an unknown
                one by the status and `type`.
            message:
              type: string
              description: For people; don't parse it.
            param:
              type: string
              description: >-
                The request field at fault, as a dotted path (`subject.0.text`,
                `claims.1.value`).
            details:
              type: object
              additionalProperties: {}
          required:
            - type
            - message
          additionalProperties: false
      required:
        - error
      additionalProperties: false
    Spec:
      type: object
      properties:
        format:
          type: string
          enum:
            - moonbounce.spec/0
        guidance:
          type: string
          minLength: 1
          maxLength: 20000
        nodes:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/SpecNode'
          description: >-
            The spec's named nodes, 1 to 64: boolean features and the
            aggregators that read them. Every reference (`output`, a combine
            term's `node`, a calibrate's `node`) names a node in this map, with
            no cycles.
        output:
          $ref: '#/components/schemas/Name'
        target:
          type: string
          minLength: 1
          maxLength: 4000
          description: >-
            The yes/no statement the output's score is the probability of.
            Required when `output` is a combine or calibrate node, optional on a
            boolean output. Feedback `target` claims need it.
        meta:
          $ref: '#/components/schemas/Meta'
      required:
        - format
        - nodes
        - output
      additionalProperties: false
    WeightsRef:
      anyOf:
        - type: string
          enum:
            - default
        - $ref: '#/components/schemas/VersionHash'
      default: default
    Outcomes:
      type: object
      properties:
        below:
          type: string
          pattern: ^[a-z][a-z0-9_]{0,63}$
          description: The outcome for a score under the first cut.
        cuts:
          type: array
          items:
            type: object
            properties:
              at:
                type: number
              outcome:
                $ref: '#/components/schemas/Name'
            required:
              - at
              - outcome
            additionalProperties: false
          minItems: 1
          maxItems: 9
        abstain:
          $ref: '#/components/schemas/Name'
      required:
        - below
        - cuts
      additionalProperties: false
    SugId:
      type: string
      pattern: >-
        ^sug_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
    VersionHash:
      type: string
      pattern: ^sha256:[0-9a-f]{64}$
    RunId:
      type: string
      pattern: >-
        ^run_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
    Environment:
      type: string
      enum:
        - production
        - test
      description: >-
        The environment of the key that made the call: `production` or `test`.
        Evaluations, feedback, reviews and suggestions are separate per
        environment; registered structures are shared.
    VersionRef:
      type: object
      properties:
        structure:
          $ref: '#/components/schemas/VersionHash'
        weights:
          $ref: '#/components/schemas/VersionHash'
        engine_profile:
          type: string
      required:
        - structure
        - weights
        - engine_profile
      additionalProperties: false
    Evidence:
      type: object
      properties:
        label:
          $ref: '#/components/schemas/EvidenceLabel'
        frozen_at:
          $ref: '#/components/schemas/Timestamp'
        by_split:
          type: object
          properties:
            seed:
              $ref: '#/components/schemas/SplitStats'
            train:
              $ref: '#/components/schemas/SplitStats'
            holdout:
              $ref: '#/components/schemas/SplitStats'
          additionalProperties: false
        noise_rate:
          type:
            - number
            - 'null'
          minimum: 0
          maximum: 1
        by_source:
          type: object
          properties:
            app_native:
              $ref: '#/components/schemas/SplitStats'
            app_resolution:
              $ref: '#/components/schemas/SplitStats'
            reviewer:
              $ref: '#/components/schemas/SplitStats'
            owner:
              $ref: '#/components/schemas/SplitStats'
            import:
              $ref: '#/components/schemas/SplitStats'
            synthetic_review:
              $ref: '#/components/schemas/SplitStats'
        action_mix:
          anyOf:
            - $ref: '#/components/schemas/ActionMix'
            - type: 'null'
        changed_items:
          type: array
          items:
            $ref: '#/components/schemas/ChangedItem'
        credited_feedback:
          type: array
          items:
            $ref: '#/components/schemas/FbkId'
      required:
        - label
        - frozen_at
        - by_split
        - noise_rate
        - action_mix
        - changed_items
        - credited_feedback
      additionalProperties: false
    PreviewCost:
      type: object
      properties:
        engine_calls:
          type: integer
          minimum: 0
          description: >-
            On evaluate and seeds, upstream engine calls reported in usage,
            including retries; one call can answer several features (Jev
            compiles them). On previews, features sent to engines, not upstream
            calls.
        cache_hits:
          type: integer
          minimum: 0
          description: >-
            Feature answers reused from cache. Only previews use the cache
            today; evaluate and seeds report 0.
        input_tokens:
          type: integer
          minimum: 0
        usd_micros:
          type: integer
          minimum: 0
      required:
        - engine_calls
        - cache_hits
        - input_tokens
        - usd_micros
      additionalProperties: false
    ErrorType:
      type: string
      enum:
        - invalid_request
        - authentication
        - permission_denied
        - not_found
        - conflict
        - rate_limited
        - engine_failed
        - unavailable
        - internal
    SpecNode:
      oneOf:
        - $ref: '#/components/schemas/BooleanFeature'
        - $ref: '#/components/schemas/Combine'
        - $ref: '#/components/schemas/Calibrate'
      discriminator:
        propertyName: type
        mapping:
          boolean: '#/components/schemas/BooleanFeature'
          combine: '#/components/schemas/Combine'
          calibrate: '#/components/schemas/Calibrate'
    Name:
      type: string
      pattern: ^[a-z][a-z0-9_]{0,63}$
    Meta:
      type: object
      properties:
        title:
          type: string
          maxLength: 200
        description:
          type: string
          maxLength: 4000
        owner:
          type: string
          maxLength: 200
        tags:
          type: array
          items:
            type: string
            maxLength: 64
          maxItems: 32
        display:
          type: object
          additionalProperties:
            type: object
            properties:
              text:
                type: string
                minLength: 1
                maxLength: 40
              help:
                type: string
                maxLength: 200
            required:
              - text
            additionalProperties: false
      additionalProperties: false
    EvidenceLabel:
      type: string
      enum:
        - starting_examples
        - training_cases
        - holdout
    Timestamp:
      type: string
      format: date-time
    SplitStats:
      type: object
      properties:
        'n':
          type: integer
          minimum: 0
        fixes:
          type: integer
          minimum: 0
        breaks:
          type: integer
          minimum: 0
        base_correct:
          type: integer
          minimum: 0
        candidate_correct:
          type: integer
          minimum: 0
        holdout_uses:
          type: integer
          minimum: 0
        owner_decided:
          type: integer
          minimum: 0
      required:
        - 'n'
        - fixes
        - breaks
        - base_correct
        - candidate_correct
      additionalProperties: false
    ActionMix:
      type: object
      properties:
        before:
          type: object
          additionalProperties:
            type: number
            minimum: 0
            maximum: 1
        after:
          type: object
          additionalProperties:
            type: number
            minimum: 0
            maximum: 1
        'on':
          type: string
        'n':
          type: integer
          minimum: 0
      required:
        - before
        - after
        - 'on'
        - 'n'
      additionalProperties: false
    ChangedItem:
      type: object
      properties:
        evaluation_id:
          $ref: '#/components/schemas/EvlId'
        feedback_id:
          $ref: '#/components/schemas/FbkId'
        split:
          $ref: '#/components/schemas/Split'
        change:
          type: string
          enum:
            - fix
            - break
        before:
          type: object
          properties:
            outcome:
              $ref: '#/components/schemas/Name'
            score:
              type: number
            features:
              type: object
              additionalProperties:
                type:
                  - number
                  - 'null'
                minimum: 0
                maximum: 1
          required:
            - features
          additionalProperties: false
        after:
          type: object
          properties:
            outcome:
              $ref: '#/components/schemas/Name'
            score:
              type: number
            features:
              type: object
              additionalProperties:
                type:
                  - number
                  - 'null'
                minimum: 0
                maximum: 1
          required:
            - features
          additionalProperties: false
      required:
        - evaluation_id
        - feedback_id
        - split
        - change
        - before
        - after
      additionalProperties: false
    FbkId:
      type: string
      pattern: >-
        ^fbk_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
    BooleanFeature:
      type: object
      properties:
        type:
          type: string
          enum:
            - boolean
        prompt:
          type: string
          minLength: 1
          maxLength: 4000
        guidance:
          type: string
          minLength: 1
          maxLength: 4000
        inputs:
          type: array
          items:
            $ref: '#/components/schemas/Name'
          minItems: 1
          description: >-
            Names of the `subject` parts this feature reads (default: all of
            them); `context` parts are always read. Every name must match a
            subject part's `name`, or the request is a 400 CONTENT_INVALID.
        criteria:
          type: object
          properties:
            'true':
              type: string
              minLength: 1
              maxLength: 4000
            'false':
              type: string
              minLength: 1
              maxLength: 4000
          additionalProperties: false
      required:
        - type
        - prompt
      additionalProperties: false
    Combine:
      type: object
      properties:
        type:
          type: string
          enum:
            - combine
        terms:
          type: array
          items:
            $ref: '#/components/schemas/CombineTerm'
          minItems: 1
          maxItems: 64
        prior:
          type: string
          enum:
            - mean
            - any
          description: >-
            Default combine weights before fitting. mean averages oriented
            inputs; any computes sigmoid(-4 + 8·Σraises - 8·Σlowers), a logistic
            of a signed sum: one strong raise is enough and weak raises add up.
        fixed:
          type: object
          properties:
            link:
              type: string
              enum:
                - identity
                - logistic
              description: >-
                identity: the weighted sum plus bias, clipped to 0..1; logistic:
                the sigmoid of the weighted sum plus bias.
            weights:
              type: object
              additionalProperties:
                type: number
              description: Signed weights per term key (node; options are reserved).
            bias:
              type: number
              description: Offset added to the weighted sum before applying the link.
          required:
            - link
            - weights
            - bias
          additionalProperties: false
          description: >-
            Numbers fixed in code, never overridden by fitted weights. Signed
            weights multiply raw P(yes): raises weights must be >= 0 and lowers
            weights <= 0.
      required:
        - type
        - terms
      additionalProperties: false
      description: >-
        Scores a node from its terms. Before fitting (and without fixed
        numbers), mean averages the oriented inputs, so each of n terms weighs
        1/n and adding a term dilutes the others; any computes sigmoid(-4 +
        8·Σraises - 8·Σlowers): one strong raise is enough, weak raises add up,
        and lowers cancel raises. Fitted or fixed numbers replace the prior.
    Calibrate:
      type: object
      properties:
        type:
          type: string
          enum:
            - calibrate
        node:
          $ref: '#/components/schemas/Name'
      required:
        - type
        - node
      additionalProperties: false
    EvlId:
      type: string
      pattern: >-
        ^evl_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
    Split:
      type: string
      enum:
        - seed
        - train
        - holdout
        - burned
    CombineTerm:
      type: object
      properties:
        node:
          $ref: '#/components/schemas/Name'
        option:
          type: string
          pattern: ^[a-z][a-z0-9_]{0,63}$
          description: >-
            Reserved for choice and scale features, which are not available yet:
            any value is a 400.
        effect:
          type: string
          enum:
            - raises
            - lowers
          default: raises
          description: >-
            raises: a yes pushes the score up; lowers: a yes pushes it down. The
            sign lives here; fixed weights must agree with it.
      required:
        - node
      additionalProperties: false
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        An API key, sent as `Authorization: Bearer <key>`. Each operation lists
        the scope its key needs: `decisions:evaluate` (evaluate and read
        evaluations), `decisions:feedback` (your app's feedback) or
        `decisions:develop` (everything else, including owner feedback and the
        review queue). An operation listing no scope takes any scoped key. A key
        with no scope, or without the operation's, gets 403 INSUFFICIENT_SCOPE.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.