Skip to main content
Every request except the public /.well-known/* discovery documents must carry an Authorization: Bearer <token> header. The token is validated by introspection against doola’s IAM service, and the resolved identity (customerId, email, role) is attached to the request.
No tool takes a customerId argument — identity rides the bearer. The active companyId is resolved on demand from the authenticated customer.

Discovery

A request without a valid bearer returns 401 with a challenge that points clients at the OAuth metadata, per RFC 9728 and RFC 8414:
MCP clients follow it automatically:

OAuth endpoints

Advertised on the configured issuer:
  • Grants: authorization_code, client_credentials, refresh_token.
  • PKCE: S256 supported.
Most MCP clients handle this end to end — you add the server URL, click connect, and complete the sign-in. The endpoints above matter only if you implement the OAuth flow yourself.
New users complete signup at the doola web app during the OAuth flow. If the user isn’t signed in, send them there before any tool call.

Session lifecycle

The transport is stateless, and the server serves both protocol eras. Which one applies is decided per request, by the version the request declares. On 2026-07-28 there is no handshake and no session. Send tools/list or tools/call directly, carrying the required _meta fields and the matching request-metadata headers:
with io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities in params._meta. Send io.modelcontextprotocol/clientInfo too — on this revision it is the only way the server can attribute the call to your client. On 2025-11-25 and earlier the handshake still works exactly as before:
  • The server mints a fresh Mcp-Session-Id (UUID) on every initialize and returns it as a response header.
  • Echo that header on subsequent requests. It is no longer required, and a request without it is served rather than rejected, but the server still uses it to tie the calls of one conversation together.
  • A new initialize mints a new ID — there is no cross-conversation continuity at the transport layer. Continuity is anchored to the OAuth identity instead.
Call server/discover to read the revisions this server implements before choosing. It also returns the server’s capabilities and instructions — on 2026-07-28 that is the only place the instructions are served, since there is no initialize response to carry them.

Checkout round trip

On 2026-07-28, if your client declares URL-mode elicitation ("elicitation": {"url": {}}), create_checkout_session returns an interim result rather than a finished one:
Show the customer the URL, take their consent, and open it. Then retry the same create_checkout_session call with requestState echoed back verbatim and {"action": "accept"} under inputResponses.payment_confirmation. Use a new JSON-RPC id: the retry is an independent request. Do not inspect or construct requestState. An acceptance means the customer agreed to open the link, not that they paid. The server asks Stripe what actually happened, so the retry returns the finished payment result only when Stripe says the session is paid; otherwise it hands back checkoutSessionId and steers you to verify_payment. Declining or cancelling does the same, which is what a client running without a human to ask should expect. Otherwise the two-action flow applies: create_checkout_session returns checkoutSessionId, and you pass it back to verify_payment yourself. This is the only piece of conversation state a client on that path must thread. A client that declares form-mode elicitation only gets this flow too, since a form request cannot carry a usable checkout link.