openapi: 3.0.3
info:
  title: Botonom Public Developer API
  version: "1.0"
  description: |
    Programmatic access to Botonom: manage agents and skills, create contacts,
    calendar events and tasks, and most importantly **send commands to your
    agents** (asynchronous agentic runs with signed webhook callbacks).

    ### Conventions
    - Paths are `<resource>/<action>/` (RPC style). Identifiers are passed as
      query parameters or in the JSON body, never in the path.
    - `agent_id` is always the agent's **uuid** (from `agents/list`).
    - Every response uses the envelope `{status, code, title, data, msg}`;
      failures add `error.code` (machine-readable, the stable contract) and use
      real HTTP status codes.
    - Rate limits: 120 requests/min per key (all endpoints, `X-RateLimit-*`
      headers) plus 10/min for `agents/command`. Breach returns `429
      RATE_LIMITED` with `Retry-After`.
    - `Idempotency-Key` header (max 128 chars) is supported on
      `agents/command` and every create endpoint: an identical retry replays
      the stored response (`Idempotency-Replayed: true`); the same key with a
      different payload returns `409 IDEMPOTENCY_CONFLICT`.
servers:
  - url: https://api.botonom.com/en/api/v1
security:
  - ApiKeyAuth: []
tags:
  - name: agents
  - name: commands
  - name: skills
  - name: contacts
  - name: calendar
  - name: tasks
  - name: usage-billing
  - name: webhooks
  - name: keys
  - name: workspace

