Docs

REST API reference

The documented api.plori.ai/v1 surface: create agents, submit and track runs, schedules, credits, and the public rate cards. OpenAPI 3.1 spec included.

Everything the dashboard and the MCP tools do runs over one REST API at https://api.plori.ai/v1. If you are writing code rather than operating an MCP client, call it directly. The machine-readable contract is the OpenAPI 3.1 document at https://plori.ai/openapi.json. This page describes the same surface.

Authentication

Account endpoints take a bearer credential:

Authorization: Bearer plori_sk_...

Either a plori API key (plori_sk_…, minted in the dashboard under Settings → API keys, or by plori login for the CLI) or an OAuth 2.1 access token from the same flow MCP clients use. The full credential walkthrough, including how an agent can create a key non-interactively, is at plori.ai/auth.md.

A request through MCP or authenticated with an OAuth access token is an AI-client request. A direct owner session uses the owner's own plori_sk_ key outside MCP. Only a direct owner session can approve an action, set a write grant, confirm a schedule, activate a workflow, or run a workflow that is not active. AI clients can deny approvals and answer questions. An approval's approve_url opens its card in the web app.

GitHub Actions use a separate workload-identity path: a job mints a GitHub OIDC JWT for audience plori.ai, exchanges it at POST /v1/github/oidc/exchange, and receives a short-lived capability for one repository agent run. That exchange creates no Plori secret or customer account. The claim URL combines the verified repository capability with a normal signed-in payer when the free quota ends or the repository is private.

The main path: create, invoke, read

Create an agent (get-or-create by name, so reruns are safe):

curl -s https://api.plori.ai/v1/agents \
  -H "Authorization: Bearer $PLORI_KEY" \
  -d '{"name": "my-agent", "if_exists": "return"}'

Send it work. A successful submission returns 202 with the run's IDs. The endpoint returns without waiting for the run to complete:

curl -s https://api.plori.ai/v1/agents/$AGENT_ID/runs \
  -H "Authorization: Bearer $PLORI_KEY" \
  -H "Idempotency-Key: hello-1" \
  -d '{"message": "Say hello and tell me what tools you have."}'
# -> {"run_id": "...", "session_id": "..."}

Poll the run until it finishes:

curl -s https://api.plori.ai/v1/agents/$AGENT_ID/runs/$RUN_ID \
  -H "Authorization: Bearer $PLORI_KEY"

