Skip to content

Apps

For developers who connect a client to OpenBot or extend its server. The examples need the access stated on this page. Start with the developer manual.

An App is an outside service that the Bot can use. Each App is published on the web with the OpenBot App Protocol. This page shows how to connect an App to your account, sign in to it, and use it in Turns.

All examples use a user token. The same calls also work with the session cookie. See Authentication.

Word Meaning
Card URL The stable HTTPS address of the App’s Server Card. It is the App’s identity.
Connection An App saved to your account, with your settings and sign-in. You can have up to 30.
Connection draft A saved discovery result. You review it before you create a connection. It lasts 24 hours.
App operation One recorded call from a Turn to an App, with its status and result.

Give OpenBot the App’s address. This can be a domain, an App page, a Catalog link, or a Card link. A QR code holds the same HTTPS link.

Terminal window
curl -X POST https://api.example.com/v1/connection-drafts \
-H "Authorization: Bearer $OPENBOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "input": "apps.example.com" }'

The answer is a draft with status 201. Keep its id and revision. The discovery field is one of two kinds:

  • Single has one candidate.
  • Multiple has a candidates list. Ask the user to choose one. OpenBot never adds a whole Catalog.

Each candidate has a cardUrl, the card, a list of endpoints, and the sources that led to it. Each endpoint has an index and the requirements it needs, such as an API key header.

To only look, without saving a draft, send the same body to POST /v1/apps/discover. Discovery never adds, signs in to, or calls an App.

draftId must be the UUID from the saved draft. Old v4 and new v7 values both work. The API rejects other text before it reaches storage. Outside connection IDs keep their existing format.

Show the user the App name, its source, and the endpoint. Then create the connection. You must send "confirmed": true.

Terminal window
curl -X POST https://api.example.com/v1/connections \
-H "Authorization: Bearer $OPENBOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"draftId": "<draft id>",
"revision": 0,
"cardUrl": "https://apps.example.com/mcp/server-card",
"remoteIndex": 0,
"name": "Work calendar",
"values": { "header:X-API-Key": "<secret>" },
"confirmed": true
}'

The keys in values come from the endpoint’s requirements. They look like variable:<name>, header:<name>, or header:<name>:variable:<name>. OpenBot encrypts these values. Later reads only show values that are not secret.

OpenBot reads the Card again before it saves. The new connection uses the draft id as its own id. If you send the same request again, you get the same connection back.

Terminal window
curl -X POST https://api.example.com/v1/connections/<connection id>/verify \
-H "Authorization: Bearer $OPENBOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"commandKey":"check-connection-1"}'

Connection updates and checks are durable commands. verify, revalidate, PUT /v1/connections/:connectionId, and the manual update endpoint require a commandKey and return HTTP 202 with an executionId. Keep that key for network retries. Read progress at /v1/executions/:executionId/observe. Closing the stream does not cancel the command. Use the execution cancel endpoint to stop it.

Read the saved result at GET /v1/connections/:connectionId/executions/:executionId. The response has execution and result. A completed check returns Verified with capability lists, or Unavailable. Read the connection to see its current availability. A completed change returns Updated with the new revision. The SDK helpers Connections.verify, update, updateManual, and revalidate make one key per command. One connection accepts only one change at a time. A new command must use its current revision for updates, manual changes, and revalidation. A check needs only its command key.

Status Error What to do
409 ConnectionConflict Read reason: CandidateChanged, RevisionChanged, DraftExpired, AlreadyAdded or LimitReached.
422 ConnectionConfigurationInvalid Fill in the setting names listed in keys.
422 AppDiscoveryFailed Check the address. reason and issues say what failed.
403 AppRestricted The deployment blocks this App. See restriction.reason and restriction.appealUrl.

The availability field tells you if the Bot can use a connection.

