Plith Rigor — API Reference

Production-quality deliverables from a single API call. Rigor classifies your task, builds a methodology-driven plan, and executes each step — returning a structured deliverable with an independent quality review.

Base URL: https://plith.ai

Platform endpoints (authentication, billing, workspaces, notifications, GDPR) are documented in EXTERNAL-PLATFORM-API-REFERENCE.md.

  • Window: full history — the response shapes below were diffed against live production responses on 2026-08-20; no claim here is a rate over a period
  • Self-review: single session, 2026-08-20 (WS-5). Verified and written by the same session, which is a material limitation — see the verification note below.

Verification note (2026-08-20). The response blocks for GET /workflows, GET /templates, GET /connections, GET /status/{id}, GET /lineage/{id}, GET /task-types, GET /costs, and GET /usage-summary were diffed field by field against live responses from https://plith.ai, at 0 credits. Corrections are marked inline where they were made. The write endpoints were not exercisedPOST /plan, /execute, /approve, /resume, /step, /enhance, /workflows/actions, and every connections mutation are documented from source, not from a live call, and their response blocks carry the same risk of drift that the read endpoints turned out to carry. Treat them as VERIFY.

Verification note (2026-08-29). The q parameter and the task_excerpt field on GET /api/rigor/workflows were corrected on this date after the documented behaviour was diffed against the handler (lib/rigor/workflows.ts) and against plith-prod. Unlike the 2026-08-20 corrections, these were not diffed against a live HTTP response — the session that made them had no network egress to the API — so they are documented from source and from the database, and the task_excerpt line in the response block below is written, not observed. Treat both as VERIFY until one live call confirms them.


Table of Contents


Authentication

All API requests require an API key sent via the x-api-key header:

x-api-key: plth_your_key_here

Do NOT use the Authorization: Bearer format — that is not supported for API authentication.

Keys are created at signup or via POST /api/keys (see Platform reference). Your key identifies your organization — all workflows, billing, and data are scoped to it.

Anonymous mode (MCP only)

When connecting via MCP (Smithery, Claude Desktop, Cursor, etc.) without an API key, rigor_plan is available for evaluation against a demo org. Anonymous calls are rate-limited to 30 requests/minute and 500 requests/day per IP.


Quick Start

1. Get a key

curl -X POST https://plith.ai/api/keys -H "Content-Type: application/json" -d "{\"email\": \"you@example.com\", \"name\": \"My Org\"}"

Response (store the key — shown once):

{
  "org_id": "a1b2c3d4-...",
  "key": "plth_live_xxxxxxxxxxxx...",
  "prefix": "plth_live_xxx",
  "message": "Store this key securely — it will not be shown again."
}

2. Plan a workflow (free)

curl -X POST https://plith.ai/api/rigor/plan -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"task_description\": \"Design a caching layer for our API gateway\"}"

Returns a workflow_id and step sequence. No credits used.

3. Execute

curl -X POST https://plith.ai/api/rigor/execute -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"workflow_id\": \"wr_2a4b6c8d...\", \"delivery\": {\"method\": \"polling\"}}"

4. Get results

curl https://plith.ai/api/rigor/status/wr_2a4b6c8d... -H "x-api-key: YOUR_KEY"

Returns progress during execution and the full deliverable when complete.


Credits & Billing

Plith is metered in credits at 1,000 credits per USD.

Credit packs

Pack Price Credits
10 $10 10,000
25 $25 25,000
100 $100 100,000
250 $250 250,000
Custom $5–$10,000 1,000 credits / dollar

Purchase via POST /api/billing/checkout. See Platform reference.

How credits work

Credits are deducted after each successful call. The amount deducted is reported in the response as credits_used; your new balance as credits_remaining.

For Rigor workflows, credits accumulate server-side as steps execute. The pre-execution estimate appears in plan.cost; final usage is reported by GET /api/rigor/status as credits_used.


Rate Limits

Every limit below is scoped to your organization, not to the API key that made the call. Two keys in the same org share one budget.

Scope Limit
Rigor endpoints (per org, sliding 60 s window) 120 requests / minute. Enterprise: 500
/api/rigor/execute (per org, UTC day) 1,000 requests / day. Enterprise: 100,000
Concurrent active workflows per org 1 free, 3 Pro, 10 Team, unlimited Enterprise
Anonymous MCP (per IP) 30 requests / minute, 500 / day
POST /api/keys (per IP) 3 per 24 hours

Corrected 2026-08-20. Two rows of the previous table did not match the code:

  • Concurrency was given as a flat 10 for every org. It is tier-scaled (lib/tier-limits.ts), and the authoritative value for your org is concurrent_summary.limit on any GET /workflows response.
  • /api/rigor/workflows/actions was given a separate 30/minute limit. It has none. It calls the same shared 120/minute per-org window as every other limited endpoint.

Endpoints with no rate limit at all: the 4 /api/rigor/connections routes, GET /api/rigor/templates, and GET /api/rigor/task-types. Do not read the 120/minute figure as covering the whole surface.

Which endpoints the limits apply to: the per-minute window is checked by plan, execute, status, resume, approve, step, lineage, workflows, workflows/actions, enhance, costs, usage-summary, and vfv/artifacts. The daily and concurrency limits are checked by execute only.

Active workflows (counting toward the concurrent limit): queued, executing, step_executing, resuming, quality_review, interactive_waiting, pending_approval, pending_human_review.

When a rate limit is exceeded, the response includes retry_after (seconds) and a Retry-After HTTP header.

Rate limiting degrades open. The per-minute and daily checks allow the request through if Redis is unavailable, and the concurrency check allows it through on a database error. Limits are a cost and fairness control, not a safety control — do not build a client that relies on receiving a 429.


Roles

Your API key carries a workspace role, and every Rigor endpoint has a minimum. A key below the minimum gets 403 insufficient_permissions; the message names the role required.

Minimum role Endpoints
none — no key required GET /templates
any valid key, no role floor GET /task-types — inside the auth matcher, so an unauthenticated call returns 401, but no role is checked
viewer POST /plan, GET /status/{id}, GET /workflows, GET /lineage/{id}, GET /connections
member POST /execute, POST /resume, POST /step, POST /connections/{id}/test
admin POST /approve, POST /workflows/actions, POST /connections, PATCH /connections/{id}, DELETE /connections/{id}

A key created at signup through POST /api/keys is an owner and passes every gate. A key created through POST /api/workspaces/{id}/api-keys defaults to member and can be requested as member, admin, or owner, never above the role of whoever creates it. See the Platform reference.

If you are issuing a key to an automated caller, member is the right default. It can plan, execute, resume, and drive interactive steps, and it cannot approve a gated workflow, cancel work, or alter your MCP connections.


Rigor Endpoints

Workflow Lifecycle

POST /api/rigor/plan          → workflow_id + step sequence (free)
POST /api/rigor/execute       → triggers execution (SSE / polling / webhook)
GET  /api/rigor/status/{id}   → progress + deliverable when done
POST /api/rigor/approve       → approve/reject pending_approval workflows
POST /api/rigor/resume        → continue failed/stalled workflows
POST /api/rigor/step          → drive interactive (Mode C) workflows one step at a time

POST /api/rigor/plan

Auth: Required (x-api-key) Cost: Free — no credits deducted.

Classifies the task and builds a workflow plan without executing it. The plan is persisted so a subsequent /execute call can pass the workflow_id to skip re-classification.

Request body:

Field Type Required Description
task_description string (≥ 2 words) Yes What you want produced. Be specific.
task_type string No Hint to skip auto-classification.
available_mcp_connections object[] No MCP connection metadata for source-map generation. Each: { id, label, server_url, capabilities }.
preferences object No See preferences fields below.
context object No { additional_context: string } — free-form context the planner uses. Persisted with the plan, so a later POST /api/rigor/execute?workflow_id= call inherits it automatically — see context persistence in the execute section.

preferences fields:

Field Type Description
rigor_level "quick" | "standard" | "thorough" Review depth. Default auto-detected from task type.
tool_scope "mandatory" | "recommended" | "all" Which tool tiers to include.
model_preference "quality_first" | "cost_optimized" | "balanced" LLM model selection bias.
max_budget_usd number Cost target, not a ceiling — nothing is refused. Rigor fits the plan to it where it can, then warns if it still cannot. See below.
skip_frameworks string[] Framework display names to exclude from the plan.
only_frameworks string[] Restrict the plan to only these frameworks (mutually exclusive with skip_frameworks).
add_frameworks (string | { name, before_step })[] Inject additional frameworks into the sequence.
require_approval boolean Pause at pending_approval before final steps.
approval_before_step number[] Zero-based step indices at which to insert an approval gate.
execution string Set to 'direct' to compose the plan's content frameworks into a single LLM call and route cost-first. Research, process steps, and the quality review remain separate calls — see Direct execution. When omitted, each step runs as a separate call with intermediate outputs available via SSE and the status endpoint. Available at every tier; subject to workflow size limits.
output_contract object Only read when execution is 'direct'. Declares the JSON shape you want back, so the answer is generated against your schema instead of composed as a prose document you have to parse. See output_contract below.

Unknown top-level body fields are detected and surfaced as unknown_field warnings in the response rather than silently ignored.

Response (200):

{
  "ok": true,
  "plan": { },
  "generated_title": "Caching Layer Design — API Gateway"
}

max_budget_usd is a target, not a limit. This was previously documented as a "budget ceiling", which overstated it. What actually happens, in lib/rigor/planner.ts:2275-2427:

  1. If the plan's estimate is above max_budget_usd, Rigor downgrades tier-1 models to tier-2 and re-costs. If that brings it under, you get a budget_below_estimate warning recording the reduction.
  2. If it does not, Rigor also drops every step marked tool_scope: optional and re-costs.
  3. If it is still above, you get a second warning — WARNING: plan cost $X still exceeds max_budget_usd ($Y) after all optimisationsand the plan is returned and remains executable. POST /api/rigor/execute does not read max_budget_usd and will not refuse the workflow.

Two further limits on what it can promise, both of which apply even when the plan does fit:

  • It is applied to the estimate, not to actual spend. Actual credits charged are what execution consumes and can differ from the estimate.
  • It is a per-request preference with no memory. It caps nothing across a sequence of calls.

If you need spend that is actually refused, use the workspace monthly credit budget (Team and Enterprise). When a workflow's estimate would take a workspace past its budget, POST /api/rigor/execute returns 402 workspace_budget_exceeded with budget, used, estimated, and remaining, and nothing runs. Set it via the workspace settings API in the Platform reference. A hard org-level stop also exists implicitly: a workflow whose estimate exceeds your credit balance is refused for insufficient credits.

Whether max_budget_usd should refuse is an open product question, filed as B.1272. This document records what it does.

output_contract

Direct execution's default answer is a prose document. If your consumer is a program rather than a person, declare the shape you want and Rigor asks the vendor for it at generation time — Anthropic forced tool-use, or an OpenAI-compatible json_schema response format — then validates whatever comes back against the same declaration before returning it.

Supply it under preferences, together with execution: 'direct'. It persists with the plan, so a later POST /api/rigor/execute against the stored workflow_id inherits it.

Field Type Required Description
task_type string Yes Your own label for the work. Echoed into telemetry. It is not read as a framework name and does not change routing.
shape "object" | "array" Yes "object" for 1 record, "array" for 1 entry per input item.
fields object[] Yes The schema for 1 record. See the field table below.
count object No { min, max } bounds on the number of entries. Only read when shape is "array".
selection object No { scoreField, minScore, minCount, maxCount }. Use when you want the model to over-generate candidates and Rigor to sort, threshold, and cap them before the count bounds are checked.

fields entries:

Field Type Description
name string The JSON key.
type "string" | "number" | "integer" | "boolean" | "array" | "object" The value's type.
required boolean Defaults to true. Set false for a field that may be absent.
enum string[] Restricts a string field to a fixed set of values.
minimum, maximum number Bounds for a numeric field.
items object The element schema. Only read when type is "array".
properties object[] The nested field schema. Only read when type is "object".
description string Passed to the model as the field's description.

The schema is closed. A record carrying a key you did not declare is rejected, exactly as a record missing a required key is. If you want a field back, declare it — with required: false when it is optional. This is what stops a model returning a confident value for a field nobody asked for.

Example — 1 record per invoice, from a batch of supplied documents:

{
  "task_description": "Extract the header fields from each supplied invoice.",
  "preferences": {
    "execution": "direct",
    "output_contract": {
      "task_type": "invoice_header",
      "shape": "array",
      "count": { "min": 1, "max": 50 },
      "fields": [
        { "name": "invoiceNumber", "type": "string" },
        { "name": "issueDate", "type": "string", "description": "ISO 8601 date" },
        { "name": "totalAmount", "type": "number", "minimum": 0 },
        { "name": "currency", "type": "string", "enum": ["EUR", "GBP", "USD"] },
        { "name": "purchaseOrder", "type": "string", "required": false }
      ]
    }
  }
}

What you get back. The validated records are returned on the step's output_findings, and the step's extras carry output_validation_status — one of valid, valid_after_normalization, invalid_schema, invalid_count, invalid_shape, unparseable, or empty — plus output_validation_violations listing every violation by field path when there are any. A step whose result does not satisfy the contract is reported as failed rather than returned as though it had succeeded.

What it saves. Declaring the contract replaces direct execution's quality-review call with deterministic validation, so a conforming run costs 1 LLM call rather than 2.

When you omit it. You get today's behaviour: a prose document, and the parsing is yours. Nothing refuses the call. Note that a document cannot be scored against a schema, so an omitted contract also means the run contributes no schema-conformance evidence, which is what lets Rigor route later calls of the same shape to a cheaper model.

Plan response shape

{
  "workflow_id": "wr_2a4b6c8d...",
  "classification": {
    "task_type": "solution_design",
    "value_class": "operational"
  },
  "sequence": [
    {
      "step": 1,
      "name": "Requirements Analysis",
      "estimated_credits": 80,
      "priority_class": "must"
    },
    {
      "step": 2,
      "name": "Solution Design",
      "estimated_credits": 240,
      "priority_class": "must"
    },
    {
      "step": 3,
      "name": "Assumption Verification",
      "estimated_credits": 100,
      "priority_class": "should",
      "auto_added": true
    },
    {
      "step": 4,
      "name": "Multi-Perspective Review",
      "estimated_credits": 120,
      "priority_class": "must"
    }
  ],
  "cost": {
    "credits": 540,
    "usd": 0.54,
    "range": {
      "estimated_min": 400,
      "estimated_max": 680
    }
  },
  "alternatives": {
    "quick": {
      "steps": 3,
      "cost": {
        "credits": 260,
        "range": { "estimated_min": 190, "estimated_max": 330 }
      },
      "skips": ["Assumption Verification"],
      "review_model": "Haiku",
      "includes_verification": false,
      "tradeoff": "Fewer steps, skips verification."
    },
    "thorough": {
      "steps": 6,
      "cost": {
        "credits": 860,
        "range": { "estimated_min": 640, "estimated_max": 1080 }
      },
      "adds": ["Gap Analysis"],
      "review_model": "Sonnet + Opus",
      "includes_verification": true,
      "tradeoff": "More verification steps and multi-model review."
    }
  },
  "context": {
    "connections": [
      { "name": "GitHub", "type": "github", "status": "active" }
    ],
    "referenced_files": {
      "found": ["src/api/gateway.ts"],
      "not_found": []
    },
    "sufficient": true
  },
  "info": ["Review: Haiku + Sonnet"],
  "warnings": [],
  "rigor_level": "standard"
}

