Skip to content

Developer tools

For developers. Start with the developer manual to choose API work, App publishing, or project changes.

These tools are for people who publish Apps. You can check your App, prove that you control it, share access with your team, run private tests, ask for indexing, and get notices when something breaks.

Only the inspection call is public. All other calls need a user token or the session cookie, and publish on App. See Authentication.

Claim, proof, member, report, and indexing request IDs must be UUID text. This also applies to report page cursors (before) and comparison IDs (other). Both old UUID v4 and new UUID v7 values work. The API rejects other text before it reaches storage. Invitation bearer tokens and notice IDs keep their own formats.


Anyone can inspect an App. You do not need to sign in. Inspection reads only public documents. It saves nothing and does not connect to the App.

Terminal window
curl -X POST https://api.example.com/v1/developer/inspect \
-H "Content-Type: application/json" \
-d '{ "input": "apps.example.com" }'

input can be a domain or an HTTPS link, up to 8192 characters. The report has:

Field Description
apps Each App found. eligibility is Eligible, Excluded or Unconfirmed for the public Index. stored shows the last saved index check, if any.
issues Problems to fix. Each has a code, a stage, a severity (blocking or suggestion), a message, and a repair hint.
stages Each check step: Input, Discovery, CardRead, CardSchema, Compatibility, PublisherAssociation and Eligibility. Each is Passed, Failed, Unconfirmed or NotChecked.

An Eligible result does not mean the App is in the Index yet. See App Index.


Most tools need proof that you control the App. You can prove control in two ways:

Scope What it covers Where you put the token
PublisherHostname Every App whose Card is on this host name and listed in a Catalog on the same host name. A DNS TXT record
AppCard One App, by its exact Card URL. The Server Card
Terminal window
curl -X POST https://api.example.com/v1/developer/claims \
-H "Authorization: Bearer $OPENBOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "_tag": "PublisherHostname", "hostname": "apps.example.com" }'

For one App, send { "_tag": "AppCard", "cardUrl": "https://apps.example.com/mcp/server-card" }. The host name must be one exact public name, with no port, path or wildcard.

The answer has an id and a token like openbot-verification=.... The claim lasts 24 hours.

For a host name, add a DNS TXT record at _openbot-verification.<hostname>. Use the full token as the value:

_openbot-verification.apps.example.com. TXT "openbot-verification=abc123..."

For one Card, add the token to the Card:

{
"_meta": {
"openbot/developer-verification": {
"version": 1,
"tokens": ["openbot-verification=abc123..."]
}
}
}

OpenBot reads the Card at its exact URL, with no redirects. Your robots.txt must allow the OpenBot user bot.

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

The API returns 202 with an executionId. Reuse the same commandKey if a reply is lost. Read /v1/executions/:executionId or subscribe to its events. On completion, read GET /v1/developer/proofs/<claim id> for the proof. You become the administrator for that scope. A failed check has a safe error category and an errorId; its full cause stays in an encrypted error record. A missing token or an expired claim gives a conflict result. A failed network read gives an external result. Closing the event stream does not stop a check. Use the execution cancel endpoint to stop it.

The SDK has Proofs.verifyClaim(id) and Proofs.verifyProof(id). Each returns a command with a stable key and a send effect. Reuse that command to retry.

Keep the token in place. A proof is valid for 7 days, and OpenBot checks it again every day:

  • If the token is gone, the proof becomes Suspended at once.
  • If the check cannot run, the proof becomes Unconfirmed. It still works until it ends. OpenBot tries again after about one minute.
  • A Suspended proof gives no access.

To check now, call POST /v1/developer/proofs/:id/verify with a new commandKey. It also returns an execution receipt. To give up a proof, call POST /v1/developer/proofs/:id/relinquish. To cancel a claim, call POST /v1/developer/claims/:id/revoke.

GET /v1/developer returns your Apps, your proofs, and your access grants.


The administrator can invite other accounts.

Role What they can do
administrator The account that made the proof. Everything, and manage members.
maintainer Save reports, run tests, and ask for indexing.
viewer Read Apps, reports and indexing requests. No changes.
  1. Invite: POST /v1/developer/proofs/:id/invitations with { "role": "maintainer" } or "viewer". The invitation lasts 7 days.
  2. Send the invitation id to the person. They read it with GET /v1/developer/invitations/:id and accept it with POST /v1/developer/invitations/:id/accept.
  3. Manage: GET /v1/developer/proofs/:id/members, PUT /v1/developer/members/:id with a new role, and DELETE /v1/developer/members/:id. Members can also remove themselves.

To cancel an invitation, call POST /v1/developer/invitations/:id/revoke. When a proof ends, its members lose access too.


A report is a saved inspection. Use reports to see how your App changes over time.

Task Request
Save a new report POST /v1/developer/reports with { "cardUrl" }
List reports for App GET /v1/developer/reports?cardUrl=... (newest first, 50 per page, page with before)
Read one report GET /v1/developer/reports/:id
Compare two reports GET /v1/developer/reports/:id/compare?other=<id>
Delete a report you saved DELETE /v1/developer/reports/:id
List reports you saved GET /v1/developer/personal-reports

A comparison lists the issues that are introduced, resolved and persisting.


A test connects to your App from OpenBot, like a user would. It is private. It does not save a user connection, and it does not add the App to the Index. A test ends after one hour.

  1. Create a test with your own UUID: POST /v1/developer/tests with { "id", "cardUrl", "remoteIndex" }.
  2. If the endpoint needs settings, send them: PUT /v1/developer/tests/:id/configuration with an object of keys and values. The keys are the same as for connections.
  3. Run it: POST /v1/developer/tests/:id/run with { "commandKey": "test-run-1" }. It returns 202 and an execution receipt. Read the test with GET /v1/developer/tests/:id and follow the saved events with GET /v1/executions/:executionId/observe.