_tag Meaning
Unverified Saved, but not checked yet.
Available Checked and ready.
PendingAuthorization The App needs an account sign-in. scopes lists the access it asks for.
AuthorizationExpired The sign-in no longer works. Refresh it or sign in again.
ConnectionFailed The last check failed. message says why.
NeedsRevalidation The App changed its endpoint or sign-in needs. Review it again.
CleanupPending Removal did not finish. Try to remove it again.
Restricted The deployment blocks this App.
PendingConfiguration The connection needs more settings.

Once a command is accepted, a failure is in its execution record as a safe category and errorId. Provider text and the full cause stay in the sealed error record. Reading OAuth client metadata returns 503 SettingsUnavailable when platform settings cannot be read.

When you use @openbot-build/protocol on its own, MCP errors keep a safe ClientError message. Network, SDK and schema details stay in a private Cause, the full set of reasons for the failure. They do not go to the normal log or the public error schema.

Client.errorCause(error) gives an Option with a Redacted<Cause> when the error has private details. Only a trusted error handler should open this value. Seal it before saving it. Client.withCause(error, cause) lets your own protocol adapter keep details in the same form. OpenBot saves these details in its sealed error record. Public indexing uses the same private cause on an Unconfirmed result. Its reason and fixed issue are safe to show. A host must save the private cause before it encodes the result.

Some Apps need the user’s own account. OpenBot uses OAuth with PKCE for this. The user signs in at the App’s provider, not at OpenBot.

  1. Start the flow. Send the connection revision. Leave scopes empty to use the scopes the App asks for. returnTo is a same-origin path. It defaults to /.

    Terminal window
    curl -X POST https://api.example.com/v1/connections/<connection id>/authorization \
    -H "Authorization: Bearer $OPENBOT_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{ "commandKey": "begin-app-sign-in-1", "revision": 0, "scopes": [], "returnTo": "/apps" }'
  2. The response is HTTP 202 with an execution receipt. Read GET /v1/app-authorization-actions/:executionId until it completes, then read result. The link is available only after the flow is saved. Reuse the same command key and body if a reply is lost:

    • AuthorizationStarted has a flowId and an authorizationUrl. Open the URL in the user’s browser.
    • IssuerSelectionRequired lists issuers. Ask the user to pick one, then start again with issuer.
    • AuthorizationNotRequired means the App needs no sign-in.
  3. The user signs in. The provider sends the browser to https://api.example.com/auth/apps/callback.

  4. OpenBot sends the browser to returnTo?authorization=<flowId>. If the browser has no OpenBot session, it goes to /?authorization=<flowId>. Your client signs the user in first (see Authentication).

  5. Your client finishes the flow:

    Terminal window
    curl -X POST https://api.example.com/v1/app-authorizations/<flow id>/complete \
    -H "Authorization: Bearer $OPENBOT_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{ "commandKey": "finish-app-sign-in-1" }'

To see the flow, call GET /v1/app-authorizations/:flowId. Its status is Waiting, Received, Processing, Succeeded, Cancelled or Failed. Completion returns HTTP 202 with an execution receipt. Keep the same commandKey when a reply is lost. A signed-in callback submits this command with the caller that began the flow. Its original token still limits the work. The flow’s executionId links to the common execution API. Read GET /v1/app-authorization-actions/:executionId for its state and safe result. Closing an observer does not cancel the command. Use the execution cancel API. A code exchange with an unknown result waits for a decision; it does not send the code again on its own.

OpenBot’s OAuth client ID is a public document at https://api.example.com/v1/apps/client-metadata. Providers read it, so you do not need to register OpenBot with each provider.

To sign out of the App, send DELETE /v1/connections/:connectionId/authorization with { "commandKey" }. It returns HTTP 202. OpenBot closes active App scopes, then clears the local sign-in and saves remote cleanup in one transaction. Read GET /v1/app-authorization-actions/:executionId. Its execution can be completed while result is RevocationPending. The cleanupExecutions array gives the saved remote cleanup states. When that work ends, the result is Revoked, or LocalOnly if the provider cannot revoke tokens.