Field reference — sequence steps:

Field Always present Description
step Yes 1-based step number.
name Yes Display name. Review steps are "Multi-Perspective Review". When a plan has multiple review passes, each is qualified by tier: "Multi-Perspective Review (Lightweight)", "Multi-Perspective Review (Standard)", "Multi-Perspective Review (Full)".
estimated_credits Yes Per-step credit estimate.
priority_class When set MoSCoW priority: "must", "should", "could".
auto_added When true Step was auto-inserted by prerequisite resolution.

Field reference — cost:

Field Description
credits Primary estimate — adaptive when historical data exists, static otherwise.
usd credits × 0.001.
range.estimated_min / range.estimated_max Confidence range when adaptive data exists. Absent for task types with no execution history.

Field reference — alternatives:

The selected tier is removed. The remaining tiers each contain:

Field Description
steps Total step count for this tier.
cost.credits Estimated credits.
cost.range Confidence range when adaptive data exists.
skips Display names of frameworks in the current plan that this tier would omit.
adds Display names of frameworks this tier would add beyond the current plan.
review_model LLM(s) used for quality review: "Haiku", "Haiku + Sonnet", or "Sonnet + Opus".
includes_verification Whether this tier includes Assumption Verification or Dependency Verification.
tradeoff Human-readable tradeoff sentence.

Field reference — warnings:

Structured actionable warnings. Each entry: { code, message, suggestion }.

Warning code Trigger
unknown_field Caller sent an unrecognized top-level body field.
budget_below_estimate The plan's cost estimate is above preferences.max_budget_usd. Advisory. Execution is not blocked.
context_insufficient An MCP connection returned degraded context.
connection_error An MCP connection failed to respond.
no_coding_agent No coding-agent MCP server is connected; a prompt-protocol step was substituted.
direct_execution_not_eligible This request carries attachments or prior_workflow_id, which direct execution cannot apply, so it ran as standard execution instead. The context was applied. 1 warning per reason.
mcp_cost_optimization Claude Code is connected without GitHub MCP — broad codebase scanning will incur extra token cost.
file_not_found A file referenced in context.additional_context was not found in the connected repository.

Field reference — execution mode:

Field Description
execution "direct" when direct execution was requested and the workflow is eligible. Absent otherwise.
execution_fallback true when the requested execution approach was overridden. This occurs when: (1) 'direct' was requested but unavailable due to server configuration, or (2) 'direct' was requested alongside require_approval: true or interactive mode, which require intermediate steps that direct execution cannot provide. Absent when no fallback occurred.
allowed_modes Always present. Contains "direct" when the org and this workflow size are eligible for direct execution; empty array otherwise.

Synthesis step: Rigor automatically appends a terminal Synthesis step to every workflow. It is always the last entry in sequence and carries priority_class: "must". Its estimated cost (530 credits) is included in cost.credits.

Multi-intent response

When the task description contains multiple independent intents that cannot share a single execution context, Rigor splits them into separate workflow plans. The plan field returns a MultiWorkflowPlan instead of a single WorkflowPlan:

{
  "ok": true,
  "plan": {
    "plans": [
      { },
      { }
    ],
    "total_cost": { "credits": 1200, "usd": 1.20 },
    "intent_groups": [
      {
        "workflow_id": "wr_aaa...",
        "intents": [
          { "task_type": "solution_design", "confidence": 0.92, "source_phrase": "design a caching layer" }
        ],
        "primary_task_type": "solution_design",
        "reason": "single_intent"
      },
      {
        "workflow_id": "wr_bbb...",
        "intents": [
          { "task_type": "code_review", "confidence": 0.88, "source_phrase": "review the existing auth module" }
        ],
        "primary_task_type": "code_review",
        "reason": "single_intent"
      }
    ],
    "info": ["Task split into 2 separate workflows for independent execution."],
    "warnings": []
  }
}

To distinguish: a single-plan response has a workflow_id at the top of plan; a multi-plan response has plans[] with no top-level workflow_id.

Errors:

  • 401 unauthorized
  • 400 invalid_body
  • 400 missing_fieldtask_description required.
  • 400 invalid_task_description — empty or single word.
  • 400 vague_task_description — classifier confidence too low; details.classification_confidence, details.classified_as.
  • 429 rate_limit_exceeded — 120 req/min.
  • 504 plan_timeout — planning exceeded 25 s; simplify or split the task description.
  • 500 plan_error

curl (Windows):

curl -X POST https://plith.ai/api/rigor/plan -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"task_description\": \"Design a caching layer for our API gateway with Redis integration\"}"

POST /api/rigor/execute

Auth: Required (x-api-key) Cost: Variable — equals the plan's cost.credits.

Executes a workflow end-to-end. Two entry points:

  1. With workflow_id — re-uses a stored plan from a prior /plan call. Cannot be called on a workflow that already has progress (use /resume instead).
  2. Without workflow_id — pass task_description and Rigor will classify, plan, persist, and dispatch in a single call.

Three delivery modes: sse (default), polling, webhook. Interactive mode (mode: "interactive") returns interactive_waiting and is driven step-by-step via /api/rigor/step.

Non-interactive callers should set delivery: { method: "polling" } explicitly. The default is sse, which returns a stream. A client that reads the whole body before parsing — await res.json(), most non-streaming HTTP libraries, most agent tool-call runtimes — will block on that stream until the workflow ends or the 290 s timeout fires. Polling returns a small JSON body immediately with a poll_url. If you cannot change the delivery mode, read X-Plith-Workflow-Id from the response headers, which arrive before the stream body.

Request body:

Field Type Required Description
workflow_id string Conditional If supplied, executes a stored plan. If omitted, task_description is required.
task_description string (≥ 2 words) Conditional Required when workflow_id is not supplied.
task_type string No Classification hint.
preferences object No Same shape as /plan.
context object No { additional_context: string }
delivery object No { method: "sse" | "polling" | "webhook", webhook_url?: string }. Default "sse".
mode string No "interactive" for Mode C; otherwise "full".
language string No BCP 47 language code (e.g. "fr", "de", "ja"). Overrides org default.
prior_workflow_id string No Chain a new workflow off a completed prior workflow.
context_fields string[] No Fields to inherit from prior_workflow_id.
refresh_connections boolean No Force a re-fetch of MCP connection state before planning.
agent_id string No Free-text attribution label for the calling agent. Recorded on the workflow and copied onto its cost rows. Truncated to 100 characters without warning, not validated against any list, and not used for routing or authorization. See below.
workflow_ids string[] No Pre-planned workflow IDs for multi-intent execution without re-planning.

Webhook delivery requires delivery.webhook_url to be a valid HTTPS URL.

On agent_id. This document previously described the field as being for "audit / routing". Routing is wrong — nothing in Rigor reads agent_id to make a decision. It is written to plith_rigor_workflows.agent_id, copied onto the workflow's cost rows, and echoed back to internal-mode callers. It is an analytics label you control, and because you control it, it is not evidence of who called: any caller holding the key can send any value.

Three properties that will surprise a client:

  • Truncation is silent. A value over 100 characters is cut to 100 and stored, with no warning and no error. The same field on /api/inference/completions truncates at 200, so the same identifier can be stored at 2 different lengths on 2 Plith surfaces.
  • There is no vocabulary. Any string is accepted. If you need consistent grouping, enforce the format on your side.
  • It is not the environment. Staging and production are distinguished by your key's prefix (plth_stg_), which Rigor resolves itself. Do not encode an environment in agent_id and expect Rigor to read it.

If you need attribution you cannot forge or forget, use separate API keys. The key id is resolved server-side, recorded on every call, and reported per key by GET /api/rigor/usage-summary.

Context persistence (stored-plan execute)

context.additional_context supplied to /plan is persisted with the plan. When you later call /execute with workflow_id, that context is resolved as follows:

  1. Body omits context — the context supplied to the original /plan call is used automatically. A workflow planned with context no longer executes context-less just because the execute request leaves context out.
  2. Body supplies context identical to the plan's — used as-is, no warning.
  3. Body supplies context that differs from the plan's — the body wins (explicit intent overrides the stored plan), and the response carries a context_differs_from_plan warning suggesting a re-plan so every step sees the updated context.
  4. The stored plan predates context persistence (a legacy plan) and the body omits context — the request still proceeds, but the response carries a legacy_plan_context_unavailable warning: the original plan-time context could not be recovered.
  5. The workflow was planned without any context — unaffected either way; there is nothing to inherit and nothing to differ from.

A fresh execute call (task_description, no workflow_id) is unaffected by all of this: context.additional_context in that request works standalone, exactly as before.

Field reference — warnings (stored-plan execute only):

Same shape as the plan endpoint's structured warnings: { code, message, suggestion }. Present in the response (workflow_started SSE event, or the top-level JSON body for polling/webhook/interactive delivery) only when non-empty.

Warning code Trigger
context_differs_from_plan The execute body's context.additional_context differs from the context persisted at plan time; the body's value was used.
legacy_plan_context_unavailable The stored plan predates context persistence; its plan-time context could not be recovered, and this request supplied no context.additional_context of its own.

Response — polling or webhook delivery

{
  "ok": true,
  "workflow_id": "wr_2a4b6c8d...",
  "status": "queued",
  "task_type": "solution_design",
  "value_class": "operational",
  "plan": { },
  "estimated_credits": 540,
  "poll_url": "/api/rigor/status/wr_2a4b6c8d...",
  "delivery_mode": "polling",
  "available_modes": ["sse", "polling", "webhook", "interactive"]
}

For webhook delivery the body also includes webhook_url, webhook_status: "registered", and (on first creation) a webhook_secret — a 32-byte hex HMAC secret for verifying webhook payloads.

For a stored-plan execute (workflow_id), the body also includes warnings (see context persistence above) when the request produced one.

Response — SSE delivery (default)

Content-Type: text/event-stream, Cache-Control: no-cache, no-transform, X-Accel-Buffering: no, X-Plith-Workflow-Id: wr_....

Read X-Plith-Workflow-Id if you cannot consume the body incrementally. The workflow row is created before this response is produced, so from the moment you receive the status line the workflow exists, is running, and is billable. Response headers arrive with that status line; the workflow_started event does not arrive until the stream body starts. A client that reads the body as a single document (await res.json(), or any HTTP client without streaming support) will therefore sit on an open stream until the run ends or the 240 s stream timeout fires, and if it gives up before then it has no id to poll with. The header removes that trap. It is set on SSE responses only — polling, webhook, and interactive delivery return workflow_id in the JSON body.

Event sequence:

  1. workflow_started — workflow metadata + steps preview. Carries warnings (stored-plan execute only — see context persistence above) when present.
  2. step_started / step_completed / step_skipped — per step.
  3. terminal_step_retrying — a final review or validation step failed on its time budget and is being retried with a degraded strategy. Data: { workflow_id, step_index, step_name, attempt, max_attempts, strategy, message }. strategy is compressed, chunked, or salvage.
  4. quality_review — score + summary, after execution.
  5. Terminal event: workflow_completed, workflow_pending_approval, or workflow_error. When the workflow finishes as completed_partial via deliverable salvage, the terminal event is workflow_completed with partial: true and incomplete_steps in the data — the deliverable field carries the salvaged content.
  6. processing — heartbeat every 15 s while waiting.
  7. stream_timeout — stream closes at 290 s; workflow continues server-side and you can poll status.

Each event carries an id: field of the form {workflow_id}:{step_or_marker}. To reconnect after a drop, resend the request with a Last-Event-ID header set to the last received id: — missed events will be replayed.

Response — interactive mode

{
  "ok": true,
  "workflow_id": "wr_2a4b6c8d...",
  "status": "interactive_waiting",
  "classification": { "task_type": "solution_design", "value_class": "operational" },
  "sequence": [
    { "index": 0, "name": "Requirements Analysis", "required": true }
  ],
  "total_steps": 5,
  "cost": { "credits": 540, "usd": 0.54 },
  "delivery_mode": "interactive",
  "available_modes": ["sse", "polling", "webhook", "interactive"]
}

For a stored-plan execute (workflow_id), the body also includes warnings (see context persistence above) when the request produced one.

Errors (in addition to /plan errors):

  • 400 invalid_languagelanguage is not a valid BCP 47 code.
  • 400 no_planworkflow_id row has no plan.
  • 400 webhook_url_required / 400 webhook_url_https / 400 webhook_url_invalid
  • 402 insufficient_creditsdetails.required_credits, details.balance.
  • 402 workspace_budget_exceeded — workflow estimate exceeds the workspace's monthly credit budget.
  • 403 forbiddenworkflow_id belongs to a different org.
  • 403 tier_required — feature requires a higher subscription tier.
  • 404 not_foundworkflow_id not found.
  • 404 prior_not_found
  • 409 prior_not_terminal
  • 409 duplicate_task — same task submitted in last 5 min; details.existing_workflow_id.
  • 409 workflow_has_progress — stored workflow already has step results; use /resume.
  • 410 workspace_archived — workspace has been archived.
  • 422 chain_depth_exceededprior_workflow_id chain exceeds 10 links.
  • 422 context_too_largedetails.estimated_tokens, details.max_tokens.
  • 429 concurrent_limit_exceededdetails.active_count, details.limit, details.active_workflows.
  • 500 queue_failed

curl (Windows) — polling delivery:

curl -X POST https://plith.ai/api/rigor/execute -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"workflow_id\": \"wr_2a4b6c8d...\", \"delivery\": {\"method\": \"polling\"}}"

curl (Windows) — SSE delivery:

curl -X POST https://plith.ai/api/rigor/execute -H "x-api-key: YOUR_KEY" -H "Accept: text/event-stream" -H "Content-Type: application/json" -d "{\"workflow_id\": \"wr_2a4b6c8d...\"}"

GET /api/rigor/status/{workflow_id}

Auth: Required (x-api-key) Cost: Free

