Start free
Developers

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

  1. Your client calls the MCP server without a token and gets 401 with a pointer to the protected-resource metadata.
  2. It reads that, then the authorization server's metadata, and identifies itself (a Client ID Metadata Document, or Dynamic Client Registration).
  3. 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.
  4. OneForm redirects back with a one-time code, which the client exchanges for an access token and a refresh token.
  5. The client calls https://app.oneform.si/mcp with Authorization: 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
oauth-authorization-server
{  "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.

GET /oauth/authorize
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%2Fmcp

The 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.

Exchange the code
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
200 · application/json
{  "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.

https://yourapp.example/oauth/client.json
{  "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.

Register
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

TokenStarts withLasts
Access tokenofa_60 minutes
Refresh tokenofr_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.

Refresh
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.json

Revoking

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.

Revoke
curl -X POST https://app.oneform.si/api/oauth/revoke \  -d token=ofr_… \  -d client_id=https://yourapp.example/oauth/client.json

Scopes

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.

ScopeAllowsGroup
forms:read
See forms
Forms, their questions, links and embed codeRead
responses:read
See responses
What clients answeredRead
reports:read
See reports
Reports and their delivery statusRead
leads:read
See leads
People who filled in forms, with their lead scoreRead
workflows:read
See workflows
Workflows, their steps and runsRead
bookings:read
See bookings
Booked calls and booking pagesRead
insights:read
See insights
Form and email statisticsRead
integrations:read
See integrations
Which apps are connected (never their keys)Read
settings:read
See settings
Brand, contact details, lead scoringRead
forms:write
Build and edit forms
Create, change and delete draft formsWrite
reports:write
Edit reports
Change, re-draft and delete draft reportsWrite
workflows:write
Build workflows
Create and change workflows (switching one on also needs Act on clients)Write
bookings:write
Manage bookings
Notes and marking calls done or missedWrite
settings:write
Change settings
Brand, contact details, lead scoringWrite
ai
Use OneFormAI
Generate forms and draft reports (uses your AI credits)AI
send
Act on clients
Send and approve reports, publish forms, email form links, change bookings, sync leads to your CRMAct on clients

Questions about the API, webhooks or embeds? Ask a developer.