Skip to main content
Plexara

Developers

Build on Plexara

Plexara exposes every portal capability as a typed, authenticated, audited REST API. Assets, collections, knowledge capture, memory, personas, prompts, audit, governance, and tool execution. The same endpoints the portal uses are available to your integrations, agents, and pipelines.

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>
The Keys admin screen listing API keys in a table with name, email, description, roles, expiration, and a delete action per row, one key struck through and marked Expired, and an Add Key button at the top right
Every service key in one table: who it belongs to, what it is for, which roles it carries, and when it expires. An expired key stays listed, struck through.
The Keys admin screen with the Add Key form expanded above the table, showing name, email, and description fields, a roles input with a Browse available roles link, an expiration picker set to Never, and a Create button
Minting a key is a name, an owner, the roles it should resolve to, and an expiration. The roles decide the persona, so a key never gets more than its role allows.

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" }
  }'
The Try It tab for the Trino Query tool in the Tools admin screen, with a SQL text area, limit, timeout, and output format fields, an Execute button, and an empty History panel reading no test calls yet
The same tool call from the portal: Try It on any tool in Admin runs it with the arguments you type and keeps a history of the test calls, so you can check a request before scripting it.

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/portal

Practitioner-scoped endpoints. Everything the portal SPA calls on behalf of a signed-in user. Scoped to the caller and their persona.

  • GET/api/v1/portal/meCurrent session identity and persona.
  • GET/api/v1/portal/assetsList personal assets.
  • POST/api/v1/portal/assets/{id}/copyCopy a shared asset.
  • GET/api/v1/portal/assets/{id}/versionsAsset version history.
  • GET/api/v1/portal/collectionsList personal collections.
  • PUT/api/v1/portal/collections/{id}/sectionsReorder collection sections.
  • POST/api/v1/portal/assets/{id}/sharesShare an asset.
  • GET/api/v1/portal/shared-with-meAssets shared with me.
  • GET/api/v1/portal/activity/overviewUser activity metrics.
  • GET/api/v1/portal/knowledge/insightsInsights captured by me.
  • GET/api/v1/portal/memory/recordsMy memory records.
  • GET/api/v1/portal/promptsPersonal and accessible prompts.

Admin

/api/v1/admin

Platform administration. Audit, personas, connections, knowledge governance, memory, configuration, tools catalog, and tool execution.

  • GET/api/v1/admin/system/infoPlatform identity and features.
  • GET/api/v1/admin/audit/eventsStructured audit log search.
  • GET/api/v1/admin/audit/metrics/overviewLatency and success rates.
  • GET/api/v1/admin/toolsFull tool catalog.
  • POST/api/v1/admin/tools/callExecute a tool.
  • GET/api/v1/admin/tools/schemasJSON schemas for all tools.
  • GET/api/v1/admin/personasPersona definitions.
  • PUT/api/v1/admin/personas/{name}Update persona policy.
  • GET/api/v1/admin/connectionsUpstream data-source inventory.
  • GET/api/v1/admin/knowledge/changesetsPending and applied changesets.
  • POST/api/v1/admin/knowledge/changesets/{id}/rollbackReverse a changeset.
  • POST/api/v1/admin/auth/keysMint a service API key.
  • GET/api/v1/admin/config/changelogPlatform change history.

Resources

/api/v1/resources

MCP resource CRUD. The same resource surface exposed to AI agents, directly accessible for tooling, validation, and automation.

  • GET/api/v1/resourcesList visible resources.
  • POST/api/v1/resourcesCreate a resource.
  • GET/api/v1/resources/{id}Get resource metadata.
  • GET/api/v1/resources/{id}/contentGet resource content.
  • PATCH/api/v1/resources/{id}Update resource.
  • DELETE/api/v1/resources/{id}Delete resource.

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_info

Renders 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_prompts

The 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_prompt calls 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_info first and threads the returned session_id on every call after it. Skipping the handshake returns SESSION_REQUIRED on the app's first data call. platform_info is 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, or setup_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.
CategoryWhose faultWhat the agent should do
client_inputThe callFix the arguments and retry.
not_foundThe callThe named resource does not exist; correct the reference.
authentication_failedCaller identityProvide valid credentials.
authorization_deniedCaller identityThe persona is not permitted; request access.
user_declinedThe userA consent prompt was declined.
setup_requiredSession stateCall the required setup tool first.
feature_unavailableNobodyThe capability is not part of this deployment. Report it as unavailable rather than as an outage, and do not retry.
internalThe platformNot the caller's fault; do not retry with modified input.
tool_errorUnclassifiedA 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/json and application/json both store as one type and everything downstream compares one string. The stored object key follows the detected type, so a bucket listing shows .json rather 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.

Explore the API Gateway

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 asset

The 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 page and per_page query parameters. Responses include total.
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-Key header 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 are

They 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 surface

Tool 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 deployments

Yes. 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.