Returns workflow state and results. Only the owning organization can read a workflow.

Response (200):

{
  "ok": true,
  "workflow": {
    "workflow_id": "wr_2a4b6c8d...",
    "status": "completed",
    "task_type": "solution_design",
    "value_class": "operational",
    "current_step": 5,
    "total_steps": 5,
    "actual_steps_executed": 5,
    "completed_step_summaries": [
      { "step": 1, "tool": "Requirements Analysis", "framework": "Requirements Analysis", "summary": "...", "model": "claude-sonnet-4-6" }
    ],
    "quality_score": 92,
    "quality_review": {
      "score": 92,
      "summary": "Strong design with clear trade-offs.",
      "path_to_100": [
        "Add p50/p95/p99 latency targets per cache tier."
      ]
    },
    "deliverable": {
      "title": "Solution Design — Rigor Workflow Output",
      "sections": [
        { "heading": "Requirements Analysis", "content": "..." }
      ]
    },
    "credits_used": 530,
    "error": null
  }
}

Field reference:

Field Description
workflow_id Unique workflow identifier.
status One of: pending, planned, queued, executing, step_executing, resuming, quality_review, interactive_waiting, pending_approval, pending_human_review, approved, completed, completed_partial, failed, timed_out, blocked, halted.
task_type Classified task type (e.g. solution_design, code_review).
value_class One of "operational", "governance", "strategic".
current_step Most recent step number reached.
total_steps Total steps in the plan.
actual_steps_executed Number of distinct planned steps that produced output the run retained — a step whose latest recorded status is completed or refined. A revised step is a superseded attempt that review replaced and is counted once, not twice; skipped and failed steps are not counted at all. This is not an attempt count, and it is not expected to equal total_steps: total_steps counts every planned step including one that was planned and then skipped, so a clean run that skipped a step reads one lower here. The identity that reconciles them is actual_steps_executed + skipped + incomplete = total_steps (B.1180).
completed_step_summaries Array of completed steps. Each: { step, tool, framework, summary, model? }. tool and framework are the same display name under 2 keys — tool is the original and is not going away, framework matches the key step_outputs and step_recovery already use, so one field name reads across all 3 arrays. model is the LLM identifier used for that step (Pro+ only).
quality_score 0–100 review score. null when not yet scored — never coerced to 0.
quality_review { score, summary, path_to_100? }. path_to_100 lists concrete improvements to reach a perfect score.
deliverable Present when status is completed, completed_partial, or pending_approval. Shape: { title, sections: [{ heading, content }] }. On completed_partial the deliverable may be absent when the workflow ended via chunk abandonment on a content step (see below).
credits_used Credits charged for this workflow, per-step charges plus the orchestration fee on a full completion. A halted workflow reports the per-step credits that were charged before the halt and no orchestration fee — halting saves you the remaining steps, it does not refund the ones that ran. 0 means nothing was charged, which for a failed workflow means the per-step charges were refunded.
credits_live The running per-step total maintained during execution, without the orchestration fee. On a terminal workflow this equals credits_used minus any orchestration fee. Use credits_used for anything that has to reconcile with billing.
error Error message when the workflow failed, or the partial-completion explanation when status is completed_partial.
execution_mode Which execution shape actually ran: "direct" or "standard". Always present. A direct-execution attempt that fell back reports "standard" — this field says what happened, execution_fallback says whether an explicit request was overridden.
execution_fallback true when this workflow requested direct execution but ran on the standard worker loop instead — either the pre-check rejected it (server configuration or an approval conflict) or it fell back at runtime after exceeding the composition limit. Absent when no fallback occurred, including when direct execution was never requested.

Halted workflow fields (present only when status === "halted"):

Field Description
halted_at_step Step number where the halt occurred.
halt_reason Human-readable reason (e.g. required context missing, MCP connection failed).
blocking_issues Structured list of issues that triggered the halt.
credits_saved_by_halt Credits not consumed because execution stopped early.
resolution_options Array of four suggested remediation actions.

Example halted workflow response:

{
  "workflow_id": "wr_2a4b6c8d...",
  "status": "halted",
  "task_type": "solution_design",
  "current_step": 2,
  "total_steps": 5,
  "actual_steps_executed": 1,
  "completed_step_summaries": [],
  "halted_at_step": 2,
  "halt_reason": "Required context missing: file src/api/gateway.ts not accessible via connected repository.",
  "blocking_issues": ["File src/api/gateway.ts referenced but not found in GitHub MCP connection"],
  "credits_saved_by_halt": 340,
  "resolution_options": [
    "Skip the blocking framework using preferences.skip_frameworks and re-execute",
    "Verify the MCP connection can access the referenced files, then re-execute with refresh_connections: true",
    "Re-submit with the missing file content provided inline in context.additional_context",
    "Fix the blocking issue and re-execute with prior_workflow_id to inherit completed work"
  ],
  "credits_used": 80
}

Chunk execution fields (present when a step is executing in chunked mode):

Field Description
current_chunk.index Zero-based index of the chunk currently executing.
current_chunk.total Total number of chunks for the current step.

Partial completion (status === "completed_partial"):

A workflow finishes as completed_partial in 4 cases:

  1. Deliverable salvage. All content steps completed, but a final review or validation step could not complete within its retry ladder. The finished deliverable is preserved and returned, quality_score is computed on what exists, and incomplete_steps names exactly what did not run. Billing reflects the delivered work: step credits and the orchestration fee are charged, and the reserved-versus-actual balance is reconciled.
  2. Chunk abandonment. A content-producing step running in chunked mode was abandoned after repeated failures, so no deliverable could be assembled. deliverable is absent in this case.
  3. Required-step degradation. A required step that the plan had already flagged as running without its context failed the way the plan anticipated. The run continued to the end, and the named step is absent from the deliverable. The plan warning that predicted it is reproduced verbatim in the step's reason.
  4. Required-step loss. A required step that was expected to work did not complete, and the run was allowed to continue without it. Today this is the research step, whose failure has never been permitted to end a run. The remaining steps ran and their content is real, but any conclusion that depended on the missing step is unverified.

In cases 3 and 4 the orchestration fee is not charged, because that fee prices a complete run. Per-step credits for the steps that did complete stay charged.

Field Description
incomplete_steps Array of { step_index, framework, reason } for plan steps that did not complete. On salvage, reason is one of invocation_budget_exhausted, review_chunks_abandoned, not_reached. On cases 3 and 4 it is a sentence naming what is missing and what it means for the result. Absent on chunk abandonment.

Treat completed_partial with a deliverable as a usable result, but check incomplete_steps before relying on it: on cases 1 and 3 only the named process steps are missing, while on case 4 an input the deliverable was supposed to rest on is missing.

On quality_score when a required step was lost. The score is still computed and still returned. It measures the deliverable that exists, which is real information, and suppressing it would remove the run from the evidence the routing layer learns from. It does not measure whether the missing input would have changed the answer. Read it together with status and incomplete_steps, never alone: a score of 80 on a completed_partial run whose research step is named in incomplete_steps is a score over a deliverable missing a required input.

Detecting a partial run without reading step internals. Check status !== "completed". A run that lost a step never reports completed. Do not substitute actual_steps_executed < total_steps for that check: a clean run whose planner skipped a step reads lower than total_steps and is not partial (B.1180). The independent signal is incomplete_steps, which names the steps that reached a terminal state without output; actual_steps_executed + skipped + incomplete_steps reconciles to total_steps.

Terminal-step retry fields (present while a final review or validation step is being retried with a degraded strategy):

Field Description
terminal_step_attempts Number of failed attempts at the terminal step so far. Present when greater than 0.
terminal_step_strategy Strategy the next attempt will use: compressed (reduced input), chunked (map-reduce review), or salvage (finishing with the existing deliverable). Absent when the step is running normally.

Clients can render these as, for example, "final review retrying with a different strategy (2/4)" instead of showing silence between attempts.

Errors:

  • 401 unauthorized
  • 400 missing_fieldworkflow_id required.
  • 404 not_found — also returned when the workflow belongs to a different org.
  • 429 rate_limit_exceeded

curl (Windows):

curl https://plith.ai/api/rigor/status/wr_2a4b6c8d... -H "x-api-key: YOUR_KEY"

POST /api/rigor/approve

Auth: Required (x-api-key) Cost: Free

Decision endpoint for workflows paused at pending_approval. On approval, the workflow automatically resumes execution.

Request body:

Field Type Required Description
workflow_id string Yes Workflow to decide.
decision "approved" | "rejected" Yes Your decision.
notes string No Notes captured with the decision and (on approval) merged into preferences for subsequent steps.
agent_id string No Identifier of the approving agent (defaults to "board_chair").

Response (200) — approved:

{
  "ok": true,
  "workflow_id": "wr_2a4b6c8d...",
  "decision": "approved",
  "message": "Workflow approved and resuming execution."
}

Response (200) — rejected:

{
  "ok": true,
  "workflow": {
    "workflow_id": "wr_2a4b6c8d...",
    "status": "failed",
    "halt_reason": "rejected"
  },
  "message": "Workflow rejected. No further steps will execute."
}

On rejection, the workflow is marked failed with halt_reason: "rejected".

Errors:

  • 401 unauthorized
  • 400 invalid_body
  • 400 missing_fieldworkflow_id, decision.
  • 404 not_found
  • 409 invalid_status — workflow is not at pending_approval.
  • 429 rate_limit_exceeded
  • 500 approve_failed

curl (Windows):

curl -X POST https://plith.ai/api/rigor/approve -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"workflow_id\": \"wr_2a4b6c8d...\", \"decision\": \"approved\", \"notes\": \"OK to proceed\"}"

POST /api/rigor/resume

Auth: Required (x-api-key) Cost: Variable — incremental cost of remaining steps.

Resumes a workflow from the last completed step. Permitted source statuses: executing, failed, approved, blocked, timed_out, pending_approval, pending_human_review, quality_review. Uses an optimistic lock — concurrent resume calls return resume_conflict.

For pending_human_review workflows, the operator must explicitly acknowledge the escalation by passing ?approve=true on the query string.

Request body:

Field Type Required Description
workflow_id string Yes Workflow to resume.
revision_guidance string No Free-text guidance injected into the execution context for subsequent steps.

Response (200):

{
  "ok": true,
  "workflow_id": "wr_2a4b6c8d...",
  "status": "resuming",
  "resuming_from_step": 3,
  "total_steps": 5,
  "poll_url": "/api/rigor/status/wr_2a4b6c8d...",
  "message": "Resuming from step 3 of 5. Poll status for progress.",
  "revision_guidance": null
}

resuming_from_step is the next step the worker will execute.

Errors:

  • 401 unauthorized
  • 400 invalid_body
  • 400 missing_fieldworkflow_id required.
  • 404 not_found
  • 409 invalid_statusdetails.allowed_statuses lists permitted statuses.
  • 409 resume_conflict — another resume is already in flight.
  • 409 already_complete — every step has a result; nothing to resume.
  • 429 rate_limit_exceeded

curl (Windows):

curl -X POST https://plith.ai/api/rigor/resume -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"workflow_id\": \"wr_2a4b6c8d...\"}"

POST /api/rigor/step

Auth: Required (x-api-key) Cost: Variable — incremental cost of one step.

Drives a workflow created with mode: "interactive". The interactive session expires after 30 minutes of inactivity; on timeout the workflow is marked timed_out and the call returns session_expired.

Request body:

Field Type Required Description
workflow_id string Yes Interactive workflow to drive.
action "next" | "skip" | "abort" Yes What to do with the current step.
feedback string No Optional free-text forwarded to the step execution context.

Response (200) — action: "abort":

{
  "ok": true,
  "workflow_id": "wr_2a4b6c8d...",
  "status": "aborted",
  "steps_completed": 2,
  "total_steps": 5
}

Response (200) — action: "next" (step dispatched):

{
  "ok": true,
  "workflow_id": "wr_2a4b6c8d...",
  "status": "step_executing",
  "step": { "index": 2, "name": "Research" },
  "poll_url": "/api/rigor/status/wr_2a4b6c8d...",
  "message": "Step execution started. Poll status for result."
}

Response (200) — action: "skip":

{
  "ok": true,
  "workflow_id": "wr_2a4b6c8d...",
  "status": "interactive_waiting",
  "step_executed": {
    "step": 2,
    "name": "Assumption Verification",
    "status": "skipped"
  },
  "next_step": { "index": 3, "name": "Solution Design" },
  "steps_completed": 1,
  "total_steps": 5
}

Errors:

  • 401 unauthorized
  • 400 invalid_body
  • 400 missing_fieldworkflow_id required.
  • 400 invalid_action — must be next, skip, or abort.
  • 400 invalid_field — workflow must have been created with mode: "interactive".
  • 400 no_plan
  • 403 tier_required — feature requires a higher subscription tier.
  • 404 not_found
  • 409 invalid_status — workflow is not at interactive_waiting.
  • 409 concurrent_step — another step execution holds the lock; retry in 2–3 seconds.
  • 410 session_expired — 30-min TTL exceeded; workflow marked timed_out.
  • 429 rate_limit_exceeded
  • 500 step_error

curl (Windows):

curl -X POST https://plith.ai/api/rigor/step -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"workflow_id\": \"wr_2a4b6c8d...\", \"action\": \"next\"}"

GET /api/rigor/workflows

Auth: Required (x-api-key) Cost: Free

Lists workflows for the authenticated org with filtering and cursor-based pagination. Also returns a real-time concurrent-usage summary.

Query parameters:

Parameter Type Description
status string Comma-separated status filter (e.g. executing,queued).
task_type string Filter by task type.
created_after ISO timestamp Workflows created after this time.
created_before ISO timestamp Workflows created before this time.
counts_toward_limit "true" | "false" Filter to workflows that do / do not count toward the concurrent limit.
cursor string Opaque cursor from a prior response's pagination.cursor for the next page.
limit integer Page size. Min 1, max 100, default 20.
q string Search the workflow title and task description. Every whitespace-separated term must appear in one or the other. Substring matching, case-insensitive — not full-text. Max 200 characters, first 8 terms. See the note below.
folder_id string Filter by folder ID. Pass "unassigned" to return workflows not in any folder.

Response (200):

{
  "ok": true,
  "request_id": "req_abc123...",
  "credits_used": 0,
  "credits_remaining": 9484,
  "workflows": [
    {
      "workflow_id": "wr_2a4b6c8d...",
      "status": "completed",
      "task_type": "solution_design",
      "generated_title": "Migration plan — Postgres 14 to 16",
      "current_step": 5,
      "total_steps": 5,
      "credits_charged": 530,
      "created_at": "2026-05-08T12:00:00Z",
      "started_at": "2026-05-08T12:00:01Z",
      "completed_at": "2026-05-08T12:05:00Z",
      "task_excerpt": "Add vector search to an existing Postgres-backed SaaS app",
      "history_locked": false,
      "approval_status": "not_required",
      "counts_toward_limit": false,
      "available_actions": []
    }
  ],
  "pagination": {
    "cursor": "2026-05-08T12:00:00Z",
    "has_more": false,
    "total_count": 1,
    "limit": 20
  },
  "concurrent_summary": {
    "active": 0,
    "limit": 5,
    "remaining": 5
  }
}

