Why it Matters
Not an Afterthought API
The Plexara REST API is the canonical surface of the platform. The portal is a SPA that consumes it. Admin tooling, user workflows, and automated pipelines all speak the same HTTP. Whatever the portal can do, your code can do, under the same governance.
Fail-Closed Authentication
Every request is authenticated against your identity provider before any tool or data access occurs. No anonymous mode. No permissive fallback.
Persona-Enforced Authorization
Each call is evaluated against the caller persona. Unauthorized tools, connections, and resources are not just blocked; they are not visible.
Audit on Every Call
Tool calls, queries, and errors flow into structured audit with indexed fields for user, tool, timestamp, status, and latency. Query the same log the platform does.
Typed Schemas, Live Catalog
Every tool exposes a JSON schema. Every endpoint returns typed responses. The catalog is live: new tools appear the moment they are registered.
Authentication
Three Methods, One Identity Model
Every method resolves to the same persona system. Whether a request arrives from a human, a service, or an MCP client, Plexara evaluates it against the same policy graph and writes the same audit record.
API Key
Service accounts, CI pipelines, scheduled jobs.
Minted per service with explicit persona scope, expiration, and rotation. Revocable in one click. Usage is audited.
X-API-Key: <key>
Bearer Token (OIDC)
Interactive human users via your identity provider.
Tokens validated against your upstream IdP with JWKS auto-discovery. Roles map to personas through your existing identity infrastructure.
Authorization: Bearer <id_token>
OAuth 2.1 + PKCE
MCP clients, desktop agents, third-party integrations.
The built-in OAuth 2.1 server bridges MCP clients to your upstream IdP with PKCE, rotating refresh tokens, and bcrypt-hashed client secrets.
Authorization: Bearer <access_token>


