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

# Call posts.save_variants

> Save the version written for each destination of a post — 'save these captions for Instagram and LinkedIn', 'here's the LinkedIn version of this post'. It stores the words you send; it writes none itself. A destination must already be on the piece, each version must carry its words, and a destination that already published refuses, because its copy is the record of what went out. It saves the draft only; nothing is published. Not for the piece's own copy — that is `posts.api_fill_step`.

Every result names the workspace it ran in. Say which workspace the answer is about. When the account has more than one workspace and none is selected, this tool refuses with `workspace_ambiguous` and lists the choices — offer them, never pick one.



## OpenAPI

````yaml /openapi.json post /api/v1/tools/posts.save_variants
openapi: 3.1.0
info:
  title: Goosy Bear API
  version: 1.0.0-beta
  summary: Manage workspace content, projects, Data tables, Pages, library and credits.
  description: >-
    BETA. This API is in beta: paths, request shapes and response envelopes may
    change, and every response carries an `x-goosy-api: beta` header for as long
    as that is true. A change is additive, or it is deprecated first: a response
    to a call that uses a deprecated feature carries `Deprecation` and `Sunset`
    headers, and nothing is removed until at least 90 days after its
    deprecation. Pin the OpenAPI document you generated against and re-generate
    when it changes.
servers:
  - url: https://app.goosybear.ai
    description: Production
security:
  - tenantApiKey: []
