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. |
Connect an App
Section titled “Connect an App”Find the App
Section titled “Find the App”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.
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:
Singlehas onecandidate.Multiplehas acandidateslist. 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.
Review and create the connection
Section titled “Review and create the connection”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.
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.
Check that it works
Section titled “Check that it works”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.
Errors when you connect
Section titled “Errors when you connect”| 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. |
Availability
Section titled “Availability”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.
Private protocol causes
Section titled “Private protocol causes”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.
Sign in to an App account
Section titled “Sign in to an App account”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.
-
Start the flow. Send the connection
revision. Leavescopesempty to use the scopes the App asks for.returnTois 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" }' -
The response is HTTP 202 with an execution receipt. Read
GET /v1/app-authorization-actions/:executionIduntil it completes, then readresult. The link is available only after the flow is saved. Reuse the same command key and body if a reply is lost:AuthorizationStartedhas aflowIdand anauthorizationUrl. Open the URL in the user’s browser.IssuerSelectionRequiredlistsissuers. Ask the user to pick one, then start again withissuer.AuthorizationNotRequiredmeans the App needs no sign-in.
-
The user signs in. The provider sends the browser to
https://api.example.com/auth/apps/callback. -
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). -
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.
Manage connections
Section titled “Manage connections”| 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.
Manual connections
Section titled “Manual connections”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.
Apps in Turns
Section titled “Apps in Turns”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
visibilitydoes not includemodelstay 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. |
Code Mode
Section titled “Code Mode”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.
App operations
Section titled “App operations”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.
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.
App views
Section titled “App views”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.
- Open the view for an operation:
POST /v1/app-operations/:operationId/viewwith{ "chatId", "itemId" }. The answer has a viewid, the HTMLresource, the view’stools, the callarguments, and theresult. - Show the HTML in an iframe. The
@openbot-build/protocol/McpAppsmodule has a browser host that does this. See Protocol. - 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, import and reset
Section titled “Export, import and reset”Export your App list. The file has no secrets and no sign-ins.
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.