{
  "openapi": "3.1.0",
  "info": {
    "title": "plori REST API",
    "version": "1.0.0",
    "summary": "Create cloud AI agents, send them work, read the results.",
    "description": "The documented REST surface of api.plori.ai: agents, runs, schedules, credits, and the public rate cards. Authenticate with `Authorization: Bearer <plori_sk_ API key>` (minted in the dashboard or via `plori login`) or an OAuth 2.1 access token from the same flow MCP clients use — see https://plori.ai/auth.md. Run OUTPUT is read over the session WebSocket (`GET /v1/ws`) or the MCP/CLI surfaces; `POST .../runs` is submit-only and returns 202. This document lists the supported public endpoints; other paths served by the API are internal and may change without notice.",
    "contact": { "name": "plori", "url": "https://plori.ai", "email": "agent@plori.ai" },
    "termsOfService": "https://plori.ai/terms"
  },
  "servers": [{ "url": "https://api.plori.ai" }],
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/v1/agents": {
      "post": {
        "operationId": "createAgent",
        "summary": "Create an agent (or get-or-create by name)",
        "description": "Creates a cloud agent owned by the authenticated account. With `if_exists: \"return\"` this is get-or-create: if an agent with the same name exists, it is returned (200, `existing: true`) instead of creating a duplicate — prefer this and one long-lived agent. The owner is always the credential's account; a `user_id` in the body is ignored.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": { "type": "string" },
                  "model": { "type": "string", "description": "Model slug; omit for the Plori Router default." },
                  "config": { "type": "object", "description": "Opaque agent config; defaults to {}." },
                  "if_exists": { "type": "string", "enum": ["return", "error"], "description": "\"return\" = get-or-create by name; \"error\" (default) = always create." }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Agent created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Agent" } } } },
          "200": { "description": "Existing agent returned (get-or-create hit).", "content": { "application/json": { "schema": { "allOf": [{ "$ref": "#/components/schemas/Agent" }, { "type": "object", "properties": { "existing": { "type": "boolean" } } }] } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Plan does not allow the requested model tier.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Per-plan agent cap reached. `Retry-After: 60`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "get": {
        "operationId": "listAgents",
        "summary": "List your agents",
        "responses": {
          "200": { "description": "The authenticated account's agents.", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Agent" } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/v1/agents/{agentID}": {
      "get": {
        "operationId": "getAgent",
        "summary": "Get one agent",
        "parameters": [{ "$ref": "#/components/parameters/agentID" }],
        "responses": {
          "200": { "description": "The agent.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Agent" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/v1/agents/{agentID}/runs": {
      "post": {
        "operationId": "invokeAgent",
        "summary": "Send the agent a message (submit a run)",
        "description": "Submits a run and returns 202 immediately; there is no blocking or SSE mode on this endpoint. Read the reply over the session WebSocket (`GET /v1/ws`), poll `GET /v1/agents/{agentID}/runs/{runID}` for status, or use the MCP `invoke_agent` tool, which can wait. A run is nondeterministic and billable — retry only with an `Idempotency-Key`: the same key replays the original run for 24 h (the replay carries `X-Plori-Idempotent-Replay: true`).",
        "parameters": [
          { "$ref": "#/components/parameters/agentID" },
          { "name": "Idempotency-Key", "in": "header", "required": false, "schema": { "type": "string", "maxLength": 255 }, "description": "Client-chosen retry key; wins over the body's `idempotency_key`." }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": { "type": "string", "description": "The user message. Exactly one of `message` or `messages` is required; `messages` wins if both are set." },
                  "messages": { "type": "array", "items": { "$ref": "#/components/schemas/Message" } },
                  "session_id": { "type": "string", "description": "Continue an existing thread; omit to start a new one." },
                  "idempotency_key": { "type": "string", "maxLength": 255 },
                  "max_turn_tokens": { "type": "integer", "minimum": 0, "maximum": 5000000, "description": "Cap for this turn; 0 or omitted = the default budget." }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Run submitted. Headers `X-Plori-Run-Id` and `X-Plori-Session-Id` mirror the body.",
            "content": { "application/json": { "schema": { "type": "object", "required": ["run_id", "session_id"], "properties": { "run_id": { "type": "string" }, "session_id": { "type": "string" } } } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": { "description": "Insufficient credits.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreditExhaustion" } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "Agent or session not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "A request with this Idempotency-Key is still in flight. `Retry-After: 2`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "This Idempotency-Key was already used for a different request.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Plan concurrency cap or cluster capacity. `Retry-After: 10`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "get": {
        "operationId": "listAgentRuns",
        "summary": "List the agent's runs",
        "parameters": [{ "$ref": "#/components/parameters/agentID" }],
        "responses": {
          "200": { "description": "Run history, newest first.", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Run" } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/v1/agents/{agentID}/runs/{runID}": {
      "get": {
        "operationId": "getAgentRun",
        "summary": "Get one run's status",
        "parameters": [{ "$ref": "#/components/parameters/agentID" }, { "$ref": "#/components/parameters/runID" }],
        "responses": {
          "200": { "description": "The run.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Run" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/v1/agents/{agentID}/runs/{runID}/cancel": {
      "post": {
        "operationId": "cancelAgentRun",
        "summary": "Cancel an in-flight run",
        "description": "Requests cancellation; cascades to any child runs the run delegated to.",
        "parameters": [{ "$ref": "#/components/parameters/agentID" }, { "$ref": "#/components/parameters/runID" }],
        "responses": {
          "202": { "description": "Cancellation requested.", "content": { "application/json": { "schema": { "type": "object", "properties": { "runId": { "type": "string" }, "status": { "type": "string", "const": "cancelling" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "description": "Run already finished.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/v1/agents/{agentID}/schedules": {
      "post": {
        "operationId": "createSchedule",
        "summary": "Schedule a deferred one-shot run",
        "parameters": [{ "$ref": "#/components/parameters/agentID" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["prompt"],
                "properties": {
                  "prompt": { "type": "string" },
                  "fire_at": { "type": "string", "format": "date-time", "description": "RFC3339 UTC. Exactly one of `fire_at` or `delay_seconds`." },
                  "delay_seconds": { "type": "integer", "minimum": 0 },
                  "session_id": { "type": "string", "description": "Pin the thread; omit for the agent's active thread." },
                  "metadata": { "type": "object" }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Schedule created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Schedule" } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "get": {
        "operationId": "listSchedules",
        "summary": "List the agent's schedules",
        "parameters": [{ "$ref": "#/components/parameters/agentID" }],
        "responses": {
          "200": { "description": "Schedules.", "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Schedule" } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/v1/agents/{agentID}/schedules/{scheduleID}": {
      "get": {
        "operationId": "getSchedule",
        "summary": "Get one schedule",
        "parameters": [{ "$ref": "#/components/parameters/agentID" }, { "$ref": "#/components/parameters/scheduleID" }],
        "responses": {
          "200": { "description": "The schedule.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Schedule" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "operationId": "cancelSchedule",
        "summary": "Cancel a pending schedule",
        "description": "Returns the updated schedule (200, not 204). A schedule that already fired is a 404 — there is nothing left to cancel.",
        "parameters": [{ "$ref": "#/components/parameters/agentID" }, { "$ref": "#/components/parameters/scheduleID" }],
        "responses": {
          "200": { "description": "Cancelled.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Schedule" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/v1/users/{userID}/credits": {
      "get": {
        "operationId": "getCredits",
        "summary": "Get the account's credit balance and plan",
        "description": "`userID` must be the authenticated account's own id (anything else is 403). Check the balance before taking on long work.",
        "parameters": [{ "name": "userID", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "description": "Balance, plan, and purchase options.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Credits" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/v1/pricing/models": {
      "get": {
        "operationId": "getModelPricing",
        "summary": "Public model rate card",
        "security": [],
        "responses": {
          "200": { "description": "Per-model token rates in credits (peg: 120 credits per USD).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ModelPricing" } } } }
        }
      }
    },
    "/v1/pricing/tools": {
      "get": {
        "operationId": "getToolPricing",
        "summary": "Public tool and workflow rate card",
        "security": [],
        "responses": {
          "200": { "description": "Per-tool credit rates and workflow run pricing.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ToolPricing" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "A plori API key (plori_sk_…) or an OAuth 2.1 access token. How to get either: https://plori.ai/auth.md"
      }
    },
    "parameters": {
      "agentID": { "name": "agentID", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
      "runID": { "name": "runID", "in": "path", "required": true, "schema": { "type": "string" } },
      "scheduleID": { "name": "scheduleID", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
    },
    "responses": {
      "BadRequest": { "description": "Malformed request.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Unauthorized": { "description": "Missing or invalid credential.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "Forbidden": { "description": "The resource belongs to another account.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
      "NotFound": { "description": "No such resource.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string" } }
      },
      "CreditExhaustion": {
        "type": "object",
        "required": ["error", "code", "audience"],
        "properties": {
          "error": { "type": "string" },
          "code": { "type": "string", "const": "insufficient_credits" },
          "audience": { "type": "string", "enum": ["anon", "registered_free", "subscribed"] }
        }
      },
      "Agent": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "user_id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "backend": { "type": "string" },
          "model": { "type": "string" },
          "config": { "type": "object" },
          "entity_disk_id": { "type": "string" },
          "status": { "type": "string", "description": "Warm-session state when known: warming, ready, or sleeping." },
          "created_at": { "type": "string", "format": "date-time" }
        }
      },
      "Message": {
        "type": "object",
        "required": ["role", "content"],
        "properties": {
          "role": { "type": "string" },
          "content": { "type": "string" },
          "tool_call_id": { "type": "string" },
          "parts": { "type": "array", "items": { "$ref": "#/components/schemas/Part" } }
        }
      },
      "Part": {
        "type": "object",
        "properties": {
          "type": { "type": "string" },
          "path": { "type": "string" },
          "mime": { "type": "string" },
          "w": { "type": "integer" },
          "h": { "type": "integer" }
        }
      },
      "Run": {
        "type": "object",
        "description": "Run status and accounting. The run's OUTPUT is not in this DTO — read it over the session WebSocket or the MCP get_run_result tool.",
        "properties": {
          "id": { "type": "string" },
          "agent_id": { "type": "string", "format": "uuid" },
          "thread_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string" },
          "origin": { "type": "string", "enum": ["user", "schedule", "background", "agent", "agent_result"] },
          "parent_run_id": { "type": "string" },
          "started_at": { "type": "string", "format": "date-time" },
          "ended_at": { "type": "string", "format": "date-time" },
          "last_heartbeat_at": { "type": "string", "format": "date-time" },
          "cause": { "type": "string", "enum": ["user_stop", "interrupted", "stalled", "time_limit", "backend_error", "spawn_failed", "agent_deleted"] },
          "retryable": { "type": "boolean" },
          "credits": { "type": ["integer", "null"] },
          "tokens": { "type": ["integer", "null"] },
          "stop_reason": { "type": "string" }
        }
      },
      "Schedule": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "agent_id": { "type": "string", "format": "uuid" },
          "kind": { "type": "string", "const": "once" },
          "status": { "type": "string" },
          "prompt": { "type": "string" },
          "fire_at": { "type": "string", "format": "date-time" },
          "thread_id": { "type": "string" },
          "metadata": { "type": "object" },
          "last_error": { "type": "string" },
          "attempts": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" },
          "last_fired_at": { "type": "string", "format": "date-time" }
        }
      },
      "Credits": {
        "type": "object",
        "properties": {
          "balance": { "type": "integer" },
          "low_credit_threshold": { "type": "integer" },
          "peg_cents": { "type": "integer", "description": "Cents per credit (1: the 120-credits-per-USD peg, stated in whole cents)." },
          "packs": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "usd_cents": { "type": "integer" }, "credits": { "type": "integer" } } } },
          "plans": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "usd_cents": { "type": "integer" }, "monthly_credits": { "type": "integer" } } } },
          "active_plan": {
            "type": ["object", "null"],
            "properties": {
              "sku": { "type": "string" },
              "monthly_credits": { "type": "integer" },
              "status": { "type": "string" },
              "current_period_end": { "type": "string", "format": "date-time" },
              "cancel_at_period_end": { "type": "boolean" }
            }
          },
          "can_manage_billing": { "type": "boolean" },
          "audience": { "type": "string", "enum": ["anon", "registered_free", "subscribed"] }
        }
      },
      "ModelPricing": {
        "type": "object",
        "properties": {
          "credits_per_usd": { "type": "integer" },
          "peg_cents": { "type": "integer" },
          "note": { "type": "string" },
          "model_tiers": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "in_nano_usd_per_tok": { "type": "integer" }, "out_nano_usd_per_tok": { "type": "integer" }, "credits_per_1k_in": { "type": "number" }, "credits_per_1k_out": { "type": "number" }, "free_tier_eligible": { "type": "boolean" } } } },
          "models": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "in_nano_usd_per_tok": { "type": "integer" }, "out_nano_usd_per_tok": { "type": "integer" }, "free_tier_eligible": { "type": "boolean" } } } },
          "free_max_model_out_nano_usd_per_tok": { "type": "integer" }
        }
      },
      "ToolPricing": {
        "type": "object",
        "properties": {
          "peg_cents": { "type": "integer" },
          "tools": { "type": "array", "items": { "type": "object", "properties": { "tool": { "type": "string" }, "mode": { "type": "string", "enum": ["free", "per_call"] }, "credits": { "type": "integer" }, "free_per_month": { "type": "integer" }, "note": { "type": "string" } } } },
          "allowance_reset": { "type": "string" },
          "workflow": { "type": "object", "properties": { "exec_credits": { "type": "integer" }, "included_seconds": { "type": "integer" }, "long_run_credits_per_minute": { "type": "integer" }, "note": { "type": "string" } } }
        }
      }
    }
  }
}
