---
openapi: 3.0.3
info:
  title: Talkable invite program
  version: '1.0'
  description: |-
    The calls that a coding agent makes to add a Talkable invite program to a product, and the call that the backend
    of the product makes at each signup. The agent follows the setup instructions of `/skill.md` on this host.

    Each answer is JSON: `{"ok": true, "result": {...}}` or `{"ok": false, "error_message": "..."}`. The errors of
    the agent calls also have `error_code`, but a 400 answer has none, for example to a body that is not JSON.
externalDocs:
  description: Setup instructions for coding agents
  url: "/skill.md"
servers:
- url: https://www.talkable.com
tags:
- name: Agent sandbox
  description: A coding agent makes a sandbox, and the owner claims it with an emailed
    link.
- name: Invite redemptions
  description: The backend of the product records an invite at signup.
paths:
  "/api/v2/agent/sandboxes":
    post:
      summary: Create a sandbox
      operationId: createSandbox
      tags:
      - Agent sandbox
      description: Needs no key and no body. Make one sandbox for each environment
        of the product, and use it again.
      security: []
      parameters:
      - "$ref": "#/components/parameters/talkable_client"
      responses:
        '201':
          description: The sandbox. Keep the two keys in a file that only your user
            can read.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  result:
                    type: object
                    properties:
                      api_key:
                        type: string
                        description: The sandbox key. It stops at the claim.
                      site_slug:
                        type: string
                        description: The Site ID.
                      private_api_key:
                        type: string
                        description: The backend of the product signs the invite block
                          and redeems invites with it.
                      mcp_url:
                        type: string
                        description: The campaign tools, JSON-RPC over HTTP.
                        example: https://www.talkable.com/mcp
        '429':
          "$ref": "#/components/responses/rate_limited"
        '503':
          "$ref": "#/components/responses/unavailable"
  "/api/v2/agent/sandbox/claim_link":
    post:
      summary: Send a claim link
      operationId: sendClaimLink
      tags:
      - Agent sandbox
      description: |-
        Talkable emails a link to the owner. The link works for 24 hours and one time. A new link stops the earlier
        link. Each sandbox gets 3 links an hour, and each email gets 4.
      security:
      - sandbox_key: []
      parameters:
      - "$ref": "#/components/parameters/talkable_client"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              properties:
                email:
                  type: string
                  format: email
                  description: One plain ASCII address. The person who opens the link
                    gets the program.
                  example: owner@example.com
      responses:
        '200':
          description: The link is on its way.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  result:
                    type: object
                    properties:
                      expires_in:
                        type: integer
                        description: Seconds until the link stops.
                        example: 86400
        '401':
          "$ref": "#/components/responses/unauthorized"
        '422':
          description: "`invalid_email`: the email is not one plain ASCII address."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/agent_error"
        '429':
          "$ref": "#/components/responses/rate_limited"
        '503':
          "$ref": "#/components/responses/unavailable"
  "/api/v2/agent/sandbox/claim":
    post:
      summary: Get the state of the claim
      operationId: getClaimState
      tags:
      - Agent sandbox
      description: |-
        Ask every 10 seconds until the owner presses the button on the page of the link. After `claimed`, the
        sandbox key stops, and the owner key does the later calls. Talkable gives the owner key one time.
      security:
      - sandbox_key: []
      parameters:
      - "$ref": "#/components/parameters/talkable_client"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                owner_key:
                  type: string
                  description: |-
                    For staging and production only: the owner key of the development claim. Then Talkable gives
                    the owner key of this site too, after the move into the same account.
      responses:
        '200':
          description: |-
            `pending`: the owner did not press the button yet. `claimed`: the new owner key, the Site ID and the
            private API key. `moved`: the program moved into the account of the owner, and the agent gets only the
            Site ID.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  result:
                    type: object
                    properties:
                      status:
                        type: string
                        enum:
                        - pending
                        - claimed
                        - moved
                      api_key:
                        type: string
                        description: The owner key. Only with `claimed`.
                      site_slug:
                        type: string
                        description: The Site ID. With `claimed` and `moved`. The
                          claim can change it.
                      private_api_key:
                        type: string
                        description: The private API key of the site. Only with `claimed`.
        '401':
          "$ref": "#/components/responses/unauthorized"
        '422':
          description: "`link_expired`: no claim link is live. Send a new link."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/agent_error"
  "/api/v2/agent/sandbox/launch":
    post:
      summary: Launch the campaign
      operationId: launchCampaign
      tags:
      - Agent sandbox
      description: |-
        Makes the invite-only campaign live. The agent calls it only after the owner says yes. Launch deletes the
        test offers and referrals, and the signups of test friends with QA emails, for example on `example.com`.
      security:
      - owner_key: []
      parameters:
      - "$ref": "#/components/parameters/talkable_client"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - campaign_id
              properties:
                campaign_id:
                  type: integer
                  description: The ID of the invite-only campaign.
                  example: 123
      responses:
        '200':
          description: The campaign is live.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  result:
                    type: object
                    properties:
                      campaign_id:
                        type: integer
                        example: 123
        '401':
          "$ref": "#/components/responses/unauthorized"
        '404':
          description: "`campaign_not_found`: use the ID of the invite-only campaign."
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/agent_error"
        '422':
          description: |-
            - `not_invite_only` and `not_in_test_mode`: launch only the invite-only campaign, and only in test mode.
            - `url_not_production`: set the site URL of this environment. Development can launch on `localhost`, a
              `*.localhost` name or `127.0.0.1`.
            - `no_invite_code`: put the invite code back into the link for friends.
            - `no_test_redemption`: test one invite, then launch again.
            - `campaign_invalid`: send `error_message` to the AI assistant as a fix.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/agent_error"
  "/api/v2/invite_redemptions":
    post:
      summary: Redeem an invite
      operationId: redeemInvite
      tags:
      - Invite redemptions
      description: |-
        The backend of the product calls it at signup, just before it creates the account. Never call it from a
        browser. Until the campaign ends, a repeat of the same code and email answers `ok` and records nothing new.
        But after a data removal of that email, a repeat of a one-use code answers `used_up`.
      security:
      - private_api_key: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - site_slug
              - code
              - email
              properties:
                site_slug:
                  type: string
                  description: The Site ID.
                code:
                  type: string
                  description: The invite code of the friend, from the `invite` parameter
                    of the link or the form.
                  example: Ab3dE9
                email:
                  type: string
                  format: email
                  description: The email of the new account.
                  example: friend@example.com
                ip_address:
                  type: string
                  description: The IP address of the friend. Optional.
                  example: 192.0.2.255
      responses:
        '200':
          description: |-
            `ok`: continue the signup. `invalid` (for example an unknown code, or the advocate's own email) or
            `used_up` (the advocate has no invites left, or another email already used this one-use code): show the
            product's own message.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true
                  result:
                    type: object
                    properties:
                      status:
                        type: string
                        enum:
                        - ok
                        - invalid
                        - used_up
        '400':
          description: |-
            The call does not have the key or a required parameter, or the email or the Site ID is not valid. A call
            from a browser gets this answer too.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/error"
        '401':
          description: The private API key is not valid.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/error"
        '429':
          description: More than 120 calls a minute for the site. Treat it as a failed
            call, and do not wait.
          headers:
            Retry-After:
              schema:
                type: integer
                example: 60
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/error"
components:
  securitySchemes:
    sandbox_key:
      type: http
      scheme: bearer
      description: The `api_key` of `POST /api/v2/agent/sandboxes`. It works until
        the claim.
    owner_key:
      type: http
      scheme: bearer
      description: The `api_key` that the claim gives with the status `claimed`.
    private_api_key:
      type: http
      scheme: bearer
      description: The private API key of the site. Send it only from the backend
        of the product.
  parameters:
    talkable_client:
      name: X-Talkable-Client
      in: header
      description: The name of the coding agent, in lowercase kebab-case.
      schema:
        type: string
        example: claude-code
  schemas:
    error:
      type: object
      properties:
        ok:
          type: boolean
          example: false
        error_message:
          type: string
    agent_error:
      type: object
      properties:
        ok:
          type: boolean
          example: false
        error_code:
          type: string
        error_message:
          type: string
  responses:
    unauthorized:
      description: "`unauthorized`: the key is wrong, or the sandbox has an owner
        already. Use the key of the current step."
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/agent_error"
    rate_limited:
      description: "`rate_limited`: wait for the seconds in `Retry-After`, then try
        again."
      headers:
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/agent_error"
    unavailable:
      description: "`paused` or `rate_limit_unavailable`: try again later."
      headers:
        Retry-After:
          description: Only with `rate_limit_unavailable`.
          schema:
            type: integer
            example: 60
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/agent_error"