Keep the same command key when a reply is lost. A new key starts a new run after the last run ends. The test keeps its last executionId. The SDK helper DeveloperTests.run(id) makes a command whose send method reuses its key. Closing the event stream does not stop the test. Cancel through the execution API, or use test cleanup to stop the run and then remove its private data. Cleanup waits for the run to release its connection. A later server start does not change an active workflow run. Saved call results are reused after node loss; an unfinished read can run again.

Each run has stages: configuration, negotiation, tools, resources, resourceTemplates and prompts. Each stage is Passed, Failed, NotAdvertised or NotChecked. The test status ends as Succeeded or Failed.

When the App needs an account, the test status becomes AwaitingAuthorization.

  1. Start the sign-in: POST /v1/developer/tests/:id/authorization with { "scopes": [], "commandKey": "test-sign-in-1" }. Add issuer if the App has more than one provider. The reply is 202 with an execution receipt. Follow its events, then read the test for authorization.authorizationUrl. PreparingAuthorization means that this work has not ended. The link and sealed flow become visible in one commit. Reuse the key after a lost reply. The SDK helper DeveloperTests.authorize(id, { scopes: [] }) keeps this key.
  2. Open authorization.authorizationUrl in your browser and sign in.
  3. The provider sends you to /auth/developer/apps/callback. OpenBot then sends you to /?developerTest=<testId>.
  4. Finish: POST /v1/developer/tests/:id/authorization/complete with a new commandKey. It returns a 202 execution receipt. Retry a lost reply with the same key. DeveloperTests.completeAuthorization in the SDK keeps this key. Wait for the execution to end, then read the test and run it again.

The exchange keeps the caller that started sign-in as an access limit. It checks that caller and the caller that submits completion before using a saved result. Codes and tokens stay in sealed records. A stopped or lost request does not stop a submitted exchange. An exchange with no known result waits for a decision through the execution API; OpenBot does not send the code again on its own. A grant that cannot be installed gets platform cleanup.

When you are done, call POST /v1/developer/tests/:id/cleanup with a commandKey. The cancel endpoint uses the same body. Both return 202 with an execution receipt. DELETE /v1/developer/tests/:id?commandKey=... also returns a receipt. Keep the key when you retry a request. The SDK has DeveloperTests.cleanup, cancel, and remove helpers.

The test’s cleanupExecutionId names the service that owns expiry and cleanup. Observe it through the execution API. A test stays Cancelling while its work or token cleanup is still active. An exchange with an unknown result must be resolved through its own execution before cleanup can end. OpenBot then revokes the test sign-in where the provider allows it. It clears private data and marks cleanup complete in one transaction. A delete command removes the test in that transaction. Closing an observer does not stop cleanup.

Cleanup keeps every saved token in sealed work records before it removes the local sign-in. Shared grant cleanup owns remote calls and retries. Private test data stays until that work ends. cleanupRemote is Complete only after the provider confirms revocation. It is LocalOnly if the provider has no way to revoke the grant.

The same service owns the saved expiry timer. It also checks that the test still has App access once a minute. It can clean up after access is removed. A server restart does not reset tests or their authorization flows.


An OAuth test keeps the reason for a refused authorization step, such as FlowExpired or CallbackMismatch. An unavailable connection gives ConnectionUnavailable. A missing test record gives 404 DeveloperNotFound. A network or storage failure gives 503 DeveloperUnavailable.

You do not have to wait for a user to connect your App. Ask OpenBot to check it:

Terminal window
curl -X POST https://api.example.com/v1/developer/indexing \
-H "Authorization: Bearer $OPENBOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "cardUrl": "https://apps.example.com/mcp/server-card", "commandKey": "index-check-1" }'

OpenBot accepts the command after it checks your current rights and the App’s restriction. A blocked App gives 403 DeveloperRestricted. The public check runs in the background and saves its result.

Keep commandKey when you retry after a lost reply. The same key returns the same request. Each request has an executionId. Read or observe it through /v1/executions/:executionId. Cancel through the execution cancel endpoint. Cancelling your request does not stop the shared public check.

The request’s status describes the shared check: Waiting, Verifying, then Completed. Its check holds the result. The execution record gives the state of your request, including cancellation and failure. List requests with GET /v1/developer/indexing, or read one with GET /v1/developer/indexing/:id. A completed request keeps its result across later checks and server restarts.


OpenBot checks the Apps you manage about every hour. It sends you a notice when something changes.

Regular App and proof checks use the shared execution engine. The next check time and each result stay in PostgreSQL. A new server continues from that record. A proof change and its access notices commit in one transaction. An older check cannot replace a newer proof state. Replaying a saved App check does not send the same issue notice again.

Kind When
IssueAppeared A new blocking issue appears.
IssueResolved A blocking issue goes away.
AccessChanged A proof changes state, or you lose member access.

Read notices with GET /v1/developer/notifications. Use before to read older pages. Mark one as read with POST /v1/developer/notifications/:id/read.

Issue notices are on for every App. To turn them off for one App, send PUT /v1/developer/subscriptions with { "cardUrl": "...", "enabled": false }. List your settings with GET /v1/developer/subscriptions.

See the full shapes in the API reference.

The data move keeps existing claims, proofs, members, invitations, Apps, reports, and notice settings. App and proof checks keep their saved next check times. A relinquished proof starts no new check. Old index requests keep their IDs and get a history execution. A saved result stays complete. A request without a saved result is interrupted because its original caller is missing. The platform can still finish the separate public check.