Workflow summary fields. Corrected 2026-08-20 against a live response; the previous list omitted 4 fields that are always returned.

Field Always present Notes
workflow_id, status, task_type, current_step, total_steps, credits_charged, created_at, started_at, completed_at yes started_at and completed_at are null until the workflow reaches those states
generated_title yes Model-generated summary title, truncated. null when none was generated
history_locked yes true when the workflow falls outside your tier's history window. The row still appears in the list; its detail is withheld by GET /status
counts_toward_limit yes Whether this workflow is currently consuming concurrency
available_actions yes ["cancel"], ["force_cancel"], or [] — the actions POST /workflows/actions will accept for this row right now
approval_status when set e.g. not_required, and present on effectively every row in practice
error, halt_reason, halted_at_step when set omitted entirely when the workflow has not errored or halted
task_excerpt when readable The first 200 characters of the task description, so a q hit can be judged without a second call. null when the workflow has no description. Omitted on history_locked rows, and omitted for orgs on the internal response mode, which receive the full task_description instead

concurrent_summary.limit is null on Enterprise plans (unlimited); otherwise it is the tier's integer cap. concurrent_summary.remaining is null when limit is null.

q — corrected 2026-08-29, and how the matching actually works. This table previously described q as "full-text search on workflow title and task description". It was neither: the handler ran a single task_description ILIKE '%q%', which searched only the task description and only for one contiguous substring. The parameter now matches the description above. What it does:

  • q is split into terms on whitespace. Characters that are not letters, digits, or one of _ - . / + # @ & and ' act as separators — including %, *, ,, (, ), " and \ — so **wildcards are not supported**, and a query made only of those characters applies no filter. _ is matched literally, not as a wildcard, so q=task_description searches for that text.
  • A workflow matches when every term is found in either generated_title or the task description, as a case-insensitive substring. q=vector search postgres therefore matches a task described as "add vector search to an existing Postgres-backed SaaS app"; before this correction it matched nothing.
  • It is substring matching, not tsvector full-text. There is no stemming, no ranking, no stop words, and no relevance ordering — results come back newest first like any other page. q=migrating does not match "migration".
  • At most the first 200 characters and the first 8 terms are used. Terms are deduplicated.

Matching is applied identically to the rows returned and to pagination.total_count.

Errors:

  • 401 unauthorized
  • 429 rate_limit_exceeded
  • 500 internal_error

curl (Windows):

curl "https://plith.ai/api/rigor/workflows?status=executing,queued&limit=10" -H "x-api-key: YOUR_KEY"

POST /api/rigor/workflows/actions

Auth: Required (x-api-key) Cost: Free Rate limit: 30 requests/minute (separate from the main 120 req/min limit)

Bulk workflow action endpoint. Currently supports action: "cancel". Operates in two modes:

  • Preview (default, confirm: false) — shows what would happen without making changes.
  • Execute (confirm: true) — applies the cancellations.

Request body:

Field Type Required Description
action "cancel" Yes Action to perform. Only "cancel" is currently supported.
workflow_ids string[] Yes IDs of workflows to act on. Max 20 per request. Deduplicated automatically.
confirm boolean No false (default) = preview only. true = execute.
force boolean No true = force-cancel workflows in executing or step_executing status. Without this, those statuses are skipped.

Workflows not owned by the caller's org are silently skipped.

Response (200) — preview (confirm: false):

{
  "ok": true,
  "request_id": "req_abc123...",
  "preview": true,
  "affected": [
    { "workflow_id": "wr_aaa...", "current_status": "queued", "action": "cancel" },
    { "workflow_id": "wr_bbb...", "current_status": "executing", "action": "force_cancel" }
  ],
  "would_skip": [
    { "workflow_id": "wr_ccc...", "reason": "Status 'completed' cannot be cancelled" }
  ],
  "message": "2 workflow(s) would be cancelled. Send confirm: true to execute."
}

Response (200) — execute (confirm: true):

{
  "ok": true,
  "request_id": "req_abc123...",
  "credits_used": 0,
  "credits_remaining": 9484,
  "results": {
    "succeeded": [
      { "workflow_id": "wr_aaa...", "previous_status": "queued", "new_status": "cancelled" }
    ],
    "failed": [],
    "skipped": [
      { "workflow_id": "wr_ccc...", "reason": "Status 'completed' cannot be cancelled" }
    ]
  },
  "message": "1 cancelled, 1 skipped, 0 failed."
}

Errors:

  • 401 unauthorized
  • 400 invalid_body
  • 400 invalid_action — only "cancel" is accepted.
  • 400 no_workflow_idsworkflow_ids missing or empty.
  • 400 bulk_limit_exceededworkflow_ids exceeds 20.
  • 429 rate_limit_exceeded (30 req/min)
  • 500 internal_error

curl (Windows) — preview:

curl -X POST https://plith.ai/api/rigor/workflows/actions -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"action\": \"cancel\", \"workflow_ids\": [\"wr_aaa...\"]}"

curl (Windows) — execute:

curl -X POST https://plith.ai/api/rigor/workflows/actions -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"action\": \"cancel\", \"workflow_ids\": [\"wr_aaa...\"], \"confirm\": true}"

GET /api/rigor/lineage/{workflowId}

Auth: Required (x-api-key) — minimum role: viewer Cost: Free

Walks the prior_workflow_id chain backward from the given workflow and returns the full lineage, ordered oldest-first. Capped at 10 hops.

Response (200):

{
  "ok": true,
  "chain": [
    {
      "workflow_id": "wr_oldest...",
      "status": "completed",
      "task_type": "solution_design",
      "total_steps": 4,
      "created_at": "2026-05-01T10:00:00Z"
    },
    {
      "workflow_id": "wr_2a4b6c8d...",
      "status": "completed",
      "task_type": "code_review",
      "total_steps": 3,
      "created_at": "2026-05-08T12:00:00Z"
    }
  ],
  "total_steps": 7,
  "chain_depth": 2
}

Errors:

  • 401 unauthorized
  • 400 missing_fieldworkflowId required in path.
  • 404 not_found — workflow not found or belongs to another org.
  • 422 chain_depth_exceeded — chain exceeds 10 hops.
  • 429 rate_limit_exceeded

curl (Windows):

curl https://plith.ai/api/rigor/lineage/wr_2a4b6c8d... -H "x-api-key: YOUR_KEY"

GET /api/rigor/templates

Auth: None — public endpoint. Cost: Free

Returns deliverable templates that describe how Rigor structures its output for each task type. Templates can be used to preview the deliverable shape before running a workflow.

Catalogue size changes as templates are added. Do not hard-code a count. As of 2026-08-20 the live endpoint returns 70 templates across 67 task types and 8 categories — this document previously said "43 types across 8 categories", which was correct when written and is not now.

Query parameters:

Parameter Type Description
task_type string Exact match filter on task type.
category string Exact match filter on category.

Response (200):

{
  "templates": [
    {
      "id": "ce19e34e-3677-4171-bde3-92671db22368",
      "task_type": "solution_design",
      "template_name": "Solution design",
      "template_name_i18n": null,
      "template_prompt": "Design a solution for {{PROBLEM}}. Constraints: {{CONSTRAINTS}}.",
      "placeholder_fields": [
        {
          "key": "PROBLEM",
          "label": "Problem",
          "required": true,
          "placeholder": "Search latency is above 2 s at p95"
        }
      ],
      "description": "Architecture and design document for a technical system.",
      "description_i18n": null,
      "example_filled": "...",
      "display_order": 1,
      "version": 1,
      "category": "Engineering",
      "categories": ["Engineering", "Operations"],
      "demo": false
    }
  ]
}

Two shape corrections, both verified live on 2026-08-20.

  • placeholder_fields is an array of objects, not an array of strings. Each object is { key, label, required, placeholder }, where key is the token substituted into template_prompt as {{KEY}}. A client that treated the array as strings — as this document previously showed — reads [object Object] for every field.
  • category is a single string and categories is a separate array that may name more than 1. The category query parameter filters on the singular field. The 8 values of category are Content & communication, Engineering, Governance & compliance, HR & recruiting, Operations & PM, Product & growth, Research & analysis, and Strategy & finance.

template_name_i18n and description_i18n are localisation slots. Both are null on all 70 templates today; treat a non-null value as a map keyed by BCP 47 language code and fall back to the base field. demo is a boolean marking templates surfaced in the product's demo flow; it is false on all 70 today.

curl (Windows):

curl "https://plith.ai/api/rigor/templates?category=Engineering"

POST /api/rigor/enhance

Auth: Not required for deterministic tier. Required (x-api-key) for llm tier. Cost: Free for deterministic. 270 credits for llm.

Scores and optionally enhances a task description before submitting it to Rigor. Use this to check task quality and get improvement suggestions before spending credits on execution.

Request body:

Field Type Required Description
task_description string Yes Task description to score. Max 10,000 characters.
task_type string No Hint for more accurate scoring.
tier "deterministic" | "llm" No Default "deterministic". "llm" uses an LLM for richer suggestions.

Response (200):

{
  "ok": true,
  "request_id": "req_abc123...",
  "score": 72,
  "suggestions": [
    "Specify the target programming language or framework.",
    "Mention any performance or scale constraints."
  ],
  "enhanced": "Design a Redis-backed caching layer for the REST API gateway, targeting sub-10ms p99 cache hit latency and 99.9% availability. The cache should handle 50k req/s at peak..."
}

enhanced is only present when tier: "llm".

Errors:

  • 400 invalid_body
  • 400 missing_fieldtask_description required.
  • 400 input_too_longtask_description exceeds 10,000 characters.
  • 500 enhance_error

curl (Windows) — deterministic (no auth):

curl -X POST https://plith.ai/api/rigor/enhance -H "Content-Type: application/json" -d "{\"task_description\": \"Design a caching layer\"}"

curl (Windows) — LLM tier:

curl -X POST https://plith.ai/api/rigor/enhance -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"task_description\": \"Design a caching layer\", \"tier\": \"llm\"}"

GET /api/rigor/task-types

Auth: Required (x-api-key) — no minimum role; any valid key is accepted. Verified 2026-08-20: an unauthenticated call returns 401. Unlike GET /templates, this route has no public carve-out in middleware. Cost: Free Rate limit: None.

Returns the task-type vocabulary that task_type accepts on /plan and /execute.

Response (200): { "task_types": [ { "id": "competitive_scan", "label": "Competitive Scan", "shape": "workflow" } ] }

shape is "atomic" for single-call primitives — tag, score, rerank, extract_entities, parse_query, and the quick_* types — and "workflow" for multi-step deliverable types. Atomic types auto-select direct execution when you express no execution preference. The distinction is worth reading before you pick a task_type: the 2 shapes differ by roughly 2 orders of magnitude in cost and latency.


GET /api/rigor/usage-summary

Auth: Required (x-api-key) — minimum role: viewer Cost: Free

Credit balance, burn rate, and usage broken down by workspace, task type, and API key.

Query parameters: from, to (ISO timestamps; default the last 30 days) and granularity (day by default).

Response (200) — top-level keys:

Field Description
balance { credits, usd }
burn_rate { credits_per_day, usd_per_day, window_days, by_surface }. by_surface splits workflow from gateway.
period_totals { credits, usd, workflow_count, by_surface } for the requested range.
trend Per-granularity buckets across the range.
breakdown_by_workspace Per-workspace credits, usd, and share.
breakdown_by_task_type { task_type, credits, usd, workflow_count, pct }.
breakdown_by_api_key { api_key_id, api_key_name, credits, usd, call_count, pct }. Calls made from a signed-in browser rather than a key appear as a single row with api_key_id: null and api_key_name: "Browser session".

breakdown_by_api_key is the endpoint to use if you want to attribute spend to a caller you can trust. It groups on the key id the middleware resolved, so it is not affected by what a caller puts in agent_id — and it is only as granular as your key allocation. Several callers sharing one key are one row.


GET /api/rigor/costs

Auth: Required (x-api-key) — minimum role: viewer Cost: Free

Workflow and step counts with credits charged, grouped over a time range.

Query parameters: from, to, and group_by (day by default).

Response (200): { ok, request_id, org_id, workspace_id, raw_cogs_visible, range, group_by, summary, breakdown }, where summary is { workflow_count, step_count, credits_used, revenue_usd } and breakdown is the same shape per bucket. raw_cogs_visible is false for customer keys — underlying cost of goods is not exposed.


MCP Connections

MCP connections let your organization register external MCP servers as data sources. Once registered and tested, connected servers provide read-only external context to Rigor workflow executions.

All connection endpoints require x-api-key and return 501 if the MCP gateway is not enabled.

POST /api/rigor/connections

Auth: Required (x-api-key) — minimum role: admin Cost: Free

Registers a new MCP server connection. The server URL is validated for SSRF safety at registration time. Credentials are encrypted at rest immediately and never returned in any response.

Request body:

Field Type Required Description
display_name string Yes Slug-safe identifier (1-32 chars, [a-z0-9_-]+). Used in tool namespacing as mcp__{display_name}__{tool}. Immutable after first tool call.
scope "org" | "workspace" Yes org = visible to all workspaces. workspace = visible to one workspace only.
workspace_id string (UUID) Conditional Required when scope is "workspace". Must be a valid workspace in the org.
label string No Alias for display_name. If both are provided, display_name takes precedence.
server_url string Yes HTTPS URL of the MCP server.
auth_type string Yes One of: bearer_token, api_key, oauth, none.
transport string No streamable-http (default) or sse.
auth_credential object Conditional Required when auth_type is not none.

Credential shapes by auth_type:

auth_type auth_credential shape
bearer_token { "token": "your-bearer-token" }
api_key { "key": "your-api-key", "header": "x-api-key" }
oauth { "access_token": "your-access-token" }
none Omit auth_credential or pass {}

Response (201):

{
  "connection": {
    "id": "conn_a1b2c3d4-...",
    "display_name": "my-mcp-server",
    "label": "My MCP Server",
    "scope": "org",
    "workspace_id": null,
    "server_url": "https://mcp.example.com/sse",
    "transport": "sse",
    "auth_type": "bearer_token",
    "status": "pending",
    "created_at": "2026-04-25T12:00:00Z"
  }
}

