Sign in with OneForm
AI tools and other MCP clients connect to OneForm's MCP server by having a person sign in with OneForm. It is OAuth 2.1 as the MCP authorization spec describes it: discovery, the authorization code flow with PKCE, and clients that register themselves.
How it works
- Your client calls the MCP server without a token and gets 401 with a pointer to the protected-resource metadata.
- It reads that, then the authorization server's metadata, and identifies itself (a Client ID Metadata Document, or Dynamic Client Registration).
- It sends the person to the authorize endpoint with a PKCE challenge. They sign in to OneForm, choose a firm, and choose what to allow.
- OneForm redirects back with a one-time code, which the client exchanges for an access token and a refresh token.
- The client calls
https://app.oneform.si/mcpwithAuthorization: Bearer ofa_…, and refreshes the token when it expires.
Discovery
| Protected resource (RFC 9728) | https://app.oneform.si/.well-known/oauth-protected-resource (also at …/oauth-protected-resource/mcp) |
| Authorization server (RFC 8414) | https://app.oneform.si/.well-known/oauth-authorization-server, and the same document at /.well-known/openid-configuration |
{ "issuer": "https://app.oneform.si", "authorization_endpoint": "https://app.oneform.si/oauth/authorize", "token_endpoint": "https://app.oneform.si/api/oauth/token", "registration_endpoint": "https://app.oneform.si/api/oauth/register", "revocation_endpoint": "https://app.oneform.si/api/oauth/revoke", "response_types_supported": [ "code" ], "response_modes_supported": [ "query" ], "grant_types_supported": [ "authorization_code", "refresh_token" ], "code_challenge_methods_supported": [ "S256" ], "token_endpoint_auth_methods_supported": [ "none", "client_secret_post", "client_secret_basic" ], "revocation_endpoint_auth_methods_supported": [ "none", "client_secret_post", "client_secret_basic" ], "scopes_supported": [ "forms:read", "responses:read", "reports:read", "leads:read", "workflows:read", "bookings:read", "insights:read", "integrations:read", "settings:read", "forms:write", "reports:write", "workflows:write", "bookings:write", "settings:write", "ai", "send" ], "client_id_metadata_document_supported": true, "authorization_response_iss_parameter_supported": true, "service_documentation": "https://oneform.si/developers/mcp"}A 401 from the MCP server carries WWW-Authenticate: Bearer resource_metadata="…" with the protected-resource address.
Authorization code with PKCE
Only response_type=code with PKCE S256 is accepted. resource may be left out; if sent, it must be the MCP server's address. scope is optional: without it, the consent screen starts with every read scope ticked, plus the writes and AI the person's role allows; never Act on clients.
https://app.oneform.si/oauth/authorize ?response_type=code &client_id=https%3A%2F%2Fyourapp.example%2Foauth%2Fclient.json &redirect_uri=https%3A%2F%2Fyourapp.example%2Fcallback &scope=forms%3Aread%20responses%3Aread &state=af0ifjsldkj &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256 &resource=https%3A%2F%2Fapp.oneform.si%2FmcpThe request is kept for 10 minutes while the person decides. On approval, the redirect carries code, your state and iss (RFC 9207). The code works once, for 2 minutes.
curl -X POST https://app.oneform.si/api/oauth/token \ -d grant_type=authorization_code \ -d code=<code from the redirect> \ -d code_verifier=<the verifier you made the challenge from> \ -d client_id=https://yourapp.example/oauth/client.json \ -d redirect_uri=https://yourapp.example/callback \ -d resource=https://app.oneform.si/mcp{ "access_token": "ofa_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "ofr_…", "scope": "forms:read responses:read"}Registering a client
Client ID Metadata Documents (preferred)
Use an https URL as your client_id, serving your app's metadata as JSON. OneForm reads it the first time it sees it and keeps it for as long as its Cache-Control says, between 5 minutes and 24 hours. The document's client_id must equal its own URL. Being public, it can't hold a secret: use none with PKCE.
{ "client_id": "https://yourapp.example/oauth/client.json", "client_name": "Your app", "client_uri": "https://yourapp.example", "redirect_uris": [ "https://yourapp.example/callback" ], "grant_types": [ "authorization_code", "refresh_token" ], "response_types": [ "code" ], "token_endpoint_auth_method": "none"}Dynamic Client Registration
For clients that don't support metadata documents yet (RFC 7591). The answer has a client_id starting ofk_, and a client_secret (ofs_…) only if you asked for client_secret_post or client_secret_basic. Registration is open: a registered app can do nothing until a person approves it.
curl -X POST https://app.oneform.si/api/oauth/register \ -H "Content-Type: application/json" \ -d '{"client_name":"Your app","redirect_uris":["http://127.0.0.1/callback"],"token_endpoint_auth_method":"none"}'Redirect URIs
- https anywhere, matched exactly.
- http only to this machine (127.0.0.1, [::1] or localhost), on any port, for desktop and command-line apps.
- An app's own scheme with a dot in it (com.example.app:/callback), or the editors' (cursor:, vscode:, windsurf:, claude:, zed:).
- No fragments, and no javascript:, data: or file: addresses.
Tokens
| Token | Starts with | Lasts |
|---|---|---|
| Access token | ofa_ | 60 minutes |
| Refresh token | ofr_ | 30 days, renewed with every use |
| Agent token (made by a person in Settings → AI agents) | ofp_ | 30, 90, 180, 365 days (at most 20 per person and firm) |
Tokens are opaque: don't parse them. OneForm stores only their SHA-256 hashes.
Refreshing
Each refresh token works once. Using it returns a new access token and a new refresh token; keep the new one. You may ask for a narrower scope, never a wider one.
curl -X POST https://app.oneform.si/api/oauth/token \ -d grant_type=refresh_token \ -d refresh_token=ofr_… \ -d client_id=https://yourapp.example/oauth/client.jsonRevoking
Revoking any token of a connection disconnects it (RFC 7009). Unknown tokens answer 200 too. People can also disconnect an app in OneForm under Settings → AI agents; leaving the firm or being removed from it disconnects their apps.
curl -X POST https://app.oneform.si/api/oauth/revoke \ -d token=ofr_… \ -d client_id=https://yourapp.example/oauth/client.jsonScopes
A connection never gets more than the person's role allows, checked on every call, so a change of role takes effect at once. The firm can also switch agents off or keep client data from them.
| Scope | Allows | Group |
|---|---|---|
forms:readSee forms | Forms, their questions, links and embed code | Read |
responses:readSee responses | What clients answered | Read |
reports:readSee reports | Reports and their delivery status | Read |
leads:readSee leads | People who filled in forms, with their lead score | Read |
workflows:readSee workflows | Workflows, their steps and runs | Read |
bookings:readSee bookings | Booked calls and booking pages | Read |
insights:readSee insights | Form and email statistics | Read |
integrations:readSee integrations | Which apps are connected (never their keys) | Read |
settings:readSee settings | Brand, contact details, lead scoring | Read |
forms:writeBuild and edit forms | Create, change and delete draft forms | Write |
reports:writeEdit reports | Change, re-draft and delete draft reports | Write |
workflows:writeBuild workflows | Create and change workflows (switching one on also needs Act on clients) | Write |
bookings:writeManage bookings | Notes and marking calls done or missed | Write |
settings:writeChange settings | Brand, contact details, lead scoring | Write |
aiUse OneFormAI | Generate forms and draft reports (uses your AI credits) | AI |
sendAct on clients | Send and approve reports, publish forms, email form links, change bookings, sync leads to your CRM | Act on clients |