Cleanup waits while any connection or developer test uses the same issuer and OAuth client ID, across all OpenBot accounts. A provider can revoke related tokens together. Different token strings do not show that revocation is safe. Failed remote requests retry with saved timers. A refresh also saves cleanup for the old credential. Local removal can finish while this cleanup waits.

Each cleanup execution belongs to the source organization. Its service actor does the work. You can read its state and events only while current rules let you read the source operation. Other organizations cannot find the execution.


Task Request
List or read GET /v1/connections, GET /v1/connections/:connectionId
Rename, turn off, or turn on PUT /v1/connections/:connectionId with { "commandKey", "revision", "name", "enabled" }
Refresh the sign-in POST /v1/connections/:connectionId/refresh
Review a changed App Make a new draft from the Card URL. Then POST /v1/connections/:connectionId/revalidate.
Remove DELETE /v1/connections/:connectionId

A refresh takes { "commandKey" } and returns HTTP 202. Its action result is Refreshed. It never asks for more access. A refresh can use up the old token, so an unknown result waits for a decision. If work stops after a send without installing a new grant, the connection is disabled and marked AuthorizationExpired. Start a new sign-in to restore it. The SDK helpers AppAuthorizations.begin, complete, refresh, and revoke keep one command key for network retries.

When an App changes its endpoint, private settings, provider, or scopes, the connection becomes NeedsRevalidation. The Bot cannot use it until you review it. Send { "commandKey", "revision", "draftId", "remoteIndex", "values", "confirmed": true } to revalidate. OpenBot does not send old tokens to a new endpoint. An App that needs sign-in then needs a new sign-in.

Remove takes { "commandKey" } and returns HTTP 202. OpenBot first turns the connection off and closes its active App scopes on all nodes. A node that does not answer keeps this stage pending. Saved close results survive lost replies. The final transaction removes the connection and its local tokens and gives remote cleanup to its own execution. Removed means local removal is complete; it does not mean every remote grant was revoked. Read cleanupExecutions from the authorization action for those states. Reuse the command key after a lost reply. Cancelling before removal leaves the connection off; a new remove command can finish it.

A closed App client keeps its full close result. The caller can close its scope again after the server stops, without calling services that have already stopped. If the first close failed, later closes keep that failure.

You can also connect an MCP server that has no Server Card. Send POST /v1/connections/manual with a new UUID id, a name, the HTTPS url, "transport": "streamable-http", and headers. Change it with PUT /v1/connections/:connectionId/manual with commandKey, the current revision, and the complete new configuration. A null header keeps its saved value. Leave a header out of headers to remove it. A new header needs a value. Manual connections never go into the public App Index.


When a Turn runs, OpenBot gives the model a tool for each capability of your usable connections. Each tool name looks like openbot_<hash>. Its description starts with the App name, the connection name, and the capability name.

  • Tools become callable tools. Tools whose visibility does not include model stay hidden from the model.
  • Resources, resource templates and prompts become read tools.

Every App tool call goes through the capability check first. The first call of a tool usually makes a request that a decider answers. A Grant or a Rule can then cover later calls. Reads of resources and prompts need no decision, and neither do tools that the organization marked as reads in its settings. App notes never decide this. Each App tool has an extra openbot_reason parameter for the reason of the Bot. OpenBot never sends it to the App.

The model also gets three helper tools. They never add an App, sign in, or approve an action.

Tool What it does
openbot_search_apps Searches the public App Index for a missing capability.
openbot_app_details Reads the public details and sources of one App by its Card URL.
openbot_app_access Lists the user’s connections and their setup or sign-in needs.

When the platform turns on Code Mode, the model also gets openbot_code_api and openbot_run_code. With them, the Bot writes a short program that calls these App tools. Each call in the program takes the same path as a normal App tool call.