status starts as pending. Call POST /api/rigor/connections/{id}/test to promote it to active.

Errors:

  • 400 — invalid or HTTPS-only violation on server_url.
  • 400 — missing required fields (display_name, server_url, auth_type, scope).
  • 400display_name does not match [a-z0-9_-]+ or exceeds 32 characters.
  • 409 — duplicate server_url within the same scope; existing_id returned. The same URL can exist once at org scope and once per workspace.
  • 409 — maximum 10 connections per organization.
  • 501 feature_not_enabled

curl (Windows):

curl -X POST https://plith.ai/api/rigor/connections -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"display_name\": \"my-mcp-server\", \"scope\": \"org\", \"server_url\": \"https://mcp.example.com/sse\", \"auth_type\": \"bearer_token\", \"auth_credential\": {\"token\": \"my-token\"}, \"transport\": \"sse\"}"

GET /api/rigor/connections

Auth: Required (x-api-key) Cost: Free

Returns all connections visible to your organization and workspace, plus providers available for new connections. Credentials are never included in the response.

Response (200):

{
  "connections": [
    {
      "id": "8188ac32-e8aa-4413-9c45-95f58bcedd1b",
      "label": "GitHub",
      "provider": "github",
      "server_url": "https://api.githubcopilot.com/mcp/",
      "transport": "streamable-http",
      "auth_type": "oauth",
      "capabilities": { "github_owner": "acme", "github_repo": "api" },
      "config": {},
      "status": "active",
      "token_valid": true,
      "last_tested_at": "2026-08-20T00:01:44.085+00:00",
      "created_at": "2026-07-29T15:25:54.725053+00:00",
      "updated_at": "2026-08-20T00:01:44.263238+00:00",
      "workspace_id": null,
      "scope": "org",
      "provider_metadata": {
        "name": "GitHub",
        "description": "Search code, pull issues and PRs into your workflows",
        "icon": "...",
        "tier": 1,
        "category": "code",
        "auth_type": "oauth",
        "capabilities_read": ["search_code", "list_issues", "get_file"],
        "capabilities_write": ["create_issue"]
      }
    }
  ],
  "available_providers": [
    {
      "id": "linear",
      "name": "Linear",
      "description": "Sync issues, projects, and cycles.",
      "icon": "...",
      "tier": 1,
      "auth_type": "oauth",
      "category": "project"
    }
  ],
  "coming_soon_providers": [
    {
      "id": "microsoft-teams",
      "name": "Microsoft Teams",
      "description": "Search messages and channels.",
      "icon": "...",
      "tier": 2,
      "category": "communication"
    }
  ]
}

Corrected 2026-08-20 against a live response. Three fields this document previously documented are not returned, and 5 that are returned were not documented.

Was documented Reality
count Not returned. Use connections.length. app/api/rigor/connections/route.ts:170-174 returns exactly 3 top-level keys
display_name on the connection object Not returned. Tool namespacing uses provider; the human-readable name is label
id shown as conn_a1b2c3d4-... A bare UUID, with no conn_ prefix
available_providers[].tier as "standard" An integer (1, 2), not a string

Connection object fields:

Field Type Description
id string Connection UUID.
label string Human-readable name.
provider string Registry provider id — github, linear, atlassian, mcp-server for a self-hosted server, and so on.
scope "org" | "workspace" Derived from workspace_id: "workspace" when set, "org" when null. Org-scoped connections are visible to all workspaces.
workspace_id string | null UUID of the owning workspace for workspace-scoped connections; null for org-scoped.
config object Provider-specific configuration. {} when none.
token_valid boolean false once the stored OAuth token's token_expires_at has passed; true when it has not, and true when there is no expiry to check. A true here is not a liveness check — use POST /connections/{id}/test for that.
provider_metadata object | null Static catalogue entry for provider, including capabilities_read and capabilities_write. null for a provider not in the registry.

Credentials are never returned. auth_credential is explicitly stripped during enrichment.

Top-level response fields:

Field Type Description
connections object[] All connections visible to this org and workspace.
available_providers object[] Registry providers you have not already connected. Each: { id, name, description, icon, tier, auth_type, category }.
coming_soon_providers object[] Announced but not yet connectable. Each: { id, name, description, icon, tier, category }. Not filtered by what you have connected.

On a gateway failure this endpoint still returns 200-shaped provider lists. If the MCP gateway is unreachable the route returns connections: [] alongside an error object and the full static provider catalogue, with status 500. A client that checks only for the presence of available_providers will read an outage as "no connections configured".

Connection status values:

status Meaning
pending Registered but not yet tested.
active Last test passed — safe to use in workflows.
error Last test failed (DNS check failed or connection refused).
revoked Deleted — credentials cleared, no longer usable.

capabilities is null until a connection test completes. After testing it may contain the server's declared MCP capability set.

curl (Windows):

curl https://plith.ai/api/rigor/connections -H "x-api-key: YOUR_KEY"

PATCH /api/rigor/connections/{id}

Auth: Required (x-api-key) — minimum role: admin Cost: Free

Updates a registered connection. Use to update credentials, label, transport, or permission mode.

When updating permission_mode to "read_write" for an OAuth provider that requires additional scopes, the response may indicate requires_reauth: true and require you to re-initiate the OAuth flow.

Request body:

Send only the fields you want to change. All fields are optional.

Field Type Description
display_name string New slug-safe identifier. Cannot be changed after the connection's first tool call in a workflow execution.
label string New display name.
auth_credential object Updated credential (same shapes as POST).
permission_mode "read" | "read_write" Change the permission scope.

Response (200): Updated connection object (same shape as POST response).

Response (200) — re-auth required:

{
  "requires_reauth": true,
  "reauth_provider": "github",
  "message": "GitHub requires re-authorization to grant write permissions."
}

Errors:

  • 401 unauthorized
  • 403 forbidden — requires admin role.
  • 400 invalid_body
  • 404 not_found
  • 501 feature_not_enabled

curl (Windows):

curl -X PATCH https://plith.ai/api/rigor/connections/conn_a1b2c3d4-... -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"label\": \"Updated MCP Server\"}"

POST /api/rigor/connections/{id}/test

Auth: Required (x-api-key) Cost: Free

Tests connectivity to the registered MCP server. Performs a DNS-level SSRF check — validates that the hostname does not resolve to a private or reserved IP. On success, the connection's status is updated to active and last_tested_at is recorded.

Call this once after registration and again whenever you suspect the server URL's destination may have changed.

Request: No body. Pass the connection ID in the URL path.

Response (200 — test passed):

{
  "status": "active",
  "connection_id": "conn_a1b2c3d4-...",
  "resolved_ips": ["93.184.216.34"],
  "message": "DNS validation passed."
}

Response (400 — SSRF check failed):

{
  "error": "SSRF check failed: mcp.example.internal resolves to blocked IP: IP 10.0.0.1 is in blocked range: private"
}

When an SSRF check fails, the connection's status is also updated to error.

Errors:

  • 401 unauthorized
  • 400 — SSRF check failed.
  • 404 — connection not found.
  • 501 feature_not_enabled

curl (Windows):

curl -X POST https://plith.ai/api/rigor/connections/conn_a1b2c3d4-.../test -H "x-api-key: YOUR_KEY"

DELETE /api/rigor/connections/{id}

Auth: Required (x-api-key) — minimum role: admin Cost: Free

Revokes a connection. This is a soft delete: the record is retained for audit purposes, status is set to revoked, and all stored credentials are cleared. A revoked connection cannot be used in workflow executions.

Request: No body. Pass the connection ID in the URL path.

Response (200):

{
  "connection": {
    "id": "conn_a1b2c3d4-...",
    "status": "revoked"
  },
  "message": "Connection revoked"
}

Errors:

  • 401 unauthorized
  • 404 — connection not found.
  • 501 feature_not_enabled

curl (Windows):

curl -X DELETE https://plith.ai/api/rigor/connections/conn_a1b2c3d4-... -H "x-api-key: YOUR_KEY"

Connection Security