paths:
  /agents/list/:
    get:
      tags: [agents]
      summary: List agents
      parameters:
        - { name: status, in: query, schema: { type: string } }
        - { name: search, in: query, schema: { type: string } }
        - { name: page, in: query, schema: { type: integer, default: 1 } }
        - { name: per_page, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /agents/get/:
    get:
      tags: [agents]
      summary: Get agent details
      parameters: [ { $ref: "#/components/parameters/AgentId" } ]
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/Error" }
  /agents/presets/:
    get:
      tags: [agents]
      summary: List hireable agent presets
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /agents/create/:
    post:
      tags: [agents]
      summary: Hire a new agent from a preset
      description: Plan, trial, token-limit and seat gates apply exactly like the dashboard.
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [preset_id]
              properties:
                preset_id: { type: string, description: Preset id or slug from agents/presets }
                name: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/Error" }
  /agents/status/:
    get:
      tags: [agents]
      summary: Live runtime health of an agent
      parameters: [ { $ref: "#/components/parameters/AgentId" } ]
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /agents/start/:
    post:
      tags: [agents]
      summary: Start a stopped agent
      requestBody: { $ref: "#/components/requestBodies/AgentRef" }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /agents/stop/:
    post:
      tags: [agents]
      summary: Stop a running agent
      requestBody: { $ref: "#/components/requestBodies/AgentRef" }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /agents/delete/:
    post:
      tags: [agents]
      summary: Permanently delete an agent
      requestBody: { $ref: "#/components/requestBodies/AgentRef" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/Error" }

  /agents/command/:
    post:
      tags: [commands]
      summary: Send a natural-language command to an agent
      description: |
        The instruction runs **asynchronously** as a headless agentic turn with
        the agent's own persona, rules, installed skills and permissions (with
        `autonomy: full` it may act, e.g. send an email from its own address).
        Returns `202` with a `run_id`. Poll `agents/command_status` or subscribe
        to the `agent.command.completed` webhook. Rate limit: 10/min per key.
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_id, instruction]
              properties:
                agent_id: { type: string, format: uuid }
                instruction: { type: string, maxLength: 8000 }
                data: { type: object, description: Optional structured context appended to the instruction }
                autonomy: { type: string, enum: [full, restricted], default: full }
      responses:
        "202":
          description: Command queued
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CommandRunEnvelope" }
        "400": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
  /agents/command_status/:
    get:
      tags: [commands]
      summary: Poll a command run
      parameters:
        - { name: run_id, in: query, required: true, schema: { type: string, example: cmd_654741d050ff325bb6f4cd50 } }
      responses:
        "200":
          description: Run state
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CommandRunEnvelope" }
        "404": { $ref: "#/components/responses/Error" }
  /agents/commands/:
    get:
      tags: [commands]
      summary: List recent command runs
      parameters:
        - { name: agent_id, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }

  /skills/list/:
    get:
      tags: [skills]
      summary: Marketplace skill catalog
      description: Each item carries the skill code in both `id` and `code`.
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /skills/get/:
    get:
      tags: [skills]
      summary: Skill detail
      parameters:
        - { name: code, in: query, required: true, schema: { type: string, example: meta_ads } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /skills/installed/:
    get:
      tags: [skills]
      summary: Skills installed on an agent
      parameters: [ { $ref: "#/components/parameters/AgentId" } ]
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /skills/install/:
    post:
      tags: [skills]
      summary: Install a skill on an agent
      description: |
        Purchase/plan/OAuth requirements are enforced; expect error codes
        `SKILL_PURCHASE_REQUIRED`, `SKILL_PLAN_REQUIRED`,
        `SKILL_AGENT_PLAN_REQUIRED`, `SKILL_OAUTH_REQUIRED`.
      requestBody: { $ref: "#/components/requestBodies/SkillRef" }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/Error" }
  /skills/remove/:
    post:
      tags: [skills]
      summary: Remove a skill from an agent
      requestBody: { $ref: "#/components/requestBodies/SkillRef" }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }

  /contacts/create/:
    post:
      tags: [contacts]
      summary: Create a contact
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                email: { type: string }
                phone: { type: string }
                kind: { type: string, enum: [individual, company], default: individual }
                label: { type: string }
                note: { type: string }
                tags: { type: string }
                dedupe: { type: integer, enum: [0, 1], description: 1 = resolve to an existing person by email/phone instead of duplicating }
      responses:
        "201": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/Error" }
  /contacts/list/:
    get:
      tags: [contacts]
      summary: Search contacts
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: kind, in: query, schema: { type: string } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /contacts/get/:
    get:
      tags: [contacts]
      summary: Get one contact
      parameters:
        - { name: id, in: query, required: true, schema: { type: integer } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/Error" }

  /calendar/event_create/:
    post:
      tags: [calendar]
      summary: Create a company calendar event
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, start_time]
              properties:
                title: { type: string }
                start_time: { type: string, description: ISO-8601 WITH timezone offset, example: "2026-07-15T10:00:00+03:00" }
                end_time: { type: string }
                description: { type: string }
                location: { type: string }
                meeting_url: { type: string }
                agent_id: { type: string, format: uuid }
                contact_id: { type: integer }
                timezone: { type: string, example: Europe/Istanbul }
                priority: { type: string, enum: [low, normal, high, critical] }
                is_all_day: { type: integer, enum: [0, 1] }
                remind_minutes_before: { type: integer }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/Error" }
  /calendar/events/:
    get:
      tags: [calendar]
      summary: List calendar events
      parameters:
        - { name: date_from, in: query, schema: { type: string, format: date } }
        - { name: date_to, in: query, schema: { type: string, format: date } }
        - { name: agent_id, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 500 } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /calendar/event_delete/:
    post:
      tags: [calendar]
      summary: Delete a calendar event
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [id], properties: { id: { type: integer } } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }

  /tasks/create/:
    post:
      tags: [tasks]
      summary: Create a planned task (recurring or one-off)
      description: Provide exactly one of `cron_expression` (recurring) or `run_at` (one-off, future ISO-8601 with offset).
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [agent_id, title, instruction]
              properties:
                agent_id: { type: string, format: uuid }
                title: { type: string }
                instruction: { type: string }
                cron_expression: { type: string, example: "0 9 * * 1" }
                run_at: { type: string, example: "2026-07-15T09:00:00+03:00" }
                description: { type: string }
                timezone: { type: string, default: UTC }
                autonomy: { type: string, enum: [restricted, full], default: restricted }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "201": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/Error" }
  /tasks/list/:
    get:
      tags: [tasks]
      summary: List planned tasks
      parameters:
        - { name: agent_id, in: query, schema: { type: string } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /tasks/runs/:
    get:
      tags: [tasks]
      summary: Task execution history
      parameters:
        - { name: agent_id, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /tasks/delete/:
    post:
      tags: [tasks]
      summary: Delete a planned task
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [task_id], properties: { task_id: { type: integer } } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/Error" }

  /usage/tokens/:
    get:
      tags: [usage-billing]
      summary: Token usage (daily series + per-agent + plan context)
      parameters:
        - { name: days, in: query, schema: { type: integer, default: 30, maximum: 365 } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /billing/get/:
    get:
      tags: [usage-billing]
      summary: Active plan + recent invoices (owner/admin)
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /billing/invoices/:
    get:
      tags: [usage-billing]
      summary: Invoice list (owner/admin)
      parameters:
        - { name: limit, in: query, schema: { type: integer, default: 20 } }
      responses: { "200": { $ref: "#/components/responses/Envelope" } }

  /webhooks/list/:
    get:
      tags: [webhooks]
      summary: List outbound webhook subscriptions + available events
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /webhooks/create/:
    post:
      tags: [webhooks]
      summary: Subscribe an https endpoint to events
      description: |
        The signing secret (`whsec_...`) is returned ONCE. Deliveries are signed
        `X-Botonom-Signature: t=<unix>,v1=HMAC_SHA256(secret, "<t>.<rawBody>")`.
        Failed deliveries retry on a 2m/10m/30m/2h ladder (max 5 attempts);
        20 consecutive failures auto-disable the subscription.
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri, description: Must be https }
                events:
                  type: array
                  items: { type: string, enum: [agent.command.completed, agent.command.failed] }
      responses:
        "201": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/Error" }
      callbacks:
        event:
          "{$request.body#/url}":
            post:
              summary: Signed event delivery
              requestBody:
                content:
                  application/json:
                    schema: { $ref: "#/components/schemas/WebhookEvent" }
              responses:
                "200": { description: Return any 2xx to acknowledge }
  /webhooks/delete/:
    post:
      tags: [webhooks]
      summary: Delete a subscription
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [id], properties: { id: { type: string, example: wh_61ba0fadbd409322 } } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/Error" }

  /keys/list/:
    get:
      tags: [keys]
      summary: List API keys (masked)
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /keys/create/:
    post:
      tags: [keys]
      summary: Create an API key (full key returned once)
      parameters: [ { $ref: "#/components/parameters/IdempotencyKey" } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [name], properties: { name: { type: string } } }
      responses:
        "201": { $ref: "#/components/responses/Envelope" }
  /keys/revoke/:
    post:
      tags: [keys]
      summary: Revoke an API key
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, required: [id], properties: { id: { type: integer } } }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "404": { $ref: "#/components/responses/Error" }

  /workspace/get/:
    get:
      tags: [workspace]
      summary: Workspace profile
      responses: { "200": { $ref: "#/components/responses/Envelope" } }
  /workspace/update/:
    post:
      tags: [workspace]
      summary: Update workspace profile (owner/admin)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                website: { type: string }
                email: { type: string }
                phone: { type: string }
      responses:
        "200": { $ref: "#/components/responses/Envelope" }
        "400": { $ref: "#/components/responses/Error" }

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Botonom-Api-Key
      description: bk_live_ key from Dashboard > Developers > Credentials. `Authorization Bearer bk_live_...` is also accepted.
  parameters:
    AgentId:
      name: agent_id
      in: query
      required: true
      schema: { type: string, format: uuid }
      description: The agent's uuid from agents/list
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema: { type: string, maxLength: 128 }
      description: Identical retry replays the stored response; same key + different payload = 409.
  requestBodies:
    AgentRef:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [agent_id]
            properties:
              agent_id: { type: string, format: uuid }
    SkillRef:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [agent_id, skill_code]
            properties:
              agent_id: { type: string, format: uuid }
              skill_code: { type: string, example: email }
  schemas:
    Envelope:
      type: object
      properties:
        status: { type: boolean }
        code: { type: integer, description: Mirrors the HTTP status }
        title: { type: string }
        data: { type: object }
        msg: { type: string }
    ErrorEnvelope:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            error:
              type: object
              properties:
                code:
                  type: string
                  description: >
                    Stable machine-readable code. Common values:
                    API_KEY_REQUIRED, API_KEY_INVALID, KEY_COMPANY_MISMATCH,
                    ROLE_REQUIRED, METHOD_NOT_ALLOWED, VALIDATION_ERROR,
                    AGENT_NOT_FOUND, RUN_NOT_FOUND, NOT_FOUND, RATE_LIMITED,
                    IDEMPOTENCY_CONFLICT, IDEMPOTENCY_IN_PROGRESS,
                    SKILL_NOT_FOUND, SKILL_PURCHASE_REQUIRED, SKILL_PLAN_REQUIRED,
                    SKILL_AGENT_PLAN_REQUIRED, SKILL_OAUTH_REQUIRED,
                    TOKEN_LIMIT_REACHED, TRIAL_ENDED, PLAN_REQUIRED, OPERATION_FAILED
    CommandRun:
      type: object
      properties:
        run_id: { type: string, example: cmd_654741d050ff325bb6f4cd50 }
        agent_id: { type: string, format: uuid }
        status: { type: string, enum: [queued, completed, failed] }
        instruction: { type: string }
        data: { type: object, nullable: true }
        autonomy: { type: string, enum: [full, restricted] }
        result_text: { type: string, nullable: true }
        error_code: { type: string, nullable: true }
        created_at: { type: string }
        completed_at: { type: string, nullable: true }
    CommandRunEnvelope:
      allOf:
        - $ref: "#/components/schemas/Envelope"
        - type: object
          properties:
            data:
              type: object
              properties:
                run: { $ref: "#/components/schemas/CommandRun" }
    WebhookEvent:
      type: object
      properties:
        id: { type: string, example: evt_a1b2c3d4e5f60718 }
        event: { type: string, enum: [agent.command.completed, agent.command.failed] }
        created_at: { type: string, format: date-time }
        data:
          type: object
          properties:
            run_id: { type: string }
            agent_id: { type: string, format: uuid }
            agent_name: { type: string }
            status: { type: string, enum: [completed, failed] }
            instruction: { type: string }
            result_text: { type: string, nullable: true }
            created_at: { type: string }
  responses:
    Envelope:
      description: Success envelope
      headers:
        X-RateLimit-Limit: { schema: { type: integer } }
        X-RateLimit-Remaining: { schema: { type: integer } }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Envelope" }
    Error:
      description: Error envelope (real HTTP status + error.code)
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