Quickstart
Your First Request
List the tools your persona has access to. No SDK required. Any HTTP client works.
List tools (Admin)
curl https://api.plexara.io/api/v1/admin/tools \ -H "X-API-Key: $PLEXARA_API_KEY" \ -H "Accept: application/json"
Get current session (Portal)
curl https://api.plexara.io/api/v1/portal/me \ -H "Authorization: Bearer $PLEXARA_TOKEN" \ -H "Accept: application/json"
Execute a tool
curl https://api.plexara.io/api/v1/admin/tools/call \
-X POST \
-H "X-API-Key: $PLEXARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "trino_query",
"arguments": { "query": "SELECT 1" }
}'
API Surface
Three Namespaces, One Platform
The full API is organized into three top-level namespaces. A representative subset of each is shown below. The complete endpoint catalog with request and response schemas lives in the API reference.
Portal
/api/v1/portalPractitioner-scoped endpoints. Everything the portal SPA calls on behalf of a signed-in user. Scoped to the caller and their persona.
- GET
/Current session identity and persona.api /v1 /portal /me - GET
/List personal assets.api /v1 /portal /assets - POST
/Copy a shared asset.api /v1 /portal /assets /{id} /copy - GET
/Asset version history.api /v1 /portal /assets /{id} /versions - GET
/List personal collections.api /v1 /portal /collections - PUT
/Reorder collection sections.api /v1 /portal /collections /{id} /sections - POST
/Share an asset.api /v1 /portal /assets /{id} /shares - GET
/Assets shared with me.api /v1 /portal /shared-with-me - GET
/User activity metrics.api /v1 /portal /activity /overview - GET
/Insights captured by me.api /v1 /portal /knowledge /insights - GET
/My memory records.api /v1 /portal /memory /records - GET
/Personal and accessible prompts.api /v1 /portal /prompts
Admin
/api/v1/adminPlatform administration. Audit, personas, connections, knowledge governance, memory, configuration, tools catalog, and tool execution.
- GET
/Platform identity and features.api /v1 /admin /system /info - GET
/Structured audit log search.api /v1 /admin /audit /events - GET
/Latency and success rates.api /v1 /admin /audit /metrics /overview - GET
/Full tool catalog.api /v1 /admin /tools - POST
/Execute a tool.api /v1 /admin /tools /call - GET
/JSON schemas for all tools.api /v1 /admin /tools /schemas - GET
/Persona definitions.api /v1 /admin /personas - PUT
/Update persona policy.api /v1 /admin /personas /{name} - GET
/Upstream data-source inventory.api /v1 /admin /connections - GET
/Pending and applied changesets.api /v1 /admin /knowledge /changesets - POST
/Reverse a changeset.api /v1 /admin /knowledge /changesets /{id} /rollback - POST
/Mint a service API key.api /v1 /admin /auth /keys - GET
/Platform change history.api /v1 /admin /config /changelog
Resources
/api/v1/resourcesMCP resource CRUD. The same resource surface exposed to AI agents, directly accessible for tooling, validation, and automation.
- GET
/List visible resources.api /v1 /resources - POST
/Create a resource.api /v1 /resources - GET
/Get resource metadata.api /v1 /resources /{id} - GET
/Get resource content.api /v1 /resources /{id} /content - PATCH
/Update resource.api /v1 /resources /{id} - DELETE
/Delete resource.api /v1 /resources /{id}
MCP Apps
Interactive UI Alongside a Tool Result
A tool result can carry a reference to a UI resource. A host that understands the reference fetches the app HTML and renders it in a sandboxed iframe beside the answer, passing the tool result in. Plexara delivers two of these apps today, and adds more as host support for MCP Apps matures.
Platform Info
platform_infoRenders the platform name, version, and description, the connected toolkits with their icons, which feature flags are on, and the personas active for the session. It is there in every conversation with nothing to set up.
List Prompts
show_promptsThe prompt browser: search-as-you-type over the ranked query, My Prompts and Library buckets, collection and tag filters, usage-based sorting, and cards carrying display name, description, version, approval provenance, and run count. A detail view generates its form from the prompt argument specs, and Run resolves through manage_prompt use, placing the rendered prompt directly into the chat where the host supports conversation insertion.
- Presentation only, never required
- An app is a rendering of data that is complete without it. The same
manage_promptcalls the prompt browser makes return full structured JSON in clients that render no UI, so a terminal client and an app-capable desktop client see the same library with different amounts of chrome. Nothing is only reachable through an app, which is what makes it safe to build against one. - Apps call tools through the same gates
- An app calls tools itself rather than only rendering the result that opened it, and those calls travel the same MCP transport as the agent's, meeting the same persona checks and writing the same audit records. The session handle applies too: an app calls
platform_infofirst and threads the returnedsession_idon every call after it. Skipping the handshake returnsSESSION_REQUIREDon the app's first data call.platform_infois never gated, since it is the call that mints the handle. - Bound to a display tool on purpose
- The prompt browser hangs off
show_prompts, a tool that performs no data operation and does nothing but ask for the library to be displayed.manage_prompt, which resolves, runs, creates, and edits, carries no app. A window opens when a person asked to see one, not every time an agent touches a prompt.
The error contract
Every Failure Names Whose Fault It Is
An agent that cannot tell a bad argument from a denied permission has one response to every failure: try again and hope. A failed tool call sets isError and returns both a human-readable message and a machine-readable structuredContent.error object, so the agent can branch on the difference.
{
"isError": true,
"content": [
{
"type": "text",
"text": "the \"asset_id\" parameter is required (code: missing_required_parameter) Hint: Supply \"asset_id\" and retry."
}
],
"structuredContent": {
"error": {
"code": "missing_required_parameter",
"category": "client_input",
"message": "the \"asset_id\" parameter is required",
"hint": "Supply \"asset_id\" and retry. This is a problem with the call's arguments, not a platform fault."
}
}
}code- A stable identifier the agent may branch on, such as
missing_required_parameter,invalid_arguments,not_found, orsetup_required. category- The broad class of failure, which is what tells the agent whose fault it is.
message- The specific failure, in the terms of this particular call.
hint- The corrective action, whenever the caller can take one.
| Category | Whose fault | What the agent should do |
|---|---|---|
client_input | The call | Fix the arguments and retry. |
not_found | The call | The named resource does not exist; correct the reference. |
authentication_failed | Caller identity | Provide valid credentials. |
authorization_denied | Caller identity | The persona is not permitted; request access. |
user_declined | The user | A consent prompt was declined. |
setup_required | Session state | Call the required setup tool first. |
feature_unavailable | Nobody | The capability is not part of this deployment. Report it as unavailable rather than as an outage, and do not retry. |
internal | The platform | Not the caller's fault; do not retry with modified input. |
tool_error | Unclassified | A failure that has not been given a finer category. The message is still descriptive. |
The envelope is uniform by construction. A normalization layer wraps every error result even when an individual tool returns nothing but a bare string, so an agent never receives an opaque failure it cannot classify. The category is written to the audit log as error_category, which is how a spike in denials is distinguished from a spike in outages.
Unknown arguments are refused, not ignored
Plexara tool schemas are closed to unknown top-level arguments. A misnamed argument fails at the tool boundary, before the handler runs, with the offending property named in the message. Passing parameters to api_invoke_endpoint instead of query_params returns this:
{
"error": {
"code": "invalid_arguments",
"category": "client_input",
"message": "validating \"arguments\": validating root: unexpected additional properties [\"parameters\"]",
"hint": "Read the tool's schema, correct or drop the named property, and retry."
}
}The alternative, dropping the field and running anyway, is how an agent comes to believe it applied a filter it never applied, then reports a number computed over the wrong rows. A refusal at the boundary is a mistake the agent can correct on the next call. A silent drop is a wrong answer nobody catches.
Nested maps stay open where the names inside them belong to somebody else. query_params, headers, and body on the api_* tools accept arbitrary keys, because those names are the upstream API's vocabulary rather than the tool's.
Content verbs
Edit a Document Without Resending It
Regenerating a whole report to change one sentence costs output in proportion to the document rather than the change, and every regeneration is another chance to silently drop an unrelated paragraph. manage_asset and manage_prompt carry the same six content verbs, with the same argument names, the same operations, and the same error codes on both.
The loop for a large document is outline or locate to decide where, then patch that place. The body crosses the wire in neither direction.
outline- The heading tree with levels, line numbers, and per-section byte size. On an HTML, JSX, or SVG asset it also returns the addressable landmarks: every element carrying an id or a data-* marker, with its tag, a copyable selector, its line, and its size.
locate- Literal or regex matches with the total count, line numbers, enclosing section, and a context window wide enough to copy verbatim into an anchor. The count is the point: an agent that checks first never guesses.
get_content- Read one span rather than the document: the whole body, a single section or selector-addressed element, or a line range.
stats- Size, line count, current version, content type, and body hash.
patch- Apply an ordered list of anchored edits. Every edit resolves against an in-memory copy first, and the first failure aborts the whole call and writes nothing, naming the failing edit by index.
diff- Compare two versions, or a pending prompt draft against the approved snapshot still being served, which is the question a reviewer actually has.
On HTML, JSX, and SVG, you address a node
A selector names an element by CSS selector, and the region is that element's balanced subtree, running from its start tag through its matching end tag. A replace or a move cannot cut a tag in half. Type, #id, .class (which also matches a JSX className), [attr] and [attr=value] are supported, joined by descendant or child combinators.
A selector matching several elements is refused, with the count in the message, and occurrence is the explicit opt-in when the caller means a specific one. On a dashboard with no headings, outline returns the landmarks, so an agent finds where to patch without reading the body. Markdown addresses regions by heading instead, and a structureless format like JSON or CSV refuses both and takes anchored edits.
Matching is exact, with a single retry that normalizes line endings and trailing whitespace. Nothing beyond that: no fuzzy or semantic matching, because a plausible-but-wrong edit applied silently is worse than a rejection the agent can correct. The response never echoes the new body, only the new version, the new size, a per-edit outcome, and a unified diff of the changed hunks. dry_run returns exactly that report without writing.
Patch two regions of a dashboard
{
"action": "patch",
"asset_id": "ast_...",
"base_version": 7,
"edits": [
{
"op": "replace_section",
"selector": "[data-region=\"revenue\"]",
"text": "<Card data-region=\"revenue\">...</Card>"
},
{
"op": "replace",
"selector": ".metric",
"occurrence": 2,
"find": "Users",
"replace": "Active Users"
}
],
"change_summary": "restate the revenue card, relabel the second metric"
}base_version is optional and checked when supplied. A mismatch is refused with the current version in the error, so an agent that threads the version it read gets lost-update protection for free. A patch writes an ordinary new version, so history and revert keep working, and a patch to an approved prompt still produces a pending draft for review.
Content-type detection
The Stored Type Is Detected, Not Taken on Faith
An upstream API that answers a JSON endpoint with text/plain, a browser that sends application/octet-stream for an extension it does not recognize, and an agent that saves a payload under a catch-all type would each otherwise produce an asset the portal can only show as raw text. Detection runs on every write path that accepts outside content: save_asset, manage_asset updates, api_export, and resource uploads.
A specific declaration still wins. Detection only runs when the declaration is absent, application/octet-stream, or text/plain.
- Binaries from magic bytes, structured text from bounded heuristics
- Images, audio, video, PDF, and archives are recognized from the first 512 bytes. JSON, NDJSON, XML, YAML, CSV, and TSV all look like plain text to a byte sniffer, so each gets its own heuristic over a bounded prefix.
- A prefix, never the whole payload
- A streaming export stays streaming. The prefix is replayed ahead of the untouched remainder, so detecting the type of a multi-gigabyte export costs the same as detecting the type of a one-line file.
- One family, one type string
- Aliases normalize, so
text/jsonandapplication/jsonboth store as one type and everything downstream compares one string. The stored object key follows the detected type, so a bucket listing shows.jsonrather than.bin. - The original declaration is kept
- When detection overrides a declaration, the original is recorded in the asset's provenance as
declared_content_type. A stored type that disagrees with its source stays explainable after the fact instead of looking like a mystery.
API Gateway
Reach any connected API over HTTP
The same external APIs your agents call are reachable from tools that do not speak MCP. One route proxies any operation on a connected API, so an Apache NiFi processor, an n8n workflow, an Airflow task, or a shell script gets the persona limits, stored credentials, and audit trail without holding a secret of its own.
The response carries two separate statuses: the API's own status inside the body, and an HTTP status for whether Plexara allowed and completed the call. A pipeline can check one for the API result and the other for a platform error.
Call a connected API
curl https://api.plexara.io/api/v1/gateway/vendor/invoke \
-X POST \
-H "X-API-Key: $PLEXARA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"method": "GET",
"path": "/v1/orders",
"query_params": { "status": "open", "limit": 25 }
}'Addressing an operation
Call It by the Name Discovery Gave You
api_invoke_endpoint and api_export address an operation in one of two ways. The first is the operation_id that api_list_endpoints and api_get_endpoint_schema already returned, which makes invocation the natural continuation of discovery: read a schema by that identifier, then call it by the same one.
Plexara resolves the identifier to the method and path template from the connection's catalog, then substitutes and URL-escapes the values in path_params. Nobody hand-builds /v1/users/123 out of /v1/users/{id}, which is where escaping bugs come from. When the same identifier appears in more than one spec in the catalog, spec disambiguates and the error names the candidates.
The second is raw method plus path, for an uncataloged call. Supply one form or the other, never both.
Invoke by operation_id
{
"connection": "vendor",
"operation_id": "getUser",
"path_params": { "id": "123" }
}The built-in util connection
A connection named util is present alongside your own, discovered and invoked exactly like any other. Its fetch_url operation reaches a public URL server-side and returns it inline or streams it straight into a saved asset. That closes a real gap: an ordinary gateway call joins a path to a registered base URL, so it cannot follow a one-time presigned download link whose host and token are generated on the spot.
api_invoke_endpoint vendor POST /exports -> job id
api_invoke_endpoint vendor GET /exports/{id} -> signed download_url
api_export util POST /util/fetch -> saved assetThe URL is used exactly as given and the query string is never re-encoded, so a signature survives byte for byte. No credential is ever attached, only GET and HEAD are accepted, and internal address space is closed: loopback, private ranges, link-local including the cloud metadata endpoint, and internal hostnames. The hostname is resolved first and only the vetted address is dialed, on every redirect hop, so a public name cannot rebind to an internal one between the check and the connection. Like every connection, util is deny-by-default and a persona reaches it only when its rules allow it.
Conventions
Predictable by Design
- Versioned, stable paths
- Every route is rooted at
/api/v1. Breaking changes are versioned, never patched in place. - Pagination
- List endpoints accept
pageandper_pagequery parameters. Responses includetotal. - Content types
- JSON request and response bodies. Binary asset content is returned with the original content type. Errors use
application/problem+json. - Idempotency
- PUT and DELETE are idempotent. POST creation endpoints accept an
Idempotency-Keyheader when deduplication matters.
Errors
RFC 7807 Problem Details
HTTP errors are never a wall of HTML or a terse string. Every failure returns a structured problem response with a machine-readable type, a human-readable title, an HTTP status, and a detail line that explains what went wrong in the context of this specific call. A tool call that fails carries the richer error contract above instead, with its category and hint.
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Forbidden",
"status": 403,
"detail": "persona 'analyst' does not allow tool 'trino_execute'",
"instance": "/api/v1/admin/tools/call"
}Common questions
Developer FAQ
OIDC with required JWT claims is the primary path. OAuth 2.1 with PKCE and Dynamic Client Registration is supported for new client types. API key management (X-API-Key header) is available for service accounts that cannot do interactive auth. Your IdP provides the identity; Plexara provides the persona resolution.
Learn more: Meeting enterprise systems where they areThey expose the same governed surface (assets, collections, knowledge, memory, prompts, personas, audit, tool execution). Choose REST when you need a service-to-service call with a stable contract. Choose MCP when an AI agent needs the same operations through a protocol it already understands. Both share one identity, audit, and persona model.
Learn more: Is MCP just an API wrapper?The MCP server works with any MCP-compatible client SDK (Anthropic, OpenAI, the open-source MCP libraries in TypeScript and Python). The REST API ships an OpenAPI specification, so generated clients work with the standard openapi-generator tooling for whichever language your service is in.
Learn more: Two front doors, one governed surfaceTool calls and queries are bounded per persona, with hard limits on result size to keep responses inside agent context windows. Customer-specific quotas (concurrent agents, queries per minute, storage) are negotiated as part of the engagement. The audit log records throttling events so capacity tuning is data-driven, not guesswork.
Learn more: Token efficiency in enterprise MCP deploymentsYes. Custom tools can be registered through the Portal or via the management API, with persona-scoped visibility. They appear in the agent's tool list like any other Plexara tool, with the same audit and authorization treatment.
Learn more: Two front doors, one governed surface
Reference
Full API Reference
Browse every endpoint, request and response schema, and authentication detail in the complete reference.