paths:
  /api/v1/tools/posts.save_variants:
    post:
      tags:
        - posts
      summary: Call posts.save_variants
      description: >-
        Save the version written for each destination of a post — 'save these
        captions for Instagram and LinkedIn', 'here's the LinkedIn version of
        this post'. It stores the words you send; it writes none itself. A
        destination must already be on the piece, each version must carry its
        words, and a destination that already published refuses, because its
        copy is the record of what went out. It saves the draft only; nothing is
        published. Not for the piece's own copy — that is `posts.api_fill_step`.


        Every result names the workspace it ran in. Say which workspace the
        answer is about. When the account has more than one workspace and none
        is selected, this tool refuses with `workspace_ambiguous` and lists the
        choices — offer them, never pick one.
      operationId: call_posts_save_variants
      parameters:
        - name: x-request-id
          in: header
          required: false
          description: >-
            Your own id for this request, echoed back and recorded on the audit
            trail. 1–128 characters from `A–Z a–z 0–9 . _ : -`, starting with a
            letter or digit; anything else is replaced by a generated id.
          schema:
            type: string
            maxLength: 128
        - name: Idempotency-Key
          in: header
          required: true
          description: >-
            Your key for this write: send the same key when you retry and the
            first result comes back instead of a second action. Keys are
            honoured for at least 24 hours. 1–200 visible ASCII characters.
            Replaces the `idempotency_key` body field, which still works until
            2026-12-31; this action needs a key on one of the two, and the
            header is the one to send.
          schema:
            type: string
            maxLength: 200
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                piece_id:
                  type: string
                  format: uuid
                  description: >-
                    The piece's id, as posts.create_pieces,
                    content.create_request, content.status or schedule.read
                    return it.
                variants:
                  type: array
                  items:
                    type: object
                    properties:
                      platform:
                        type: string
                        description: >-
                          The destination this version is for. Must already be
                          elected.
                      caption:
                        type:
                          - string
                          - 'null'
                        description: >-
                          This destination's version of the copy — what actually
                          posts there.
                      description:
                        type:
                          - string
                          - 'null'
                        description: >-
                          The longer body, for destinations that have one (a
                          YouTube description, a Google Business post). Omit
                          where the destination only has a caption.
                      publish_at:
                        type:
                          - string
                          - 'null'
                        description: >-
                          This destination's OWN time, ISO 8601 UTC — a
                          per-platform offset from the piece's anchor. Omit
                          unless the brief asked for one; the piece's own time
                          covers every destination that has none.
                      aspect_ratio:
                        type:
                          - string
                          - 'null'
                        description: >-
                          Override the managed default aspect for this
                          destination. Omit unless the operator asked — the
                          default is the managed configuration and is nearly
                          always right.
                      hashtags:
                        anyOf:
                          - type: array
                            items:
                              type: string
                              minLength: 1
                              maxLength: 100
                              pattern: ^#?[^\s#@]+$
                            maxItems: 30
                          - type: 'null'
                        description: >-
                          The tags going out with THIS destination's version.
                          Write them WITHOUT the leading # — the surface adds
                          it, so a stored one doubles it. One word each, no
                          spaces.
                      alt_text:
                        anyOf:
                          - type: string
                            maxLength: 1000
                          - type: 'null'
                        description: >-
                          What the creative SHOWS, for a reader who cannot see
                          it — the subject, what they are doing, the setting.
                          Describe the image, never repeat the caption.
                      first_comment:
                        anyOf:
                          - type: string
                            maxLength: 2200
                          - type: 'null'
                        description: >-
                          The comment posted immediately behind the piece, where
                          the destination has the convention. Omit on
                          destinations that do not.
                      location_label:
                        anyOf:
                          - type: string
                            maxLength: 200
                          - type: 'null'
                        description: >-
                          The place this post is tagged at, as a human label
                          ("Northwind Trading Co. · Portland, OR"). Never invent
                          one the operator has not given you.
                      tagged_handles:
                        anyOf:
                          - type: array
                            items:
                              type: string
                              minLength: 1
                              maxLength: 100
                              pattern: ^@?[^\s@]+$
                            maxItems: 20
                          - type: 'null'
                        description: >-
                          The accounts tagged in this destination's version,
                          WITHOUT the leading @. Only accounts the operator
                          named — a tagged account is a claim about someone
                          else.
                      story:
                        anyOf:
                          - type: object
                            properties:
                              frames:
                                type: array
                                items:
                                  type: object
                                  properties:
                                    text_sticker:
                                      anyOf:
                                        - type: string
                                          maxLength: 300
                                        - type: 'null'
                                      description: >-
                                        The white pill laid over this frame's
                                        picture. A short line, not a caption —
                                        omit it where the picture speaks for
                                        itself.
                                    link_sticker_url:
                                      anyOf:
                                        - type: string
                                          format: uri
                                        - type: 'null'
                                      description: >-
                                        Where this frame's link sticker sends a
                                        viewer. Only a destination the operator
                                        named.
                                    link_sticker_label:
                                      anyOf:
                                        - type: string
                                          maxLength: 60
                                        - type: 'null'
                                      description: >-
                                        The words ON the link pill ("See the
                                        menu"). Ignored without link_sticker_url
                                        — a label with no destination is a pill
                                        that goes nowhere.
                                    music_label:
                                      anyOf:
                                        - type: string
                                          maxLength: 200
                                        - type: 'null'
                                      description: >-
                                        The track as the platform NAMES it
                                        ("Golden Hour · JVKE"). Only a track the
                                        operator picked — never one you chose
                                        for them.
                                    alt_text:
                                      anyOf:
                                        - type: string
                                          maxLength: 1000
                                        - type: 'null'
                                      description: >-
                                        What THIS frame's picture shows, for a
                                        reader who cannot see it. Per frame —
                                        three frames are three different
                                        pictures.
                                    poll:
                                      anyOf:
                                        - type: object
                                          properties:
                                            question:
                                              type: string
                                              maxLength: 200
                                            options:
                                              type: array
                                              items:
                                                type: string
                                                maxLength: 60
                                              minItems: 2
                                              maxItems: 4
                                          required:
                                            - question
                                            - options
                                          additionalProperties: false
                                        - type: 'null'
                                      description: >-
                                        The poll sticker on this frame. A
                                        question with two to four options;
                                        anything less is a question, not a poll.
                                    mention_handles:
                                      anyOf:
                                        - type: array
                                          items:
                                            type: string
                                            minLength: 1
                                            maxLength: 100
                                            pattern: ^@?[^\s@]+$
                                          maxItems: 10
                                        - type: 'null'
                                      description: >-
                                        Accounts @-mentioned ON this frame,
                                        WITHOUT the leading @ — the surface adds
                                        it. Only accounts the operator named.
                                    duration_seconds:
                                      anyOf:
                                        - type: integer
                                          minimum: 1
                                          maximum: 60
                                        - type: 'null'
                                      description: >-
                                        How long this frame sits on screen. Omit
                                        for the platforms' own 5-second still.
                                  additionalProperties: false
                                minItems: 1
                                maxItems: 10
                                description: >-
                                  EVERY frame of this story, in order — the
                                  first entry is the frame a viewer sees first.
                                  This REPLACES the whole rail: any frame you do
                                  not list is removed.
                              expires_at:
                                type:
                                  - string
                                  - 'null'
                                description: >-
                                  When the story stops being visible, ISO 8601
                                  UTC. Normally 24 hours after it posts; omit
                                  unless the operator said otherwise.
                              highlight_name:
                                anyOf:
                                  - type: string
                                    maxLength: 100
                                  - type: 'null'
                                description: >-
                                  The highlight the story is saved into after it
                                  expires. Only one the operator named.
                              replies_mode:
                                anyOf:
                                  - type: string
                                    enum:
                                      - everyone
                                      - followers
                                      - 'off'
                                  - type: 'null'
                                description: >-
                                  WHO MAY REPLY to the story. Omit to leave the
                                  account's own default — never guess it.
                            required:
                              - frames
                            additionalProperties: false
                          - type: 'null'
                        description: >-
                          Only on a stories piece: every frame of this
                          destination's story, in order, plus when it expires,
                          the highlight it is saved to and who may reply.
                          Including this REPLACES the whole rail.
                    required:
                      - platform
                    additionalProperties: false
                  minItems: 1
                  maxItems: 12
                  description: >-
                    Every destination's version, in ONE call. Each one must
                    carry the words — a destination with only its name on it is
                    refused, because there is nothing to put on the piece.
                idempotency_key:
                  type: string
                  minLength: 1
                  maxLength: 200
                  description: >-
                    Deprecated on REST: send the `Idempotency-Key` header
                    instead. Still accepted until 2026-12-31; if both are sent
                    they must match.
                workspace:
                  type: string
                  minLength: 1
                  description: >-
                    Which workspace to run in — its slug. Omit to use your
                    default. With more than one reachable workspace and no
                    default, the call is refused and the choices are listed.
              required:
                - piece_id
                - variants
              additionalProperties: false
      responses:
        '200':
          description: >-
            The call was admitted and dispatched. `ok` says whether the tool
            succeeded — a refusal the tool itself produced is still a 200,
            exactly as it is a successful JSON-RPC result over MCP.
          headers:
            x-goosy-api:
              description: Present while this API is in beta; the value is `beta`.
              schema:
                type: string
                const: beta
            x-request-id:
              description: >-
                This request's id: the `x-request-id` you sent when it was well
                formed, otherwise one generated for you. Quote it to support; it
                is recorded on every audit row the call wrote.
              schema:
                type: string
            RateLimit-Limit:
              description: Requests this key may make in a rolling minute.
              schema:
                type: integer
                minimum: 1
            RateLimit-Remaining:
              description: Requests left in the current rolling minute.
              schema:
                type: integer
                minimum: 0
            RateLimit-Reset:
              description: Whole seconds until the window frees more requests.
              schema:
                type: integer
                minimum: 1
            x-goosy-tool-error:
              description: >-
                `true` when a boundary step refused the call — workspace
                resolution failed or could not be read. A tool that ran and
                returned its own typed `ok: false` reports `false` here.
                **Branch on `ok` in the body for the outcome; this header is the
                transport-level hint.**
              schema:
                type: string
                enum:
                  - 'true'
                  - 'false'
          content:
            application/json:
              schema:
                oneOf:
                  - type: object
                    description: The tool ran and answered.
                    properties:
                      ok:
                        type: boolean
                        const: true
                      piece_id:
                        type: string
                      saved:
                        type: array
                        items:
                          type: string
                      refused:
                        type: array
                        items:
                          type: object
                          properties:
                            platform:
                              type: string
                            code:
                              type: string
                            message:
                              type: string
                          required:
                            - platform
                            - code
                            - message
                          additionalProperties: false
                      replayed:
                        type: boolean
                        description: >-
                          True when this is the first result of an earlier call
                          with the same idempotency_key, returned without
                          running anything again.
                      workspace:
                        type: string
                        description: The slug of the workspace this call ran in.
                      working_in:
                        type: object
                        properties:
                          label:
                            type: string
                            description: 'The chip words — e.g. "working in: Acme Main".'
                          note:
                            type: string
                            description: >-
                              Why this workspace — e.g. "your default", "this
                              call only".
                          workspace:
                            type: string
                            description: The resolved workspace's slug.
                          source:
                            type: string
                            enum:
                              - call-override
                              - key-pin
                              - stored-default
                              - sole-reachable
                            description: >-
                              Which rung of the R7 precedence resolved the
                              workspace.
                        required:
                          - label
                          - note
                          - workspace
                          - source
                        additionalProperties: false
                        description: >-
                          Which workspace this call ran in, and how that was
                          decided. Present on every workspace-scoped result.
                    required:
                      - ok
                      - piece_id
                      - refused
                      - replayed
                      - saved
                      - working_in
                      - workspace
                  - $ref: '#/components/schemas/ToolRefusal'
        '400':
          description: >-
            The request could not be used: the body is present but not a JSON
            object (`tenant_api.malformed_body`); it is a top-level array
            carrying more messages than this endpoint accepts
            (`tenant_mcp.batch_too_large`); or the `Idempotency-Key` header is
            malformed (`tenant_api.idempotency_key_invalid`), disagrees with the
            body's `idempotency_key` (`tenant_api.idempotency_key_conflict`), or
            was sent to an action that cannot replay a retry
            (`tenant_api.idempotency_unsupported`). Nothing ran.
          headers:
            x-goosy-api:
              description: Present while this API is in beta; the value is `beta`.
              schema:
                type: string
                const: beta
            x-request-id:
              description: >-
                This request's id: the `x-request-id` you sent when it was well
                formed, otherwise one generated for you. Quote it to support; it
                is recorded on every audit row the call wrote.
              schema:
                type: string
            Deprecation:
              description: >-
                RFC 9745: this refusal's nested `error` object is deprecated,
                since this instant (`@<unix seconds>`).
              schema:
                type: string
            Sunset:
              description: >-
                RFC 8594: the nested `error` object may be removed after this
                date (2026-12-31).
              schema:
                type: string
            Link:
              description: The page that says what to read instead (`rel="deprecation"`).
              schema:
                type: string
            RateLimit-Limit:
              description: Requests this key may make in a rolling minute.
              schema:
                type: integer
                minimum: 1
            RateLimit-Remaining:
              description: Requests left in the current rolling minute.
              schema:
                type: integer
                minimum: 0
            RateLimit-Reset:
              description: Whole seconds until the window frees more requests.
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: >-
            No bearer credential, or one that did not resolve. Deliberately ONE
            undifferentiated answer for absent / unknown / revoked / expired, so
            the endpoint is never an oracle over the key space.
          headers:
            x-goosy-api:
              description: Present while this API is in beta; the value is `beta`.
              schema:
                type: string
                const: beta
            x-request-id:
              description: >-
                This request's id: the `x-request-id` you sent when it was well
                formed, otherwise one generated for you. Quote it to support; it
                is recorded on every audit row the call wrote.
              schema:
                type: string
            Deprecation:
              description: >-
                RFC 9745: this refusal's nested `error` object is deprecated,
                since this instant (`@<unix seconds>`).
              schema:
                type: string
            Sunset:
              description: >-
                RFC 8594: the nested `error` object may be removed after this
                date (2026-12-31).
              schema:
                type: string
            Link:
              description: The page that says what to read instead (`rel="deprecation"`).
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: >-
            The credential is good but the surface is not open to it — the
            platform or account API switch is off, or the holder lacks the
            `api.access` capability. The `code` says which.
          headers:
            x-goosy-api:
              description: Present while this API is in beta; the value is `beta`.
              schema:
                type: string
                const: beta
            x-request-id:
              description: >-
                This request's id: the `x-request-id` you sent when it was well
                formed, otherwise one generated for you. Quote it to support; it
                is recorded on every audit row the call wrote.
              schema:
                type: string
            Deprecation:
              description: >-
                RFC 9745: this refusal's nested `error` object is deprecated,
                since this instant (`@<unix seconds>`).
              schema:
                type: string
            Sunset:
              description: >-
                RFC 8594: the nested `error` object may be removed after this
                date (2026-12-31).
              schema:
                type: string
            Link:
              description: The page that says what to read instead (`rel="deprecation"`).
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >-
            No such tool. The body names no tool and is identical for every
            unrecognised name.
          headers:
            x-goosy-api:
              description: Present while this API is in beta; the value is `beta`.
              schema:
                type: string
                const: beta
            x-request-id:
              description: >-
                This request's id: the `x-request-id` you sent when it was well
                formed, otherwise one generated for you. Quote it to support; it
                is recorded on every audit row the call wrote.
              schema:
                type: string
            Deprecation:
              description: >-
                RFC 9745: this refusal's nested `error` object is deprecated,
                since this instant (`@<unix seconds>`).
              schema:
                type: string
            Sunset:
              description: >-
                RFC 8594: the nested `error` object may be removed after this
                date (2026-12-31).
              schema:
                type: string
            Link:
              description: The page that says what to read instead (`rel="deprecation"`).
              schema:
                type: string
            RateLimit-Limit:
              description: Requests this key may make in a rolling minute.
              schema:
                type: integer
                minimum: 1
            RateLimit-Remaining:
              description: Requests left in the current rolling minute.
              schema:
                type: integer
                minimum: 0
            RateLimit-Reset:
              description: Whole seconds until the window frees more requests.
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: >-
            Over this key's per-minute cap. Carries `Retry-After` and
            `error.retry_after_seconds`.
          headers:
            x-goosy-api:
              description: Present while this API is in beta; the value is `beta`.
              schema:
                type: string
                const: beta
            x-request-id:
              description: >-
                This request's id: the `x-request-id` you sent when it was well
                formed, otherwise one generated for you. Quote it to support; it
                is recorded on every audit row the call wrote.
              schema:
                type: string
            Deprecation:
              description: >-
                RFC 9745: this refusal's nested `error` object is deprecated,
                since this instant (`@<unix seconds>`).
              schema:
                type: string
            Sunset:
              description: >-
                RFC 8594: the nested `error` object may be removed after this
                date (2026-12-31).
              schema:
                type: string
            Link:
              description: The page that says what to read instead (`rel="deprecation"`).
              schema:
                type: string
            RateLimit-Limit:
              description: Requests this key may make in a rolling minute.
              schema:
                type: integer
                minimum: 1
            RateLimit-Remaining:
              description: Requests left in the current rolling minute.
              schema:
                type: integer
                minimum: 0
            RateLimit-Reset:
              description: Whole seconds until the window frees more requests.
              schema:
                type: integer
                minimum: 1
            Retry-After:
              description: Whole seconds to wait before retrying.
              schema:
                type: integer
                minimum: 1
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    ToolRefusal:
      type: object
      properties:
        ok:
          type: boolean
          const: false
        code:
          type: string
          description: >-
            The typed reason — e.g. `workspace_ambiguous`, `invalid_arguments`,
            or the tool's own failure code.
        message:
          type: string
          description: A sentence a person can act on.
        workspaces:
          type: array
          items:
            type: string
          description: 'On a workspace refusal: the slugs this key may pass as `workspace`.'
        issues:
          type: array
          items:
            type: object
            properties:
              path:
                type: string
              message:
                type: string
            required:
              - path
              - message
            additionalProperties: false
          description: 'On `invalid_arguments`: which fields failed, and why.'
      required:
        - ok
        - code
        - message
      additionalProperties: false
      description: A call that was admitted and dispatched, and did not succeed.
    Error:
      type: object
      properties:
        ok:
          type: boolean
          const: false
        code:
          type: string
          description: >-
            The typed refusal code — e.g. `tenant_mcp.surface_disabled`,
            `tenant_mcp.rate_limited`.
        message:
          type: string
          description: A sentence a person can act on.
        retry_after_seconds:
          type: number
          description: 'On a 429: whole seconds to wait. Mirrors `Retry-After`.'
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            retry_after_seconds:
              type: number
          required:
            - code
            - message
          additionalProperties: false
          description: >-
            Deprecated: the same values as the top-level fields, kept until
            2026-12-31. Read `code` and `message` at the top level.
      required:
        - ok
        - code
        - message
        - error
      additionalProperties: false
      description: >-
        A request the boundary refused before dispatching anything. The same
        shape the MCP endpoint answers its own refusals with, and the same
        top-level fields a dispatched call's refusal carries, so one client
        branch covers both.
  securitySchemes:
    tenantApiKey:
      type: http
      scheme: bearer
      description: >-
        An API key minted at Settings › API & MCP. Send it as `Authorization:
        Bearer <key>`. A key carries its holder's own permissions, resolved on
        every call — revoking a membership closes the key's reach immediately.
        Keep it in an environment variable (`GOOSY_API_KEY`), never in a
        committed file.

````