The run object carries status and accounting (credits, tokens, stop reason), not the reply text. Read the reply where the live surfaces read it: the session WebSocket (GET /v1/ws, the browser's transport), the CLI (plori result), or the MCP send_workspace_message / get_run_result tools, which can wait for the answer and return it in one call. If you want request/response simplicity, MCP can return the reply in one call. The REST API submits and tracks runs.

Run accounting

The run's monetary integers are in micro-US-dollars, so 1000000 equals $1. credits is the net charge after any automatic refund. It is null when usage attribution is unavailable. gross_micro_usd records consumption before refunds, and refunded_micro_usd records the amount returned. refund_reason records why the run qualified for an automatic refund. refund_withheld_reason records why Plori did not refund the run. The gross and refund fields are absent when their values are zero or empty.

usage_by_role splits gross usage among executor, advisor, and reviewer. Each role includes gross credits, tokens, and the number of model calls. The role credits sum to gross_micro_usd, not the net credits value. This field is absent when usage attribution is unavailable.

The advisor is the stronger model that the Plori Router can consult during a turn. The advisor object reports calls_used, calls_allowed, and spend_micro_usd. Its spend is gross and matches usage_by_role.advisor.credits. spend_cap_micro_usd is present only when the run has a cap. By default, a run has no advisor spend cap. To set a cap for an agent, send advisor_max_spend_micro_usd in PATCH /v1/agents/{agentID}. The value is the maximum advisor spend for each turn. 0 stops advisor calls, and null clears the cap.

Retries, idempotency, and concurrency

A run is nondeterministic and billable, so never retry blindly. Send an Idempotency-Key header (or idempotency_key in the body): a retry with the same key within 24 hours returns the original run (X-Plori-Idempotent-Replay: true) instead of starting a second one. The same key with a different payload returns 422. A retry while the first request is still in flight returns 409 with Retry-After.

Anonymous trials can run two agent runs concurrently across all their agents. A third run returns 429 with Retry-After. Register free to remove this limit. Free, Pro and Power accounts have unlimited concurrent agent runs. Available fleet capacity can delay launch. Insufficient credits returns 402 whose body carries "code": "insufficient_credits".

Endpoints

Method and path What it does
POST /v1/agents Create an agent. if_exists: "return" makes it get-or-create by name
GET /v1/agents List your agents
GET /v1/agents/{agentID} One agent, including its warm/sleeping status
POST /v1/agents/{agentID}/runs Submit a run (202 {run_id, session_id}, Idempotency-Key, max_turn_tokens)
GET /v1/agents/{agentID}/runs Run history
GET /v1/agents/{agentID}/runs/{runID} One run's status and accounting receipt
POST /v1/agents/{agentID}/runs/{runID}/cancel Cancel an in-flight run (cascades to delegated child runs)
POST /v1/agents/{agentID}/schedules Defer a one-shot run (fire_at RFC3339 or delay_seconds)
GET /v1/agents/{agentID}/schedules List schedules
GET /v1/agents/{agentID}/schedules/{scheduleID} One schedule
POST /v1/agents/{agentID}/schedules/{scheduleID}/confirm Confirm a schedule from a direct owner session (200, updated schedule)
DELETE /v1/agents/{agentID}/schedules/{scheduleID} Cancel an unconfirmed, pending, or dispatched schedule (returns the updated schedule)
GET /v1/users/{userID}/credits Balance, plan, and purchase options (your own account only)
POST /v1/github/oidc/exchange Exchange a GitHub Actions OIDC JWT for one repository-scoped run capability
POST /v1/github/claim Attach a verified repository and its existing agent workspace to the signed-in account
GET /v1/github/runs/{runID} Poll one GitHub-triggered run's durable AG-UI events (narrow capability or owning API key)
GET /v1/pricing/models Public model rate card (no auth)
GET /v1/pricing/tools Public tool and workflow rate card (no auth)

Schedules

Schedule creation returns 201 with the schedule. Give a nonempty prompt and exactly one of fire_at (RFC3339) or a positive delay_seconds. session_id optionally selects this agent's thread. If omitted, creation uses the agent's active thread when available.

A direct owner session creates a schedule with status:"pending". An AI client creates one with status:"awaiting_confirmation" and confirm_url. The link opens https://plori.ai/dashboard/agents/<agent>?schedule=<schedule>. For a Workspace schedule, it opens https://plori.ai/workspaces/<workspace>?schedule=<schedule>. The owner reviews the prompt and time, then confirms in the web app. Unconfirmed schedules never fire. Their status remains awaiting_confirmation after the fire time passes, with no expiry sweep.

POST /v1/agents/{agentID}/schedules/{scheduleID}/confirm takes no body. It changes awaiting_confirmation to pending, records confirmed_at, and returns 200 with the schedule. An already confirmed row returns 200 unchanged. An unconfirmed row whose fire time passed returns 409. A row in another state without confirmed_at also returns 409. AI clients receive 403 with the confirmation link in the error text. Confirmation also returns 409 if the owner is deleting the agent.

Schedule responses include status. Possible values are awaiting_confirmation, pending, firing, dispatched, done, cancelled, and failed. confirmed_at is absent when unset. confirm_url appears only while awaiting confirmation. A done schedule does not establish task success. Read run history for the outcome. Confirmation authorizes the deferred run. External writes still require the owner's authorization when the run executes.

The schedule routes return 403 for another account's agent.

An unknown schedule or one outside the addressed agent returns 404 on owner inspection or confirmation. Cancellation returns 200 with the updated schedule, or 404 if the row is no longer cancellable.

Workflow executions and activation

POST /v1/workflows/{id}/activate returns 403 to an AI client (the MCP door or an OAuth access token). POST /v1/workflows/{id}/executions returns 403 to an AI client when the workflow is not active. An active workflow runs the version the owner activated. The error text contains a link, /agent/<agentId>?workflow=<workflowId>. The owner opens the link in the web app, reviews the workflow, and runs or activates it. A direct owner session receives the normal response.

Errors use {"error": "<message>"}. Credit exhaustion (402) also includes code and audience. A 500 always answers {"error": "internal error", "request_id": "<id>"}. Do not parse the message. Include request_id in a report to [email protected] so we can find the failure in our logs.

Other routes remain outside this public contract. If you need one documented, contact [email protected].

Workspace API candidate

The Workspace routes below are candidate endpoints. New work requires an admitted owner. Files routes have a separate gate. Deletion, revocation, adoption cancellation, changeset rejection and consumer cleanup remain available for stored resources after the owner leaves the cohort. These routes do not mean the Workspace release is live on production. Create an independent Agent with POST /v1/agents and kind: "independent". List it with GET /v1/agents?include_independent=true. It has no separate legacy disk and runs as a Workspace member.

Omit default_manager_agent_id when creating a Workspace to create a default manager, or send a UUID to select an owned Agent. Every Workspace has a manager, so the API refuses null with 400 invalid_workspace_manager. Creating a Workspace does not start a run. For an external coordinator, specify executor_agent_id on the Workspace run. A worker also needs a working copy and task group.

A copy operation returns 202 and a Location for /operations/{operationID}. Poll that resource. A completed run does not prove that a saved revision exists. A saved revision is separate from an accepted root. Read the worker save_state and saved_revision_id. Then review and accept the changeset. Acceptance requires expected_current_revision and nonempty validation_evidence. Resolution also requires nonempty validation_evidence. A conflict can return 409 with current_revision_conflict. Cold recovery opens a new client from a ready revision. It does not continue old file descriptors, locks, mmap or an active SQLite WAL session.

Use a nonempty Idempotency-Key header for Workspace creation, storage adoption and retention pins. For workers, task groups, context items, copy operations and changeset mutations, send the header or idempotency_key in JSON. If you send both, the server uses the nonempty header. Send the header when you create an independent Agent. Runs accept an optional retry key. Reuse a key only for the same request. Workspace operations can return 409 while a run holds a copy or storage is adopting. They can return 422 for a reused key or 503 with Retry-After while storage starts. Copy, revision and worker lists use cursor and limit. Retention view defaults to cleanup. Request the next page until next_cursor is empty. Files lists accept cursor. Files read serves text and download serves bytes. For canonical or working-copy Files read, limit=0 requests the maximum read window. Revision Files read returns the whole file and refuses files above the editor cap. Workspace Files download routes return the whole file. Revision read and Workspace download do not accept offset or limit. For Files restore, send the trash handle returned by Files delete as path. Files empty-trash purges all trash in the selected copy. It has no path filter. A write to a working copy still needs a saved revision. Revision Files are read-only.

What should a client do when a Files write has a stale ETag?

Send the ETag from the file read in the If-Match header when writing. A stale Files write returns HTTP 412 with {"error":"etag mismatch", "current_etag":"<etag>"}. The current_etag value is the file's current ETag as a decimal string. A rematerialized copy can have a new ETag even when the file content has not changed. The web editor checks this automatically.

Read the same file again. If its content exactly matches the content you read before editing, retry the write once with current_etag in If-Match. If the content differs or the retry gets another 412, stop and resolve the conflict. Do not overwrite the newer content without a review.

Method and path Resource
GET /v1/workspaces Workspace
POST /v1/workspaces Workspace
GET /v1/workspaces/by-agent/{agentID} Workspace
GET /v1/workspaces/{workspaceID} Workspace
PATCH /v1/workspaces/{workspaceID} Workspace
DELETE /v1/workspaces/{workspaceID} Workspace
POST /v1/workspaces/{workspaceID}/storage/adopt Storage adoption
GET /v1/workspaces/{workspaceID}/storage/adoption Storage adoption
POST /v1/workspaces/{workspaceID}/storage/adoption/cancel Storage adoption
POST /v1/workspaces/{workspaceID}/storage/adoption/rollback Storage adoption
GET /v1/workspaces/{workspaceID}/members Workspace
POST /v1/workspaces/{workspaceID}/members Workspace
DELETE /v1/workspaces/{workspaceID}/members/{memberID} Workspace
GET /v1/workspaces/{workspaceID}/threads Workspace
POST /v1/workspaces/{workspaceID}/threads Workspace
GET /v1/workspaces/{workspaceID}/runs Workspace
POST /v1/workspaces/{workspaceID}/runs Workspace
GET /v1/workspaces/{workspaceID}/costs Workspace
GET /v1/workspaces/{workspaceID}/context-items Workspace
POST /v1/workspaces/{workspaceID}/context-items Workspace
DELETE /v1/workspaces/{workspaceID}/context-items/{contextItemID} Workspace
POST /v1/workspaces/{workspaceID}/checkpoints Copies and revisions
GET /v1/workspaces/{workspaceID}/copies Copies and revisions
POST /v1/workspaces/{workspaceID}/copies Copies and revisions
GET /v1/workspaces/{workspaceID}/copies/{workingCopyID} Copies and revisions
DELETE /v1/workspaces/{workspaceID}/copies/{workingCopyID} Copies and revisions
GET /v1/workspaces/{workspaceID}/revisions Copies and revisions
GET /v1/workspaces/{workspaceID}/revisions/{revisionID} Copies and revisions
DELETE /v1/workspaces/{workspaceID}/revisions/{revisionID} Copies and revisions
GET /v1/workspaces/{workspaceID}/operations/{operationID} Copies and revisions
GET /v1/workspaces/{workspaceID}/retention Retention
POST /v1/workspaces/{workspaceID}/retention/pins Retention
DELETE /v1/workspaces/{workspaceID}/retention/pins/{pinID} Retention
GET /v1/workspaces/{workspaceID}/review-actions/{actionID} Review
GET /v1/workspaces/{workspaceID}/changesets Review
POST /v1/workspaces/{workspaceID}/changesets Review
GET /v1/workspaces/{workspaceID}/changesets/{changesetID} Review
POST /v1/workspaces/{workspaceID}/changesets/{changesetID}/reject Review
POST /v1/workspaces/{workspaceID}/changesets/{changesetID}/accept Review
POST /v1/workspaces/{workspaceID}/changesets/{changesetID}/resolve Review
GET /v1/workspaces/{workspaceID}/revisions/{revisionID}/files/list Revision Files
GET /v1/workspaces/{workspaceID}/revisions/{revisionID}/files/stat Revision Files
GET /v1/workspaces/{workspaceID}/revisions/{revisionID}/files/read Revision Files
GET /v1/workspaces/{workspaceID}/revisions/{revisionID}/files/download Revision Files
GET /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/list Working-copy Files
GET /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/stat Working-copy Files
GET /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/read Working-copy Files
GET /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/download Working-copy Files
PUT /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/write Working-copy Files
POST /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/upload Working-copy Files
POST /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/delete Working-copy Files
POST /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/restore Working-copy Files
POST /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/empty-trash Working-copy Files
POST /v1/workspaces/{workspaceID}/copies/{workingCopyID}/files/download-url Working-copy Files
GET /v1/workspaces/{workspaceID}/files/list Canonical Files
GET /v1/workspaces/{workspaceID}/files/stat Canonical Files
GET /v1/workspaces/{workspaceID}/files/read Canonical Files
GET /v1/workspaces/{workspaceID}/files/download Canonical Files
PUT /v1/workspaces/{workspaceID}/files/write Canonical Files
POST /v1/workspaces/{workspaceID}/files/upload Canonical Files
POST /v1/workspaces/{workspaceID}/files/delete Canonical Files
POST /v1/workspaces/{workspaceID}/files/restore Canonical Files
POST /v1/workspaces/{workspaceID}/files/empty-trash Canonical Files
POST /v1/workspaces/{workspaceID}/files/download-url Canonical Files
GET /v1/workspaces/{workspaceID}/workers Workers
POST /v1/workspaces/{workspaceID}/workers Workers
GET /v1/workspaces/{workspaceID}/workers/{workerRequestID} Workers
GET /v1/workspaces/{workspaceID}/task-groups Task groups
POST /v1/workspaces/{workspaceID}/task-groups Task groups
GET /v1/task-groups/{taskGroupID} Task group
GET /v1/task-groups/{taskGroupID}/costs Task group
POST /v1/task-groups/{taskGroupID}/stop Task group
GET /v1/workspaces/{workspaceID}/site Consumers
PUT /v1/workspaces/{workspaceID}/site Consumers
POST /v1/workspaces/{workspaceID}/site/open Consumers
GET /v1/workspaces/{workspaceID}/inbound-email Consumers
GET /v1/workspaces/{workspaceID}/schedules Consumers
POST /v1/workspaces/{workspaceID}/schedules Consumers
GET /v1/workspaces/{workspaceID}/schedules/{scheduleID} Consumers
DELETE /v1/workspaces/{workspaceID}/schedules/{scheduleID} Consumers
GET /v1/workspaces/{workspaceID}/workflows Consumers
PUT /v1/workspaces/{workspaceID}/workflows/{workflowID} Consumers
DELETE /v1/workspaces/{workspaceID}/workflows/{workflowID} Consumers
POST /v1/agents/{agentID}/stop Agent lifecycle
POST /v1/agents/{agentID}/retire Agent lifecycle

The legacy Agent delete route keeps its destructive behavior for an adopted compatibility Agent. Workspace delete starts erasure. A 202 receipt does not prove physical volume or object cleanup. Stop preserves an Agent identity. Retire blocks new assignment. Retire, member removal and delete refuse the current manager of a Workspace with 409 workspace_manager_in_use and list the Workspaces in workspace_ids. Select another manager for each one first.

How this page stays accurate

CI checks each OpenAPI path and method against the server's registered routes. Pricing numbers live on the rate cards and in the two pricing endpoints.