Each App call has a business id and an executionId. Read it with GET /v1/chats/:chatId/app-operations or GET /v1/app-operations/:id. The result has the shared execution status, a safe failure with an error ID, and result. Use the execution API to observe events, cancel work, or decide what to do about an unknown outside result.

An App that asks for input has result._tag: "InputRequired". Its question is in result.request. Send the answer with its round and a new commandKey. Keep that key and the same body when retrying after a lost reply.

Terminal window
curl -X POST https://api.example.com/v1/app-operations/<operation id>/inputs \
-H "Authorization: Bearer $OPENBOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "commandKey": "input-answer-1", "round": 1, "responses": { "<input key>": { "action": "accept", "content": { "label": "Team sync" } } } }'

The response is 202 with an execution command receipt. It does not mean that the App has finished. Observe that execution for the end. The SDK helper AppCalls.respond makes one command key and keeps it across network retries. The response keys and values follow the MCP input request schema.

An outcome_unknown execution waits for a decision. Check the outside state before choosing a retry. It must not send an unconfirmed write again on its own.


After an upgrade, saved App calls remain in the same history API with their original IDs. Reads check your current account and Chat access. Error details stay private; the public result gives an error kind and ID. An old call with no saved caller cannot continue by sending more input. Check an unknown outside result before you start a new call.

Some App tools come with an interactive view. A view is an HTML page from the App, defined by MCP Apps. OpenBot has no user interface, so your client shows the view. You can skip this part. Every result also has text.

  1. Open the view for an operation: POST /v1/app-operations/:operationId/view with { "chatId", "itemId" }. The answer has a view id, the HTML resource, the view’s tools, the call arguments, and the result.
  2. Show the HTML in an iframe. The @openbot-build/protocol/McpApps module has a browser host that does this. See Protocol.
  3. Pass the view’s requests to OpenBot:
View request OpenBot call
List tools, resources, templates POST /v1/app-views/:viewId/read
Call a tool or read a resource POST /v1/app-views/:viewId/actions (returns 202 and a ticket)
Check an action GET /v1/executions/:executionId/events
Update the model context PUT /v1/app-views/:viewId/context
Send a text message POST /v1/app-views/:viewId/messages
Close DELETE /v1/app-views/:viewId

Action and message commands carry commandKey. Use one key for each command and keep it on retry. The 202 reply has executionId, chat, and turn. Actions wait in the Chat queue as separate Turns. Read the execution event log for child executions, decisions, and results. Closing a view ends access to the view; an accepted action keeps running. A view stays valid across server restarts until it expires or its connection or access rights change.

View actions go through the same approval rules. A message from the view goes to the Chat as a notice through its inbox. The Bot reads it at its next step, or it starts a new Turn. If a view cannot open, you get 409 AppViewUnavailable. The text result is still there.


Export your App list. The file has no secrets and no sign-ins.

Terminal window
curl https://api.example.com/v1/app-experience/export \
-H "Authorization: Bearer $OPENBOT_TOKEN"
{
"format": "openbot-apps-v1",
"apps": [
{
"name": "Work calendar",
"cardUrl": "https://apps.example.com/mcp/server-card",
"remoteIndex": 0
}
]
}

Send the same JSON to POST /v1/app-experience/import. A file can hold up to 30 Apps. Import does not create connections. For each App you get a fresh draft, or an error. The draft’s imported field keeps the name and endpoint. Review each draft and create the connection as usual.

POST /v1/app-experience/reset takes { "commandKey" } and returns HTTP 202. It fixes the set of connections at submission and clears the drafts and saved App view state. Connections added later stay out of this reset. Each removal has its own child execution. Read GET /v1/app-experience/resets/:executionId for the parent and its children, or observe the common execution events. The reset completes only after every child succeeds. Cancellation waits for every child to release its work. A cancelled reset can leave some connections already removed. The SDK helpers AppAuthorizations.remove and reset keep the key for network retries.

See the full request and response shapes in the API reference.