> ## 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.

# Upload a dataset or structure file

> Answers a single-use `url` to send one file to, so its content never has to be typed into a call. Then send the file with a plain PUT, no key needed: `curl -sS -X PUT --data-binary @<file> <url>`, which prints what was stored. `kind: "items"`: a dataset file, one example per line (JSONL; each line is exactly one import_examples example, its `claims` optional), up to 50 MB and 50,000 lines. create_run and import_examples then take its lines as `items_from: {upload_id, offset?, limit?}`, 200 a call. Refused (400 CONTENT_NOT_KEPT) on a decision that keeps no content (`store_state: false`). `kind: "structure"`: a decision file (`{decision_id?, structure, objective?}`) or a bare structure, up to 1 MB; the PUT validates it as `POST /v1/validate` does and answers its `structure_hash`, which a dry run (evaluate or create_run) can name, and `POST /v1/decisions` can register by hash, while the upload lives. The URL takes the file for 15 minutes (`expires_at`), and a send then has 15 minutes to arrive; once received, calls can name the upload for about a day. A send that fails (other than by running out of time) gives the URL at least 15 more minutes for another try, up to an hour after the upload was created. Each call makes a new upload, so it takes no Idempotency-Key: one never sent expires unused. A team holds at most 200 unexpired items uploads and 1,000 structure uploads (409 UPLOADS_LIMIT past that). The URL carries its own authorization, so keep it out of anything shared.



## OpenAPI