Control What it does
Credential encryption Stored with AES-256-GCM. Credentials are never returned in any API response.
SSRF protection — registration Server URL validated at registration: HTTPS only, no private IPs, no cloud metadata endpoints, no internal hostnames.
SSRF protection — test DNS resolved at test time: all A/AAAA records checked against a blocklist before the connection is marked active.
Read-only enforcement Only resources/read and resources/list MCP operations are ever executed. tools/call, prompts/*, sampling/*, and all write operations are blocked regardless of server capabilities.
Org isolation Every request is filtered by your organization's ID, derived from your API key. You cannot access another organization's connections.
Scope isolation Org-scoped connections are visible to all workspaces. Workspace-scoped connections are visible only to their designated workspace.
Content sanitization External MCP content is wrapped in containment framing and scanned for prompt-injection patterns before being passed to workflow execution.

Limits:

Limit Value
Max connections per organization 10
Duplicate server URLs One per scope (same URL allowed once at org scope + once per workspace)
Accepted protocols HTTPS only

MCP Tools

Connect to Plith via MCP at:

  • Rigor: /api/mcp/rigor

Discoverable tools

These tools appear in tools/list and can be autodiscovered by MCP clients:

Tool Description
rigor_plan Plan a workflow without executing it (free). Available anonymously for evaluation.
rigor_execute Execute a planned or fresh workflow.
rigor_status Get workflow progress and results.

Hidden tools

These tools are callable but do not appear in tools/list:

Tool Description
rigor_resume Resume a stalled or failed workflow.
rigor_approve Approve or reject a workflow pending human review.
rigor_step Drive a single step of an interactive workflow.
rigor_connections Manage MCP connections.

Integration Patterns

Plan-then-Execute (Recommended)

Plan first to see the step sequence and cost estimate. Execute only when satisfied.

# Step 1: Plan (free)
POST /api/rigor/plan
  { "task_description": "...", "preferences": { "rigor_level": "standard" } }
  → { plan: { workflow_id: "wr_...", sequence: [...], cost: { credits: 540 } } }

# Step 2: Review plan, then execute with polling delivery
POST /api/rigor/execute
  { "workflow_id": "wr_...", "delivery": { "method": "polling" } }
  → { workflow_id: "wr_...", status: "queued", poll_url: "/api/rigor/status/wr_..." }

# Step 3: Poll until status is "completed"
GET /api/rigor/status/wr_...
  → { workflow: { status: "completed", deliverable: { ... } } }

Webhook Delivery

Receive results pushed to your endpoint instead of polling.

POST /api/rigor/execute
  {
    "workflow_id": "wr_...",
    "delivery": {
      "method": "webhook",
      "webhook_url": "https://your-server.example.com/rigor-webhook"
    }
  }
  → { webhook_url: "...", webhook_status: "registered", webhook_secret: "..." }

Store webhook_secret and use it to verify incoming payloads via HMAC-SHA256.

Webhook event types: step_completed (per step), workflow_completed (terminal success), workflow_completed_partial (terminal partial success — the deliverable was salvaged but a final review or validation step did not run; payload includes quality_score, total_credits, steps_completed, total_steps, and incomplete_steps), workflow_failed, timed_out.


Approval Gate

Request a human decision before final steps execute.

# Plan with approval gate
POST /api/rigor/plan
  { "task_description": "...", "preferences": { "require_approval": true } }

# Execute — workflow will pause at pending_approval with a partial deliverable
POST /api/rigor/execute
  { "workflow_id": "wr_...", "delivery": { "method": "polling" } }

# Poll until status === "pending_approval"
GET /api/rigor/status/wr_...

# Approve or reject
POST /api/rigor/approve
  { "workflow_id": "wr_...", "decision": "approved", "notes": "Looks good" }
  # workflow automatically resumes after approval

Interactive Mode (Mode C)

Drive execution one step at a time.

# Execute in interactive mode
POST /api/rigor/execute
  { "task_description": "...", "mode": "interactive" }
  → { status: "interactive_waiting", sequence: [...] }

# Drive each step
POST /api/rigor/step
  { "workflow_id": "wr_...", "action": "next" }
  → { status: "step_executing", poll_url: "..." }

# Poll until step completes (status returns to "interactive_waiting")
GET /api/rigor/status/wr_...

# Skip a step
POST /api/rigor/step
  { "workflow_id": "wr_...", "action": "skip" }

# Abort early
POST /api/rigor/step
  { "workflow_id": "wr_...", "action": "abort" }

Interactive sessions expire after 30 minutes of inactivity.


Workflow Chaining

Inherit context from a completed workflow into a new one.

# First workflow completes
GET /api/rigor/status/wr_parent...
  → { status: "completed" }

# Chain a new workflow that inherits prior context
POST /api/rigor/execute
  {
    "task_description": "Implement the solution from the prior design",
    "prior_workflow_id": "wr_parent...",
    "context_fields": ["deliverable", "quality_review"]
  }

Chain depth limit: 10 links.


Worked Example

A complete plan → execute → poll cycle for a Solution Design.

1. Plan

curl -X POST https://plith.ai/api/rigor/plan -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"task_description\": \"Design a caching layer for our API\"}"

Response:

{
  "ok": true,
  "plan": {
    "workflow_id": "wr_2a4b6c8d1f3e5a7b9c0d1e2f",
    "classification": {
      "task_type": "solution_design",
      "value_class": "operational"
    },
    "sequence": [
      { "step": 1, "name": "Requirements Analysis", "estimated_credits": 80, "priority_class": "must" },
      { "step": 2, "name": "Research", "estimated_credits": 120, "priority_class": "should" },
      { "step": 3, "name": "Solution Design", "estimated_credits": 240, "priority_class": "must" },
      { "step": 4, "name": "Multi-Perspective Review", "estimated_credits": 120, "priority_class": "must" }
    ],
    "cost": {
      "credits": 560,
      "usd": 0.56,
      "range": { "estimated_min": 420, "estimated_max": 700 }
    },
    "alternatives": {
      "quick": {
        "steps": 3,
        "cost": { "credits": 280, "range": { "estimated_min": 210, "estimated_max": 350 } },
        "skips": ["Research"],
        "review_model": "Haiku",
        "includes_verification": false,
        "tradeoff": "Fewer steps, skips verification."
      },
      "thorough": {
        "steps": 6,
        "cost": { "credits": 920, "range": { "estimated_min": 690, "estimated_max": 1150 } },
        "adds": ["Assumption Verification", "Gap Analysis"],
        "review_model": "Sonnet + Opus",
        "includes_verification": true,
        "tradeoff": "More verification steps and multi-model review."
      }
    },
    "info": ["Review: Haiku + Sonnet"],
    "warnings": [],
    "rigor_level": "standard"
  },
  "generated_title": "Caching Layer Design — API Gateway"
}

2. Execute (polling delivery)

curl -X POST https://plith.ai/api/rigor/execute -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"workflow_id\": \"wr_2a4b6c8d1f3e5a7b9c0d1e2f\", \"delivery\": {\"method\": \"polling\"}}"

Response:

{
  "ok": true,
  "workflow_id": "wr_2a4b6c8d1f3e5a7b9c0d1e2f",
  "status": "queued",
  "task_type": "solution_design",
  "delivery_mode": "polling",
  "poll_url": "/api/rigor/status/wr_2a4b6c8d1f3e5a7b9c0d1e2f"
}

3. Poll status

curl https://plith.ai/api/rigor/status/wr_2a4b6c8d1f3e5a7b9c0d1e2f -H "x-api-key: YOUR_KEY"

Response when complete:

{
  "ok": true,
  "workflow": {
    "workflow_id": "wr_2a4b6c8d1f3e5a7b9c0d1e2f",
    "status": "completed",
    "task_type": "solution_design",
    "value_class": "operational",
    "current_step": 4,
    "total_steps": 4,
    "actual_steps_executed": 4,
    "completed_step_summaries": [
      { "step": 1, "tool": "Requirements Analysis", "summary": "Captured 7 functional and 4 non-functional requirements." },
      { "step": 2, "tool": "Research", "summary": "Compared Redis, Memcached, Hazelcast for the gateway use case." },
      { "step": 3, "tool": "Solution Design", "summary": "Two-tier cache with Redis primary and in-process LRU." },
      { "step": 4, "tool": "Multi-Perspective Review", "summary": "Score 92 — minor gaps on observability." }
    ],
    "quality_score": 92,
    "quality_review": {
      "score": 92,
      "summary": "Strong design with clear trade-offs. Add concrete latency targets and observability hooks to reach 100.",
      "path_to_100": [
        "Add p50/p95/p99 latency targets per cache tier.",
        "Specify observability metrics emitted by the Solution Design hook points."
      ]
    },
    "deliverable": {
      "title": "Solution Design — Rigor Workflow Output",
      "sections": [
        { "heading": "Requirements Analysis", "content": "..." },
        { "heading": "Research", "content": "..." },
        { "heading": "Solution Design", "content": "..." },
        { "heading": "Multi-Perspective Review", "content": "..." }
      ]
    },
    "credits_used": 545,
    "error": null
  }
}

Direct execution

By default, Rigor executes each plan step as a separate LLM call. Intermediate outputs are available via SSE events and GET /api/rigor/status as execution progresses.

Set preferences.execution: "direct" to compose the plan's content frameworks into a single LLM call and route cost-first, against a per-task-type model floor that moves when a cheaper model earns the work.

It is not a one-call-per-workflow guarantee, and for atomic work it does not reduce the call count at all. Research steps, process steps (CLASSIFICATION-VERIFY, REVIEW-PROTOCOL, SYNTHESIS) and the Phase D quality review each remain their own call. An atomic task type has a single content framework, so there is nothing to compose: measured across every atomic workflow to date, direct and standard execution both issue 2 logged step calls. The saving on atomic work is the model, not the call count. Supplying an output_contract replaces the quality-review call with deterministic validation — that is 1 fewer call, and the only one.

It is built for atomic work — classification, scoring, ranking, entity extraction, and query parsing. It costs less, because it makes fewer calls and routes cost-first, and it is deterministic to consume when you declare an output_contract. It is not faster in wall-clock terms for a large plan; the saving is in calls and model price, not latency.

Rigor may select it for you. For atomic task types (tag, score, rerank, extract_entities, parse_query, quick_research, quick_classification, quick_extraction, quick_scoring), direct execution is auto-selected when you express no execution preference and request no approval gate. An explicit choice is never overridden, in either direction. GET /api/rigor/task-types returns each type's shapeatomic or workflow — so you can tell which types this applies to.

Applied the same as standard execution

delivery.method (all 3), rigor_level (all 3), skip_frameworks / only_frameworks / add_frameworks / max_budget_usd, language, MCP connections, the bounded review-protocol loop, independent quality review and rigor_qualval_score, references and citations, step-output transparency, workspace LLM policy, credits and workspace budgets, idempotency, and refund plus partial-result salvage on failure.

Not eligible for direct execution

The standard executor applies these; direct-executor.ts has no call site for either. Rather than run and drop them, a request carrying either is excluded from direct-mode eligibility and runs as standard execution, so the context is applied:

Request What happens
Attachments and saved artifacts The workflow runs as standard execution, which builds the attachment manifest.
prior_workflow_id / context_fields chaining The workflow runs as standard execution, which reads inherited_context.

You are told which applied: the response carries a direct_execution_not_eligible warning per reason, and execution_fallback: true if you asked for direct execution explicitly. No action is needed — the context reached the model. This gate matters because direct execution is auto-selected for atomic task types, so a caller can land there without choosing it. If your call carries an attachment or chains off a prior workflow, keep it on standard execution until this is fixed. Tracked as B.3012.

The rest of the contract

  • Available at every tier. It's a cost/latency optimization, not a paid perk — the same policy and quality-review guarantees apply regardless of tier.
  • Is subject to workflow size limits. When the workflow exceeds the composition limit, execution falls back to default multi-call mode automatically — execution_fallback: true appears in the plan response.
  • The composition-limit check also runs again once execution actually starts (a runtime-only signal — e.g. actual research/content framework mix — can differ from what was estimated at plan time). If it trips at that point, the workflow falls back to the standard worker loop transparently: execution_mode on the workflow row reflects what ran (never "direct" for a workflow that fell back), and execution_fallback: true is surfaced both as a GET /api/rigor/execute SSE execution_fallback event and on GET /api/rigor/status/{workflow_id}.
  • Is not compatible with require_approval: true or interactive mode (mode: "interactive"). See below.
  • Honors delivery.method exactly like default execution — SSE, polling, and webhook are all supported. Polling is often the simpler integration choice, but SSE is fully supported if requested.

Incompatibility with approval gates: Direct execution cannot be combined with require_approval: true or interactive mode (mode: "interactive"). If both are requested, the system honors the approval preference and falls back to default multi-call execution. The response will include execution_fallback: true.

This is because direct execution composes its content frameworks into a single call and surfaces no intermediate outputs — there are no step boundaries to pause at for approval.

When allowed_modes in the plan response does not contain "direct", the workflow is not eligible for direct execution regardless of the preferences.execution field.


Error Reference

Error envelope

All endpoints return errors in this shape:

{
  "error": {
    "code": "missing_field",
    "message": "workflow_id is required.",
    "request_id": "req_abc123def456...",
    "fix": "POST /api/rigor/plan with your task_description",
    "docs": "https://plith.ai/docs/errors#missing_field",
    "details": { "field": "workflow_id" }
  }
}

fix, docs, and details are included only when available. request_id is always present — use it when contacting support.

Error codes by category

Auth

Code HTTP Message (summary) Fix
missing_api_key 401 Provide your Plith API key via the x-api-key header. POST /api/keys
invalid_api_key 401 API key not found or revoked. GET /api/keys/verify
unauthorized 401 Authentication required. POST /api/keys
forbidden 403 This resource belongs to a different organization.

Validation

Code HTTP Message (summary) Fix
invalid_body 400 Request body must be valid JSON.
missing_field 400 <field> is required.
invalid_field 400 Invalid value for <field>: <reason>.
invalid_task_description 400 Must contain at least 2 words. Provide a clear description.
vague_task_description 400 Could not confidently classify this task. Add more detail about the specific task, system, or deliverable.
input_too_long 400 Input exceeds maximum length.

Rate limiting

Code HTTP Message (summary) Fix
rate_limit_exceeded 429 Rate limit exceeded. Retry in <n> seconds. Wait <n> seconds; check Retry-After header.
concurrent_limit_exceeded 429 You have N active workflows (limit: N). Wait for a workflow to complete; poll GET /api/rigor/status/{id}.

Workflow state

Code HTTP Message (summary) Fix
not_found 404 Workflow not found.
no_plan 400 Workflow has no plan. POST /api/rigor/plan first.
insufficient_credits 402 Insufficient credits. POST /api/billing/checkout.
workspace_budget_exceeded 402 Workflow cost would exceed workspace budget. Adjust budget via PUT /api/workspaces/{id}/budget or reduce plan.
workflow_has_progress 409 Workflow has completed steps; use /resume. POST /api/rigor/resume.
invalid_status 409 Cannot perform this action on workflow with status "<current>".
duplicate_task 409 Identical task submitted in the last 5 minutes. GET /api/rigor/status/<existing_workflow_id>.
already_complete 409 All steps have results; nothing to resume.
resume_conflict 409 Another resume is already in progress. Wait and check status.
pending_human_review 409 Workflow is pending human review. POST /api/rigor/approve.

Workflow chaining / lineage

Code HTTP Message (summary) Fix
prior_not_found 404 Prior workflow not found.
prior_not_terminal 409 Prior workflow must be in a terminal state. Wait for it to complete.
chain_depth_exceeded 422 Maximum workflow chain depth (10) reached.
context_too_large 422 Inherited context exceeds maximum allowed size. Use context_fields to restrict inheritance.

Webhook

Code HTTP Message (summary) Fix
webhook_url_required 400 webhook_url is required for webhook delivery.
webhook_url_https 400 webhook_url must use HTTPS.
webhook_url_invalid 400 webhook_url is not a valid URL.

Interactive / step

Code HTTP Message (summary) Fix
session_expired 410 Interactive session expired (30-min TTL). POST /api/rigor/resume to restart.
concurrent_step 409 Another step execution is in progress. Retry in 2–3 seconds.
invalid_action 400 action must be next, skip, or abort.

Connections

Code HTTP Message (summary) Fix
feature_not_enabled 501 MCP connections feature is not enabled.

Bulk workflow management

Code HTTP Message (summary) Fix
no_workflow_ids 400 workflow_ids array is required and must not be empty. Provide an array of workflow ID strings.
bulk_limit_exceeded 400 workflow_ids exceeds 20. Split into batches of 20.

Planning

Code HTTP Message (summary) Fix
plan_timeout 504 Planning exceeded the time limit. Simplify or split the task description.

LLM routing

Code HTTP Message (summary) Fix
provider_exhausted 503 All configured LLM providers failed for this step. Check provider status dashboards; contact support if persistent.
fallback_triggered 200 Primary LLM provider failed; step completed using fallback provider. Informational — no action needed.

GDPR / data deletion

Code HTTP Message (summary) Fix
deletion_already_pending 409 A deletion request is already pending for this org.
deletion_rate_limited 429 Deletion requests are limited to 5 per org per day. Retry after midnight UTC.
invalid_deletion_request 404 Deletion request not found, already processed, or confirmation_token does not match.
deletion_failed 500 Deletion partially executed. Contact support with your request_id.

Server errors

Code HTTP Message (summary) Fix
internal_error 500 An internal error occurred. Retry; include request_id when contacting support.
queue_failed 500 Failed to queue workflow for execution. Retry.
approve_failed 500 Failed to process approval decision. Retry.

Response Conventions

Every successful response includes these metadata fields:

Field Type Description
request_id string Unique request identifier — always req_ followed by 24 hex characters. Include this when contacting support.
credits_used number Credits deducted by this request. 0 for free endpoints.
credits_remaining number Your organization's balance after this request.
fallback_behavior string What the system did when an upstream dependency was degraded — e.g. "none" when everything worked normally.

Note: GET /api/keys/verify always reports credits_remaining: 0 — it does not query the live balance. Use GET /api/billing/balance for the actual balance.

Rigor workflow endpoints (/plan, /execute, /status, /approve, /resume, /step) use product-specific response shapes and do not include credits_used/credits_remaining at the top level. Credit usage for workflows is reported via the status endpoint as workflow.credits_used.


Plith — Platform API Reference

Authentication, API keys, billing, workspaces, notifications, and GDPR endpoints.

Base URL: https://plith.ai

Rigor workflow endpoints are documented in EXTERNAL-RIGOR-API-REFERENCE.md.


Table of Contents


Authentication

Plith supports two authentication mechanisms:

  1. API key (x-api-key header) — for programmatic access and all Rigor endpoints.
  2. Session cookie (Supabase Auth) — for browser-based flows and the dashboard.

Most API consumers use API keys. Session-based endpoints exist for signup, login, and OAuth flows.

Do NOT use Authorization: Bearer format — that is not supported for API authentication.


POST /api/auth/signup

Auth: None Cost: Free

Creates a user account and returns a session. Sets a session cookie on success.

Request body:

Field Type Required Description
email string Yes User email
password string Yes Minimum 8 characters
name string No Display name

Response (201):

{
  "user_id": "uuid",
  "email": "user@example.com",
  "session": {
    "access_token": "...",
    "refresh_token": "...",
    "expires_at": 1234567890
  }
}

Errors:

  • 400 invalid_body — missing required fields.
  • 409 email_already_registered — email already exists.
  • 500 signup_failed

POST /api/auth/login

Auth: None Cost: Free

Authenticates with email and password. Sets a session cookie.

Request body:

Field Type Required Description
email string Yes Registered email
password string Yes Account password

Response (200):

{
  "user_id": "uuid",
  "email": "user@example.com",
  "session": {
    "access_token": "...",
    "refresh_token": "...",
    "expires_at": 1234567890
  }
}

Errors:

  • 401 invalid_credentials — email/password mismatch.
  • 400 invalid_body — missing required fields.

POST /api/auth/logout

Auth: Session cookie Cost: Free

Signs out the current user and clears the session cookie.

Response (200): { "success": true }


GET /api/auth/callback

Auth: None (OAuth / magic-link callback) Cost: Free

Exchanges an authorization code from an OAuth or magic-link flow for a session. Redirects to the app on success.

Query parameters:

Parameter Description
code Auth code
next Optional redirect path after login (default: /dashboard)

Response: HTTP 302 redirect.

Errors:

  • 400 — missing or invalid code.

POST /api/auth/mcp/initiate

Auth: Required (x-api-key) — minimum role: admin Cost: Free

Starts an OAuth flow for an MCP provider (e.g. GitHub). Returns an authorization URL to redirect the user to.

Request body:

Field Type Required Description
provider string Yes Provider identifier (e.g. "github").
permission_mode "read" | "read_write" No Scope of access. Default "read".

Response (200):

{
  "authorization_url": "https://github.com/login/oauth/authorize?..."
}

Errors:

  • 401 unauthorized
  • 403 forbidden — requires admin role.
  • 400provider is required or not available for OAuth.
  • 400 — invalid permission_mode.

curl (Windows):

curl -X POST https://plith.ai/api/auth/mcp/initiate -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"provider\": \"github\", \"permission_mode\": \"read\"}"

GET /api/auth/mcp/callback

Auth: None (public — server-side redirect) Cost: Free

OAuth callback for MCP provider authentication. After the user authorizes the provider, this endpoint receives the code, stores the token, and redirects the user back to the app. You do not call this directly.


Health

GET /api/health

Auth: None — public endpoint. Cost: Free

Returns service availability and product health. Use this endpoint to check whether the platform is operational before making API calls.

Response (200):

{
  "status": "ok",
  "timestamp": "2026-05-18T12:00:00.000Z",
  "services": {
    "database": "ok",
    "cache": "ok"
  },
  "products": {
    "dedupq": "ok",
    "burnrate": "ok",
    "qualitygate": "ok",
    "guardrail": "ok",
    "pitfalldb": "ok",
    "rigor": "ok"
  },
  "version": "2026-05-18"
}

curl (Windows):

curl https://plith.ai/api/health

API Keys

POST /api/keys

Auth: None — public endpoint. Cost: Free Rate limit: 3 keys per IP per 24 hours.

Creates a new organization and API key pair. The raw key is returned once and never stored in plaintext.

Request body:

Field Type Required Description
email string No Contact email for the org
name string No Display name (defaults to email, then "Unnamed org")

Response (201):

{
  "org_id": "a1b2c3d4-...",
  "key": "plth_live_xxxxxxxxxxxx...",
  "prefix": "plth_live_xxx",
  "message": "Store this key securely — it will not be shown again.",
  "quickstart": "https://plith.ai/docs/quickstart",
  "docs": "https://plith.ai/docs/authentication"
}

Errors:

  • 429 rate_limited — IP exceeded 3 keys per 24 hours. Response includes retry_after_seconds: 86400.
  • 409 duplicate_email — org with that email already exists.
  • 500 db_error

curl (Windows):

curl -X POST https://plith.ai/api/keys -H "Content-Type: application/json" -d "{\"email\": \"you@example.com\", \"name\": \"My Org\"}"

GET /api/keys/verify

Auth: Required (x-api-key) Cost: Free

Confirms an API key is valid and returns the associated org context.

Response (200):

{
  "org_id": "a1b2c3d4-...",
  "is_paid": true,
  "request_id": "req_abc123def456...",
  "credits_used": 0,
  "credits_remaining": 0,
  "fallback_behavior": "none"
}

credits_remaining is always 0 on this endpoint — it does not query the live balance. Use GET /api/billing/balance for your actual balance.

Errors:

  • 401 unauthorized — missing or invalid key.

curl (Windows):

curl https://plith.ai/api/keys/verify -H "x-api-key: YOUR_KEY"

User

GET /api/user/me

Auth: Session cookie or x-api-key Cost: Free

Returns the authenticated user's profile and workspace memberships.

Response (200):

{
  "user_id": "uuid",
  "email": "user@example.com",
  "display_name": "Alice",
  "memberships": [
    { "workspace_id": "uuid", "workspace_name": "Acme Corp", "role": "admin" }
  ]
}

Errors:

  • 401 unauthorized — no valid session or API key.

curl (Windows):

curl https://plith.ai/api/user/me -H "x-api-key: YOUR_KEY"

Billing

GET /api/billing/balance

Auth: Required (x-api-key) Cost: Free

Returns the current credit balance for the authenticated organization.

Response (200):

{
  "balance": 9484,
  "org_id": "a1b2c3d4-...",
  "request_id": "req_abc123def456...",
  "credits_used": 0,
  "credits_remaining": 9484,
  "fallback_behavior": "none"
}

Errors:

  • 401 unauthorized
  • 500 internal_error

curl (Windows):

curl https://plith.ai/api/billing/balance -H "x-api-key: YOUR_KEY"

POST /api/billing/checkout

Auth: Required (x-api-key) Cost: Free

Creates a Stripe Checkout session for purchasing credits. Exactly one of pack or custom_amount must be provided.

Preset pack request:

Field Type Required Description
pack number Yes One of 10, 25, 100, 250 (USD price).

Custom amount request:

Field Type Required Description
custom_amount integer Yes USD amount, integer between 5 and 10,000 inclusive.

Both options award 1,000 credits per dollar.

Response (200):

{
  "url": "https://checkout.stripe.com/pay/cs_live_..."
}

Redirect the user to this URL to complete payment.

Errors:

  • 400 invalid_body — both or neither of pack / custom_amount provided.
  • 400 invalid_pack — not in [10, 25, 100, 250]. Response includes available.
  • 400 invalid_custom_amount — not an integer or outside [5, 10000]. Response includes min and max.
  • 401 missing_api_key
  • 503 billing_unavailable — Stripe not configured.

curl (Windows) — preset pack:

curl -X POST https://plith.ai/api/billing/checkout -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"pack\": 25}"

curl (Windows) — custom amount:

curl -X POST https://plith.ai/api/billing/checkout -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"custom_amount\": 50}"

POST /api/billing/subscribe

Auth: x-api-key or session Cost: Free

Creates a Stripe Checkout session for a recurring subscription.

Request body:

Field Type Required Description
tier string Yes One of pro, team, enterprise.
interval string No monthly (default) or annual.

Response (200): { "url": "https://checkout.stripe.com/pay/cs_live_..." }

Errors:

  • 400 invalid_tier
  • 400 invalid_interval
  • 409 already_subscribed — org already has an active subscription; use POST /api/billing/portal to manage it.
  • 503 billing_unavailable

curl (Windows):

curl -X POST https://plith.ai/api/billing/subscribe -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"tier\": \"team\", \"interval\": \"monthly\"}"

POST /api/billing/portal

Auth: x-api-key or session Cost: Free

Creates a Stripe Customer Portal session to manage your subscription (cancel, upgrade, update payment method).

Response (200): { "url": "https://billing.stripe.com/session/..." }

Redirect the user to this URL.

Errors:

  • 400 no_billing_account — no billing account found; purchase credits or a subscription first.
  • 401 unauthorized
  • 503 billing_unavailable

curl (Windows):

curl -X POST https://plith.ai/api/billing/portal -H "x-api-key: YOUR_KEY"

GET /api/billing/auto-reload

Auth: Required (x-api-key) Cost: Free

Returns the current auto-reload configuration for your organization.

Response (200):

{
  "enabled": true,
  "threshold": 500,
  "amount": 5000,
  "monthly_limit": 100,
  "monthly_spent": 45,
  "reset_at": "2026-06-01T00:00:00Z"
}
Field Description
enabled Whether auto-reload is active.
threshold Credit balance that triggers a reload. null if not set.
amount Credits added per reload. null if not set.
monthly_limit Monthly USD cap on auto-reloads. null if not set.
monthly_spent USD spent on auto-reloads this month.
reset_at When monthly_spent resets. null if not set.

curl (Windows):

curl https://plith.ai/api/billing/auto-reload -H "x-api-key: YOUR_KEY"

PUT /api/billing/auto-reload

Auth: Required (x-api-key) — minimum role: admin Cost: Free

Configures automatic credit reloads. When your balance drops below threshold, a Stripe charge for amount credits is triggered automatically. Requires a saved payment method from a prior purchase.

Request body:

Field Type Required Description
enabled boolean Yes Enable or disable auto-reload.
confirm boolean When enabling true required to acknowledge the automatic charge authorization.
threshold integer When enabling Balance floor that triggers a reload. Range: 100–100,000 credits.
amount integer When enabling Credits to add per reload. One of: 5,000 / 25,000 / 100,000 / 250,000.
monthly_limit integer No Monthly USD cap on auto-reloads. Range: 5–10,000.

Response (200): Updated auto-reload configuration (same shape as GET).

Errors:

  • 401 unauthorized
  • 403 forbidden — requires admin role.
  • 400 invalid_body — enabled=true with missing or invalid fields.

curl (Windows) — enable:

curl -X PUT https://plith.ai/api/billing/auto-reload -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"enabled\": true, \"confirm\": true, \"threshold\": 500, \"amount\": 5000}"

curl (Windows) — disable:

curl -X PUT https://plith.ai/api/billing/auto-reload -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"enabled\": false}"

POST /api/billing/webhook

Auth: Stripe-Signature header (do not call this endpoint directly) Cost: Free

Stripe webhook receiver. Called by Stripe to notify your organization of subscription lifecycle events. This endpoint verifies the Stripe signature and updates your subscription status.

Handled events:

Event Action
customer.subscription.created Activates subscription, sets tier.
customer.subscription.updated Updates tier and status.
customer.subscription.deleted Sets status to cancelled, reverts tier to free.
invoice.paid Confirms subscription active.
invoice.payment_failed Sets status to past_due.

Response (200): { "received": true }

Errors:

  • 400 invalid_signature — HMAC verification failed.

Workspaces

All workspace endpoints require the caller to be a member of the specified workspace. Role requirements are noted per endpoint.

Roles (from lowest to highest): viewermemberadminowner.


GET /api/workspaces

Auth: Required (x-api-key or session) Cost: Free

Lists all workspaces for your organization.

Query parameters:

Parameter Description
include_archived true to include archived workspaces. Default false.

Response (200):

{
  "workspaces": [
    {
      "id": "uuid",
      "name": "Default",
      "slug": "default",
      "is_default": true,
      "archived_at": null,
      "created_at": "2026-05-01T00:00:00Z"
    }
  ]
}

curl (Windows):

curl https://plith.ai/api/workspaces -H "x-api-key: YOUR_KEY"

POST /api/workspaces

Auth: Required (x-api-key) — Team or Enterprise only. Cost: Free

Creates a new workspace.

Request body:

Field Type Required Description
name string Yes Workspace display name.

Response (201): Full workspace object.

Errors:

  • 403 tier_required — Team or Enterprise subscription required.
  • 403 workspace_limit_reached — tier workspace limit reached.
  • 400 missing_fieldname required.

curl (Windows):

curl -X POST https://plith.ai/api/workspaces -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"name\": \"My New Workspace\"}"

GET /api/workspaces/{id}

Auth: Required (x-api-key or session, any role) Cost: Free

Returns workspace details including LLM policy status and budget information (when configured).

Response (200):

{
  "id": "uuid",
  "name": "Default",
  "slug": "default",
  "is_default": true,
  "archived_at": null,
  "created_at": "2026-05-01T00:00:00Z",
  "llm_policy": null,
  "enforcement_status": null,
  "account_tier": "team",
  "budget": {
    "monthly_credit_budget": 50000,
    "monthly_credits_used": 12400,
    "budget_remaining": 37600,
    "budget_reset_at": "2026-06-01T00:00:00Z"
  }
}

budget is only present when a budget is configured and the tier supports it. llm_policy is null when no policy is set.

Errors:

  • 401 unauthorized
  • 404 not_found

curl (Windows):

curl https://plith.ai/api/workspaces/uuid -H "x-api-key: YOUR_KEY"

PUT /api/workspaces/{id}

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free

Updates workspace settings.

Request body: Send only the fields you want to change.

Field Type Description
name string New workspace display name.

Response (200): Updated workspace object.

Errors:

  • 401 unauthorized
  • 403 forbidden — requires admin role.
  • 404 not_found

curl (Windows):

curl -X PUT https://plith.ai/api/workspaces/uuid -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"name\": \"Renamed Workspace\"}"

DELETE /api/workspaces/{id}

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free

Archives the workspace (soft delete). Archived workspaces are not deleted; they can be retrieved with include_archived=true.

Response (200): { "success": true }

Errors:

  • 401 unauthorized
  • 403 forbidden — requires admin role.
  • 404 not_found

curl (Windows):

curl -X DELETE https://plith.ai/api/workspaces/uuid -H "x-api-key: YOUR_KEY"

GET /api/workspaces/{id}/members

Auth: x-api-key or session (any role) Cost: Free

Lists all members of the workspace, including API key members.

Response (200):

{
  "members": [
    {
      "id": "uuid",
      "type": "user",
      "email": "alice@example.com",
      "name": "Alice",
      "role": "owner",
      "created_at": "2026-05-01T00:00:00Z"
    },
    {
      "id": "uuid",
      "type": "api_key",
      "key_prefix": "plth_live_xxx",
      "role": "member",
      "created_at": "2026-05-02T00:00:00Z"
    }
  ]
}

curl (Windows):

curl https://plith.ai/api/workspaces/uuid/members -H "x-api-key: YOUR_KEY"

POST /api/workspaces/{id}/members

Auth: x-api-key or session — requires admin or owner Cost: Free

Invites a user to the workspace. If the user exists, they are added directly. For new emails, an invite token is created and an email is sent.

Request body:

Field Type Required Description
email string Yes Email of person to invite.
role string Yes One of viewer, member, admin.

Response (201): { "invite_id": "...", "email": "...", "role": "...", "expires_at": "..." }

Errors:

  • 403 forbidden — caller is not admin or owner.
  • 409 already_member — user is already a member.
  • 402 member_limit_reached — tier member limit reached.

curl (Windows):

curl -X POST https://plith.ai/api/workspaces/uuid/members -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"email\": \"bob@example.com\", \"role\": \"member\"}"

PATCH /api/workspaces/{id}/members/{memberId}

Auth: x-api-key or session — requires owner Cost: Free

Updates a workspace member's role. Owners cannot demote their own role.

Request body:

Field Type Required Description
role string Yes New role: viewer, member, or admin.

Response (200): { "member_id": "...", "role": "..." }

Errors:

  • 403 forbidden — requires owner role.
  • 404 not_found
  • 400 cannot_demote_self

curl (Windows):

curl -X PATCH https://plith.ai/api/workspaces/uuid/members/member-uuid -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"role\": \"viewer\"}"

DELETE /api/workspaces/{id}/members/{memberId}

Auth: x-api-key or session — requires admin or owner Cost: Free

Removes a member from the workspace.

Response (200): { "success": true }

Errors:

  • 403 forbidden
  • 404 not_found
  • 400 cannot_remove_last_owner — workspace must retain at least one owner.

curl (Windows):

curl -X DELETE https://plith.ai/api/workspaces/uuid/members/member-uuid -H "x-api-key: YOUR_KEY"

POST /api/invites/accept

Auth: Session cookie (must be logged in) Cost: Free

Accepts a workspace invite by token. The token is included in the invite email.

Request body:

Field Type Required Description
token string Yes Invite token from the invite email.

Response (200): { "workspace_id": "...", "workspace_name": "...", "role": "..." }

Errors:

  • 404 invite_not_found — token not found or already used.
  • 410 invite_expired — invite has expired (default TTL: 7 days).
  • 401 unauthorized — must be logged in to accept.

GET /api/workspaces/{id}/api-keys

Auth: x-api-key or session — requires member or above Cost: Free

Lists all API keys scoped to the workspace. Raw keys are never returned after creation — only prefixes.

Response (200):

{
  "api_keys": [
    {
      "key_id": "uuid",
      "prefix": "plth_live_xxx",
      "label": "Production Key",
      "created_at": "2026-05-01T00:00:00Z",
      "last_used_at": "2026-05-10T00:00:00Z",
      "created_by": "uuid"
    }
  ]
}

curl (Windows):

curl https://plith.ai/api/workspaces/uuid/api-keys -H "x-api-key: YOUR_KEY"

POST /api/workspaces/{id}/api-keys

Auth: x-api-key or session — requires admin or owner Cost: Free

Creates a new workspace-scoped API key. The raw key is returned once.

Request body:

Field Type Required Description
label string No Human-readable label for the key.

Response (201): { "key_id": "...", "key": "plth_live_...", "prefix": "...", "label": "...", "created_at": "..." }

Errors:

  • 403 forbidden — requires admin or owner.
  • 402 key_limit_reached — tier key limit reached.

curl (Windows):

curl -X POST https://plith.ai/api/workspaces/uuid/api-keys -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"label\": \"Production Key\"}"

DELETE /api/workspaces/{id}/api-keys/{keyId}

Auth: x-api-key or session — requires admin or owner Cost: Free

Revokes a workspace API key. The key is invalidated immediately.

Response (200): { "success": true }

Errors:

  • 403 forbidden
  • 404 not_found

curl (Windows):

curl -X DELETE https://plith.ai/api/workspaces/uuid/api-keys/key-uuid -H "x-api-key: YOUR_KEY"

GET/PUT /api/workspaces/{id}/llm-policy

Auth: Required — Team/Enterprise only. GET requires any role; PUT requires admin or above. Cost: Free

Get or update the workspace's allowed-model LLM policy. This controls which AI models Rigor workflows in this workspace may use.

GET response (200):

{
  "workspace_id": "uuid",
  "allowed_models": ["claude-sonnet-4-6", "claude-haiku-4-5"],
  "enforcement_status": "active",
  "updated_at": "2026-05-01T00:00:00Z"
}

PUT request body:

Field Type Required Description
allowed_models string[] Yes List of allowed model identifiers. Pass null to remove the policy.

PUT response (200): Updated policy object.

Errors:

  • 403 tier_required — Team or Enterprise subscription required.
  • 403 forbiddenPUT requires admin role.
  • 404 not_found

curl (Windows) — GET:

curl https://plith.ai/api/workspaces/uuid/llm-policy -H "x-api-key: YOUR_KEY"

curl (Windows) — PUT:

curl -X PUT https://plith.ai/api/workspaces/uuid/llm-policy -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"allowed_models\": [\"claude-sonnet-4-6\"]}"

GET /api/workspaces/{id}/budget

Auth: x-api-key or session (any role) Cost: Free

Returns the workspace's monthly credit budget and current-month spend.

Response (200):

{
  "workspace_id": "uuid",
  "monthly_credit_budget": 50000,
  "current_month_spend": 12400,
  "budget_utilization_pct": 24.8,
  "reset_at": "2026-06-01T00:00:00Z"
}

monthly_credit_budget: null means no budget limit is set.

curl (Windows):

curl https://plith.ai/api/workspaces/uuid/budget -H "x-api-key: YOUR_KEY"

PUT /api/workspaces/{id}/budget

Auth: x-api-key or session — requires admin or owner Cost: Free Tier: Team or Enterprise only.

Sets the workspace's monthly credit budget soft limit. When a workflow's estimated cost would exceed the remaining budget, the workflow is blocked with 402 workspace_budget_exceeded.

Request body:

Field Type Required Description
monthly_credit_budget integer | null Yes New budget in credits, or null to remove the limit.

Response (200): { "workspace_id": "...", "monthly_credit_budget": 50000 }

Errors:

  • 403 forbidden — requires admin or owner.
  • 403 tier_required — Team or Enterprise subscription required.
  • 400 invalid_body — budget must be a positive integer or null.

curl (Windows):

curl -X PUT https://plith.ai/api/workspaces/uuid/budget -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"monthly_credit_budget\": 50000}"

GET /api/workspaces/{id}/folders

Auth: Required (x-api-key or session, any role) Cost: Free

Lists workflow folders for the workspace.

Response (200):

{
  "folders": [
    { "id": "uuid", "name": "Engineering", "created_at": "2026-05-01T00:00:00Z" }
  ]
}

curl (Windows):

curl https://plith.ai/api/workspaces/uuid/folders -H "x-api-key: YOUR_KEY"

GET /api/workspaces/{id}/audit-log

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free

Returns the audit log for the workspace — a record of administrative actions (key creation, member changes, policy updates, etc.).

Response (200):

{
  "events": [
    {
      "id": "uuid",
      "action": "workspace.member.added",
      "actor": "alice@example.com",
      "resource_type": "member",
      "resource_id": "uuid",
      "metadata": { "role": "admin" },
      "created_at": "2026-05-01T00:00:00Z"
    }
  ]
}

Errors:

  • 401 unauthorized
  • 403 forbidden — requires admin role.

curl (Windows):

curl https://plith.ai/api/workspaces/uuid/audit-log -H "x-api-key: YOUR_KEY"

GET /api/workspaces/{id}/webhooks

Auth: Required (x-api-key or session) — minimum role: member Cost: Free Tier: Pro or above.

Lists workflow event webhooks registered for this workspace.

Response (200):

{
  "webhooks": [
    {
      "id": "uuid",
      "url": "https://your-server.example.com/hook",
      "description": "Production notifier",
      "events": ["workflow_completed", "workflow_failed"],
      "active": true,
      "created_at": "2026-05-01T00:00:00Z"
    }
  ]
}

POST /api/workspaces/{id}/webhooks

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free Tier: Pro or above.

Registers a new webhook endpoint. The secret is shown once — store it securely for HMAC-SHA256 payload verification.

Request body:

Field Type Required Description
url string (HTTPS) Yes Endpoint that will receive POST events.
events string[] Yes One or more of: step_completed, step_failed, workflow_completed, workflow_failed, approval_requested, timed_out.
description string No Human-readable label for this webhook.
active boolean No Whether to enable immediately. Default true.

Response (201):

{
  "id": "uuid",
  "url": "https://your-server.example.com/hook",
  "description": "Production notifier",
  "events": ["workflow_completed", "workflow_failed"],
  "secret": "whsec_a3f7c2...",
  "active": true,
  "created_at": "2026-05-01T00:00:00Z"
}

Errors:

  • 403 tier_required — Pro or above required.
  • 403 forbidden — requires admin role.
  • 400 invalid_body — missing url or events, or url is not HTTPS.

curl (Windows):

curl -X POST https://plith.ai/api/workspaces/uuid/webhooks -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"url\": \"https://your-server.example.com/hook\", \"events\": [\"workflow_completed\"]}"

PATCH /api/workspaces/{id}/webhooks/{webhookId}

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free Tier: Pro or above.

Updates a webhook's URL, events, description, or active state.

Request body: Send only fields to change (url, events, description, active).

Response (200): Updated webhook object (without secret).

Errors:

  • 403 tier_required / 403 forbidden / 404 not_found

DELETE /api/workspaces/{id}/webhooks/{webhookId}

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free Tier: Pro or above.

Permanently removes the webhook.

Response (200): { "ok": true }

Errors:

  • 403 tier_required / 403 forbidden / 404 not_found

POST /api/workspaces/{id}/webhooks/{webhookId}/test

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free Tier: Pro or above.

Sends a test payload to the webhook URL to verify connectivity.

Response (200): { "ok": true, "delivered": true, "status_code": 200 }

Errors:

  • 403 tier_required / 403 forbidden / 404 not_found

POST /api/workspaces/{id}/webhooks/{webhookId}/verify

Auth: Required (x-api-key or session) — minimum role: member Cost: Free Tier: Pro or above.

Verifies an incoming webhook payload signature. Pass the raw body and the X-Plith-Signature header value to confirm the payload was signed with the registered secret.

Request body:

Field Type Required Description
payload string Yes Raw request body string as received.
signature string Yes Value of the X-Plith-Signature header.

Response (200): { "ok": true, "valid": true }


Workspace Invites

POST /api/workspaces/{id}/invites

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free

Creates a shareable invite link. The recipient follows the link to join the workspace without needing a direct email.

Request body:

Field Type Required Description
role "viewer" | "member" | "admin" Yes Role to assign on accept.
max_uses integer No How many times the link can be accepted. Default unlimited.
expires_in_days integer No TTL in days. Default 7.

Response (201):

{
  "ok": true,
  "invite_id": "uuid",
  "token": "inv_abc123...",
  "url": "https://plith.ai/invite/inv_abc123...",
  "role": "member",
  "expires_at": "2026-05-25T00:00:00Z",
  "max_uses": null
}

Errors:

  • 403 forbidden — requires admin or owner.

curl (Windows):

curl -X POST https://plith.ai/api/workspaces/uuid/invites -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"role\": \"member\"}"

GET /api/workspaces/{id}/invites

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free

Lists all active invite links for the workspace.

Response (200): { "ok": true, "invites": [ { "invite_id", "token", "role", "expires_at", "max_uses", "uses_remaining", "created_at" } ] }

curl (Windows):

curl https://plith.ai/api/workspaces/uuid/invites -H "x-api-key: YOUR_KEY"

DELETE /api/workspaces/{id}/invites/{inviteId}

Auth: Required (x-api-key or session) — minimum role: admin Cost: Free

Revokes an invite link immediately. Any further attempts to use the token will fail.

Response (200): { "ok": true }

Errors:

  • 403 forbidden / 404 not_found

POST /api/workspaces/{id}/restore

Auth: Required (x-api-key or session) — minimum role: owner Cost: Free Tier: Team or Enterprise only.

Restores an archived workspace. Cannot restore a workspace that has been hard-deleted (returns 410).

Response (200): { "ok": true, "workspace_id": "uuid", "status": "active" }

Errors:

  • 403 forbidden — requires owner role.
  • 403 tier_required — Team or Enterprise subscription required.
  • 400 not_archived — workspace is not in an archived state.
  • 410 gone — workspace has been permanently deleted and cannot be restored.

curl (Windows):

curl -X POST https://plith.ai/api/workspaces/uuid/restore -H "x-api-key: YOUR_KEY"

Notifications

GET /api/notifications

Auth: Required (x-api-key or session) Cost: Free

Returns notifications for the authenticated user, with optional filtering.

Query parameters:

Parameter Description
unread_only true to return only unread notifications.
limit Page size. Range: 1–50. Default 20.
cursor Opaque cursor for the next page.

Response (200):

{
  "ok": true,
  "notifications": [
    {
      "id": "uuid",
      "type": "workflow_completed",
      "title": "Workflow completed",
      "body": "Your Solution Design workflow finished with quality score 92.",
      "workflow_id": "wr_2a4b6c8d...",
      "action_url": "/workflows/wr_2a4b6c8d...",
      "action_label": "View results",
      "metadata": {},
      "read_at": null,
      "created_at": "2026-05-08T12:05:00Z"
    }
  ],
  "unread_count": 1,
  "has_more": false
}

curl (Windows):

curl "https://plith.ai/api/notifications?unread_only=true" -H "x-api-key: YOUR_KEY"

GET /api/notifications/count

Auth: Required (x-api-key or session) Cost: Free

Returns the unread notification count only. Suitable for polling.

Response (200):

{
  "ok": true,
  "unread_count": 3,
  "request_id": "req_abc123..."
}

curl (Windows):

curl https://plith.ai/api/notifications/count -H "x-api-key: YOUR_KEY"

PATCH /api/notifications/read

Auth: Required (x-api-key or session) Cost: Free

Marks notifications as read. Pass specific IDs or all: true to mark everything read.

Request body:

Field Type Required Description
notification_ids string[] Conditional IDs to mark read. Required unless all: true.
all boolean Conditional true to mark all notifications read.

Response (200):

{
  "ok": true,
  "updated": 3,
  "request_id": "req_abc123..."
}

curl (Windows) — specific IDs:

curl -X PATCH https://plith.ai/api/notifications/read -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"notification_ids\": [\"uuid1\", \"uuid2\"]}"

curl (Windows) — mark all read:

curl -X PATCH https://plith.ai/api/notifications/read -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"all\": true}"

Data Deletion (GDPR)

These endpoints allow you to request and confirm permanent erasure of all data associated with your organization.

Warning: Data deletion is irreversible. All workflows, keys, billing history, and org data will be permanently removed.

Rate limit: 5 deletion requests per organization per day.


GET /api/data/delete-request

Auth: Required (x-api-key) Cost: Free

Returns the deletion request history for the organization.

Response (200):

{
  "requests": [
    {
      "id": "uuid",
      "status": "pending",
      "requested_at": "2026-05-08T12:00:00Z",
      "soft_deleted_at": null,
      "hard_deleted_at": null,
      "completed_at": null,
      "deletion_log": null,
      "error": null
    }
  ]
}

curl (Windows):

curl https://plith.ai/api/data/delete-request -H "x-api-key: YOUR_KEY"

POST /api/data/delete-request

Auth: Required (x-api-key) Cost: Free

Starts a GDPR erasure request. Returns a confirmation_token required to execute the deletion. Idempotent within a short window — returns the existing pending request if one was recently created.

Response (202):

{
  "request_id": "uuid",
  "confirmation_token": "tok_...",
  "status": "pending",
  "requested_at": "2026-05-08T12:00:00Z",
  "warning": "This will permanently delete all data for your organization.",
  "affected_data": ["workflows", "organization", "request_log"],
  "next_step": "Call DELETE /api/data/confirm-deletion with request_id and confirmation_token to execute."
}

Errors:

  • 409 deletion_already_pending — a deletion request is already pending.
  • 429 deletion_rate_limited — limited to 5 per org per day.

curl (Windows):

curl -X POST https://plith.ai/api/data/delete-request -H "x-api-key: YOUR_KEY"

DELETE /api/data/confirm-deletion

Auth: Required (x-api-key) Cost: Free

Executes the cascading deletion. This action is irreversible. All org data is permanently deleted. Request logs are anonymized rather than deleted.

Request body:

Field Type Required Description
request_id string Yes UUID from POST /api/data/delete-request.
confirmation_token string Yes Token from POST /api/data/delete-request.

Response (200): { "ok": true, "message": "Organization data has been permanently deleted." }

Errors:

  • 404 invalid_deletion_request — request not found, already processed, or token mismatch.
  • 500 deletion_failed — deletion partially executed; contact support with your request_id.

curl (Windows):

curl -X DELETE https://plith.ai/api/data/confirm-deletion -H "x-api-key: YOUR_KEY" -H "Content-Type: application/json" -d "{\"request_id\": \"uuid\", \"confirmation_token\": \"tok_...\"}"