If you are building an MCP server, AI connector, or integrating from a third-party platform like ChatGPT, you can set up OAuth access to Formify without contacting us. This guide covers two self-service registration methods — pick the one that fits your client.
This page covers self-service registration — both public clients (no secret) and confidential clients (with client_secret) via DCR. If you need a manually provisioned confidential client, see Getting Access.
Choose your registration method
Formify supports two self-service registration methods. Public clients use PKCE (S256) instead of a client_secret. DCR also supports confidential clients with client_secret_basic.
| Option A: Metadata Document | Option B: Dynamic Client Registration (DCR) | |
|---|---|---|
| client_id format | HTTPS URL | GUID |
| How it works | You host a JSON document at a public HTTPS URL. That URL is your client_id. | You call POST /oauth/register and receive a GUID client_id. |
| Requires hosting | Yes — a public HTTPS endpoint | No |
| Registration | Automatic on first use | Explicit API call (RFC 7591) |
| Client types | Public only | Public or confidential |
| Best for | MCP servers, self-hosted tools | Third-party platforms, clients that cannot host a metadata document |
Option A: Client ID Metadata Document
Host a JSON file at a publicly accessible HTTPS URL, for example https://mcp.example.com/.well-known/oauth-client.json. That URL becomes your client_id.
How it works
- You host a Client ID Metadata Document (JSON) at an HTTPS URL.
- That URL is your
client_id— no GUID, no secret. - Your client uses PKCE (Proof Key for Code Exchange) instead of a
client_secret. - Formify fetches your metadata document to learn your app name, logo, and allowed redirect URIs.
Example metadata document
{
"client_id": "https://mcp.example.com/.well-known/oauth-client.json",
"client_name": "My MCP Connector",
"client_uri": "https://mcp.example.com",
"logo_uri": "https://mcp.example.com/logo.png",
"redirect_uris": [
"https://mcp.example.com/oauth/callback"
],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"scope": "read write"
}
Metadata fields
| Field | Required | Description |
|---|---|---|
| client_id | Yes | Must exactly match the URL where this document is hosted. |
| client_name | Yes | Human-readable name shown to the user during authorization. |
| redirect_uris | Yes | One or more allowed callback URLs (must be absolute HTTPS URLs). |
| client_uri | Recommended | Link to the application's homepage. |
| logo_uri | Recommended | URL to the application's logo, displayed during authorization consent. |
| token_endpoint_auth_method | Recommended | Must be "none" for public clients. |
| scope | Optional | Space-separated list of scopes the client intends to request. |
| contacts | Optional | Contact email addresses. |
| tos_uri | Optional | Link to Terms of Service. |
| policy_uri | Optional | Link to Privacy Policy. |
Hosting requirements
- Must be served over HTTPS.
- The URL must include a path (e.g.
/.well-known/oauth-client.json). A bare domain likehttps://example.com/is not valid. - Must return
Content-Type: application/json. - Must respond quickly and return a reasonably small document.
- The
client_idvalue inside the document must exactly match the hosting URL.
Caching: Formify caches metadata documents. If you change redirect URIs or other fields, allow time for the cache to expire before the changes take effect.
Option B: Dynamic Client Registration (DCR)
If you cannot host a metadata document (e.g. ChatGPT, other third-party platforms), you can register your client programmatically via the DCR endpoint (RFC 7591). You make a single API call and receive a GUID-based client_id.
Endpoint
POST https://docs-api.formify.eu/v1/oauth/register Content-Type: application/json
Request fields
| Field | Required | Description |
|---|---|---|
| client_name | Yes | Human-readable name shown to users during authorization. |
| redirect_uris | Yes | Array of allowed callback URLs. Must be absolute HTTPS URLs without fragments. Max 10 URIs. |
| token_endpoint_auth_method | Recommended | "none" for public clients or "client_secret_basic" for confidential clients. Defaults to "none". |
| grant_types | Optional | Informational. Array of grant types the client intends to use. Supported values: "authorization_code", "refresh_token". Defaults to ["authorization_code"]. |
| response_types | Optional | Array of response types. Supported: "code". Defaults to ["code"]. |
| client_uri | Optional | URL to the application's homepage. |
| logo_uri | Optional | URL to the application's logo, displayed during authorization consent. |
| tos_uri | Optional | Link to Terms of Service. |
| policy_uri | Optional | Link to Privacy Policy. |
| scope | Optional | Space-separated list of scopes the client intends to request. Max 1024 characters. |
| contacts | Optional | Array of contact email addresses. Max 5 entries. |
Validation rules
- Redirect URIs: Max 10. Must use HTTPS. Must not contain URL fragments.
- String fields (
client_name,token_endpoint_auth_method, etc.): Max 512 characters. - URI fields (
client_uri,logo_uri,tos_uri,policy_uri): Max 2048 characters. - Scope: Max 1024 characters.
- Contacts: Max 5 email addresses.
Rate limiting
Registration is rate limited. If you exceed the limit, the API returns 429 Too Many Requests.
Example: Register a public client
curl -X POST "https://docs-api.formify.eu/v1/oauth/register" \
-H "Content-Type: application/json" \
-d '{
"client_name": "My AI Assistant",
"redirect_uris": ["https://app.example.com/oauth/callback"],
"token_endpoint_auth_method": "none",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read write",
"client_uri": "https://app.example.com",
"contacts": ["dev@example.com"]
}'
Response (public client)
{
"client_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"client_name": "My AI Assistant",
"redirect_uris": ["https://app.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none",
"client_id_issued_at": 1713700000
}
Response (confidential client)
If token_endpoint_auth_method is "client_secret_basic", the response also includes:
{
"client_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"client_name": "My Integration",
"redirect_uris": ["https://app.example.com/oauth/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "client_secret_basic",
"client_id_issued_at": 1713700000,
"client_secret": "super-secret-value",
"client_secret_expires_at": 0
}
Store credentials securely. The client_secret is only returned once at registration time. If you lose it, you must register a new client. client_secret_expires_at: 0 means the secret does not expire.
Response fields
| Field | Type | Description |
|---|---|---|
| client_id | String (GUID) | The registered client identifier. |
| client_name | String | The registered client name. |
| redirect_uris | Array | The registered redirect URIs. |
| grant_types | Array | The registered grant types. |
| response_types | Array | The registered response types. |
| token_endpoint_auth_method | String | "none" or "client_secret_basic". |
| client_id_issued_at | Integer | Unix timestamp when the client was registered. |
| client_secret | String | Only for confidential clients. The client secret. |
| client_secret_expires_at | Integer | Only for confidential clients. 0 means no expiration. |
Implement PKCE
PKCE is required for all public clients (both metadata document and DCR). Generate a fresh code_verifier for every authorization request.
- Generate a random
code_verifier(43–128 characters, using[A-Za-z0-9\-._~]). - Compute the
code_challenge:BASE64URL(SHA256(code_verifier)) - Send
code_challengeandcode_challenge_method=S256in the authorization request. - Send the original
code_verifierwhen exchanging the code for tokens.
JavaScript
const codeVerifier = generateRandomString(64); // 64 URL-safe random chars
async function generateCodeChallenge(verifier) {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest("SHA-256", data);
return btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
const codeChallenge = await generateCodeChallenge(codeVerifier);
Python
import hashlib, base64, secrets
code_verifier = secrets.token_urlsafe(64)
code_challenge = (
base64.urlsafe_b64encode(
hashlib.sha256(code_verifier.encode("ascii")).digest()
)
.rstrip(b"=")
.decode("ascii")
)
Request authorization
Redirect the user to Formify's authorization endpoint. The user signs in and approves access. Formify validates your client, verifies the redirect URI, and redirects the user back with an authorization code.
Authorize URL:
https://app.formify.eu/oauth ?client_id=YOUR_CLIENT_ID &redirect_uri=https://example.com/oauth/callback &response_type=code &scope=read%20write &state=RANDOM_STATE_STRING &code_challenge=BASE64URL_SHA256_HASH &code_challenge_method=S256
client_id is either your metadata document URL (Option A) or your GUID (Option B).
On approval, the user is redirected to your callback URL:
https://example.com/oauth/callback?code=AUTH_CODE&state=RANDOM_STATE_STRING
Authorization codes expire after 10 minutes and are single-use. Always verify the state parameter matches what you sent.
Exchange code for tokens
Public clients (PKCE)
No client_secret is needed — send the code_verifier instead.
curl -X POST "https://docs-api.formify.eu/v1/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTH_CODE_HERE" \ -d "redirect_uri=https://example.com/oauth/callback" \ -d "client_id=YOUR_CLIENT_ID" \ -d "code_verifier=YOUR_CODE_VERIFIER"
Confidential clients (DCR)
Authenticate with HTTP Basic using your client_id and client_secret.
curl -X POST "https://docs-api.formify.eu/v1/oauth/token" \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=authorization_code" \ -d "code=AUTH_CODE_HERE" \ -d "redirect_uri=https://example.com/oauth/callback"
Response:
{
"access_token": "<jwt_token>",
"expires_in": 3600,
"token_type": "Bearer",
"refresh_token": "<refresh_token>",
"scope": "read write"
}
Use the access token
Include the token in the Authorization header for all API requests:
Authorization: Bearer <access_token>
See the API Reference for available endpoints.
Refresh tokens
Access tokens expire after 1 hour. Use the refresh token to get a new one. Refresh tokens are valid for 60 days.
Public clients
curl -X POST "https://docs-api.formify.eu/v1/oauth/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=YOUR_REFRESH_TOKEN" \ -d "client_id=YOUR_CLIENT_ID"
Confidential clients
curl -X POST "https://docs-api.formify.eu/v1/oauth/token" \ -u "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token" \ -d "refresh_token=YOUR_REFRESH_TOKEN"
Revoke tokens
Public clients can revoke refresh tokens when access is no longer needed:
curl -X POST "https://docs-api.formify.eu/v1/oauth/revoke" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "client_id=YOUR_CLIENT_ID" \ -d "token=YOUR_REFRESH_TOKEN" \ -d "token_type_hint=refresh_token"
Server metadata discovery
Clients that support OAuth Authorization Server Metadata (RFC 8414) can discover Formify's endpoints automatically:
GET https://docs-api.formify.eu/.well-known/oauth-authorization-server
Response:
{
"issuer": "https://docs-api.formify.eu",
"authorization_endpoint": "https://app.formify.eu/oauth",
"token_endpoint": "https://docs-api.formify.eu/v1/oauth/token",
"revocation_endpoint": "https://docs-api.formify.eu/v1/oauth/revoke",
"registration_endpoint": "https://docs-api.formify.eu/v1/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["client_secret_basic", "none"],
"code_challenge_methods_supported": ["S256"],
"client_id_metadata_document_supported": true
}
Production checklist
All public clients
- Generate a fresh
code_verifierfor every authorization request and useS256. - Always use the
stateparameter for CSRF protection. - Store refresh tokens securely and refresh access tokens before they expire.
Metadata document clients (Option A)
- Serve your metadata document over HTTPS and ensure it responds within 5 seconds.
- Verify
client_idmatches the hosting URL exactly. - Include a meaningful
client_nameandlogo_uri— these are shown to users during authorization. - Handle caching — if you change redirect URIs, allow up to 24 hours for the cache to expire.
DCR-registered clients (Option B)
- PKCE (S256) is mandatory for public clients — generate a fresh
code_verifierfor every authorization request. - Public clients have no
client_secret— protect yourclient_idand never embed it in untrusted environments unnecessarily. - Confidential DCR clients: store
client_secretsecurely — it is only returned once at registration time. - DCR registration is rate limited — plan your registration flow accordingly.