````yaml /openapi.json post /v1/decisions/{decision_id}/uploads
openapi: 3.1.0
info:
  title: Moonbounce Decisions API
  version: 0.0.0
  description: >-
    Moonbounce decisions: evaluate an item's state against a decision structure
    (System One questions, attributes you supply and aggregations over both),
    record feedback and import labeled examples, review a ranked queue, and
    improve the structure 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/validate`, and an
    upload's own `url` (`PUT /v1/uploads/{upload_id}/{token}`), which its token
    authorizes. 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.
    A larger dataset, or a file you'd rather not paste, goes up as a file:
    `createUpload` answers a URL to PUT it to.


    **Retries.** Send an `Idempotency-Key` on every POST under /v1 that takes
    one (all but `POST /v1/validate`, `POST /v1/review/items/fill`, whose retry
    is another fill within the day's budget, and `createUpload`, whose retry is
    another upload). 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.


    **Lists.** A list answers one page: `{environment, items, next_cursor,
    has_more}` (named `<Thing>Page`), in the order the operation states. Pass
    `next_cursor` back as `cursor`, with the same filters, while `has_more` is
    true; on the last page `next_cursor` is null. `limit` defaults to 50 and is
    at most 100, with these exceptions: cases go up to 200 a page (a run's
    batch; 50 with content), suggestions default to 20, since each carries its
    whole candidate structure and evidence, and the review queue (a reviewer's
    screen) defaults to 20, at most 50.


    **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 for requests without a valid key; a 429
    RATE_LIMITED carries `Retry-After`.
servers:
  - url: https://api.moonbounce.ai
    description: Production
security: []
tags:
  - name: Evaluate
    description: Run one item through a decision and read evaluations back. Start here.
  - name: Decisions
    description: >-
      Validate a decision file, register its versions, and read decisions and
      versions.
  - name: Feedback
    description: >-
      Say what the right answer was, import labeled examples, and read feedback
      and cases.
  - name: Suggestions
    description: Ask for a better version of a decision, and follow the improvement loop.
  - name: Uploads
    description: >-
      Send a dataset or structure file once, then name it in runs, imports and
      registrations.
  - name: Previews & tests
    description: >-
      Replay a structure change over past cases, and score a version over many
      items.
  - name: Models
    description: List the models a decision can run on and choose one.
  - name: Objective
    description: >-
      Set what a decision optimizes for, then judge runs and tune cuts against
      it.
  - name: Usage
    description: What a decision's model calls cost.
  - name: Account
    description: Identify your key, and check that the API is up.
paths:
  /v1/decisions/{decision_id}/uploads:
    post:
      tags:
        - Uploads
      summary: Upload a dataset or structure file
      description: >-
        Answers a single-use `url` to send one file to, so its content never has
        to be typed into a call. Then send the file with a plain PUT, no key
        needed: `curl -sS -X PUT --data-binary @<file> <url>`, which prints what
        was stored. `kind: "items"`: a dataset file, one example per line
        (JSONL; each line is exactly one import_examples example, its `claims`
        optional), up to 50 MB and 50,000 lines. create_run and import_examples
        then take its lines as `items_from: {upload_id, offset?, limit?}`, 200 a
        call. Refused (400 CONTENT_NOT_KEPT) on a decision that keeps no content
        (`store_state: false`). `kind: "structure"`: a decision file
        (`{decision_id?, structure, objective?}`) or a bare structure, up to 1
        MB; the PUT validates it as `POST /v1/validate` does and answers its
        `structure_hash`, which a dry run (evaluate or create_run) can name, and
        `POST /v1/decisions` can register by hash, while the upload lives. The
        URL takes the file for 15 minutes (`expires_at`), and a send then has 15
        minutes to arrive; once received, calls can name the upload for about a
        day. A send that fails (other than by running out of time) gives the URL
        at least 15 more minutes for another try, up to an hour after the upload
        was created. Each call makes a new upload, so it takes no
        Idempotency-Key: one never sent expires unused. A team holds at most 200
        unexpired items uploads and 1,000 structure uploads (409 UPLOADS_LIMIT
        past that). The URL carries its own authorization, so keep it out of
        anything shared.
      operationId: createUpload
      parameters:
        - schema:
            $ref: '#/components/schemas/DecisionId'
          required: true
          description: >-
            A name you choose for this decision, such as `refund-exception`: a
            lowercase letter, then up to 127 of `a-z 0-9 _ . -`. Unique in your
            organization and shared by its test and production keys. The first
            registration or evaluation under a new one creates the decision.
          name: decision_id
          in: path
      requestBody:
        required: true
        content:
          application/json:
            examples:
              dataset:
                summary: A dataset file, one example per line
                value:
                  kind: items
              structure:
                summary: A candidate structure file
                value:
                  kind: structure
            schema:
              $ref: '#/components/schemas/CreateUploadRequest'
      responses:
        '201':
          description: The upload, waiting for its file
          content:
            application/json:
              examples:
                dataset:
                  summary: Send the file to `url` with curl within 15 minutes
                  value:
                    environment: production
                    upload_id: upl_01900000-0000-7000-8000-000000000001
                    kind: items
                    url: >-
                      https://api.moonbounce.ai/v1/uploads/upl_01900000-0000-7000-8000-000000000001/q2V8eJ3x9Ld0RwQm7yTnA4bKpZs1HcUoXvGfIjEtMWa
                    method: PUT
                    expires_at: '2026-11-09T12:15:00Z'
                    max_bytes: 52428800
                    max_lines: 50000
              schema:
                $ref: '#/components/schemas/CreateUploadResponse'
        '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'
        '409':
          description: Conflict with the current state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
        '413':
          description: The HTTP body exceeds 1 MB (reason BODY_TOO_LARGE)
          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)
          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'
        '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}$
      description: >-
        A name you choose for this decision, such as `refund-exception`: a
        lowercase letter, then up to 127 of `a-z 0-9 _ . -`. Unique in your
        organization and shared by its test and production keys. The first
        registration or evaluation under a new one creates the decision.
    CreateUploadRequest:
      type: object
      properties:
        kind:
          $ref: '#/components/schemas/UploadKind'
      required:
        - kind
      additionalProperties: false
    CreateUploadResponse:
      type: object
      properties:
        environment:
          $ref: '#/components/schemas/Environment'
        upload_id:
          $ref: '#/components/schemas/UploadIdOutput'
        kind:
          $ref: '#/components/schemas/UploadKindOutput'
        url:
          type: string
          description: >-
            Send the file here, once, before `expires_at`: `curl -sS -X PUT
            --data-binary @<file> <url>`. It carries its own authorization (no
            key), so keep it out of anything shared.
        method:
          type: string
          enum:
            - PUT
        expires_at:
          type: string
          format: date-time
          description: >-
            When the URL stops taking the file. Create another upload after
            that.
        max_bytes:
          type: integer
          description: The most bytes the file may hold.
        max_lines:
          type: integer
          description: The most lines an items file may hold.
      required:
        - environment
        - upload_id
        - kind
        - url
        - method
        - expires_at
        - max_bytes
      additionalProperties: true
      x-moonbounce-sdk-known-fields: true
    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
                (`attribute_values.region`, `claims.1.value`).
            details:
              type: object
              properties:
                issues:
                  type: array
                  items:
                    $ref: '#/components/schemas/ErrorIssue'
              additionalProperties: {}
          required:
            - type
            - message
          additionalProperties: true
          x-moonbounce-sdk-known-fields: true
      required:
        - error
      additionalProperties: true
      x-moonbounce-sdk-known-fields: true
    UploadKind:
      type: string
      enum:
        - items
        - structure
      description: >-
        `items`: a dataset file, one example per line (JSONL), named later by
        `upload_id` in `items_from`. `structure`: a decision file or a bare
        structure, named later by its `structure_hash`.
    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; decisions and their versions belong to the whole
        organization.
    UploadIdOutput:
      type: string
      pattern: >-
        ^upl_[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$
    UploadKindOutput:
      type: string
      enum:
        - items
        - structure
      description: >-
        `items`: a dataset file, one example per line (JSONL), named later by
        `upload_id` in `items_from`. `structure`: a decision file or a bare
        structure, named later by its `structure_hash`.
    ErrorType:
      type: string
      enum:
        - invalid_request
        - authentication
        - permission_denied
        - not_found
        - conflict
        - rate_limited
        - engine_failed
        - unavailable
        - internal
    ErrorIssue:
      type: object
      properties:
        param:
          type: string
        code:
          type: string
          pattern: ^[A-Z][A-Z0-9_]+$
          description: >-
            Stable, extensible issue code; unknown codes still have a parameter
            and human-readable message.
        message:
          type: string
      required:
        - param
        - code
        - message
      additionalProperties: true
      x-moonbounce-sdk-known-fields: true
  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.