Authentication
Connect your service to the right customer workspace with a Bearer API key. Create the key in maxclicks, store it on your server, and verify its space before reading or changing customer data. The base URL is https://api.maxclicks.ai/v1.
An app that acts for a maxclicks user, such as an AI assistant, can instead connect with OAuth and send the access token the user approves.
A key is an opaque secret prefixed with max_. maxclicks matches it by hash, so the full value is shown once, at creation. Treat it as a password: keep it server-side, never in client code or a repository.
Send the key
Put the key in the Authorization header on every authenticated request.
cURL
curl https://api.maxclicks.ai/v1/me \
-H "Authorization: Bearer $MAXCLICKS_API_KEY"
Set MAXCLICKS_API_KEY in your server environment or secret manager. Send the standard Bearer prefix on every authenticated request.
Verify a key
GET /v1/me returns the calling key, its owning user, the bound space, and the caller's role. It works for space-bound and space-unbound keys, so use it to confirm a key and discover its context. Send it exactly as shown above: no body, no query parameters.
The response wraps a single data object. space and role are null for a space-unbound key.
Response: 200
{
"data": {
"apiKey": {
"id": "max_BcwDvBUeSaSDILA5tHgpmU7I",
"name": "Production key",
"keyDisplay": "max_...3a9f",
"createdAt": "2026-01-15T10:30:00.000Z"
},
"user": {
"id": "usr_BcwDvBUeSaSDILA5tHgpmU7I",
"email": "[email protected]",
"name": "Jordan Lee"
},
"space": {
"id": "spc_BcwDvBUeSaSDILA5tHgpmU7I",
"slug": "your-company",
"name": "Your Company"
},
"role": { "name": "member", "label": "Member", "description": "Can edit" },
"oauth": null
}
}
For an OAuth access token, oauth reports the app's clientId (its registered id, or its metadata document URL), the resource the token was issued for, its granted scopes and its expiry, and apiKey describes the connection, named after the app. It is null for an API key.
Roles and permissions
A key acts as the membership its owning user holds in the resolved space. Each membership has a role, and each role grants a set of permissions: read, write, and admin. Endpoints are gated by one permission.
| Role | Permissions |
|---|---|
admin | read, write, admin |
non-paid admin | read, admin |
member | read, write |
- Name
read- Description
- Required by all list and retrieve endpoints.
- Name
write- Description
Required by endpoints that create, update, delete, or upsert records, fire events, manage segments and webhooks, send a stored template or a broadcast, and trigger or pause a workflow.
- Name
admin- Description
- Required by the suppression endpoints.
If the resolved membership lacks the required permission, the request returns 403 with a message such as This operation needs "write" permission.
Space resolution
Most endpoints act on one space, resolved from the key's owner:
- A space-bound key uses its bound space. If that space is out of the caller's memberships, the request returns
403 Space is out of scope. - A space-unbound key resolves to the owner's single membership. If the owner has more than one membership, the request returns
400 unknown_space(Unknown space.). - If the owner has no matching membership, the request returns
403.
GET /v1/me is the exception: it does not resolve a membership and never enforces a permission, so it works for any valid key.
Manage keys
Keys are created, named, and revoked in the app: open a space, go to Settings, then Developers, and use the API keys tab.
Keys inherit their owner's permissions in the space. There is no key-management API. To rotate a credential, create a replacement, update your service, verify it with GET /v1/me, and then revoke the old key. Complete or reconcile in-flight idempotent operations before switching the key used to retry them.
To confirm which key a running service is using, call GET /v1/me with it. That returns the key's masked display value, its owner, the bound space, and the role, which is enough to identify a key in the dashboard list without ever sending the plaintext back.
The plaintext key is shown once, at creation, in the dialog that creates it.
After that only the masked keyDisplay (for example max_...ab12) is
visible, in the app and in GET /v1/me. Copy it into your secret store before
closing the dialog. If you lose it, revoke the key and create another.
Connect an app with OAuth
An app that acts for a maxclicks user gets an access token through OAuth 2.1 instead of being handed a key. The user signs in to maxclicks, chooses one workspace and what the app may do, and the app receives a token for that connection. MCP clients do this on their own when they meet the MCP server's 401; the steps below are what any client implements.
Discover the server
The authorization server metadata (RFC 8414) lists every endpoint and capability:
cURL
curl https://api.maxclicks.ai/.well-known/oauth-authorization-server
A 401 from the API names its resource metadata (RFC 9728) in the WWW-Authenticate header, https://api.maxclicks.ai/.well-known/oauth-protected-resource/v1, which points at the same server. PKCE with S256 is required, and every authorization response carries the issuer as iss (RFC 9207).
Identify the app with a metadata document
An app can skip registration: it publishes a JSON document about itself at an https URL and uses that URL as its client_id (OAuth Client ID Metadata Documents, which the MCP authorization specification recommends). The server metadata advertises it with client_id_metadata_document_supported: true, and the same client_id works with every server that supports it.
https://acme.example/oauth/client.json
{
"client_id": "https://acme.example/oauth/client.json",
"client_name": "Acme assistant",
"redirect_uris": ["http://127.0.0.1:8976/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
maxclicks reads the document when a user starts connecting, and stops with an error page that names the problem when the document cannot be used:
- The
client_idis anhttpsURL with a path, written in canonical form (a lowercase host, no default port, no.or..segments), with no fragment and no credentials. The document's ownclient_idmust be exactly that URL. - The URL answers
200within 5 seconds, asapplication/jsonor another+jsontype, in at most 5 KB. A redirect is not followed. - The document includes
client_nameandredirect_uris, under the same rules as registration below. It contains noclient_secret, andtoken_endpoint_auth_methodisnoneor omitted: the app is a public client and presents itsclient_idwithout a secret at the token and revocation endpoints.grant_typesandresponse_types, when present, includeauthorization_codeandcode. - A valid document is kept for its
Cache-Control: max-age, between 5 minutes and 24 hours, or 1 hour when it sends none. A document that fails is never kept, so fix it and connect again.
The consent screen shows the document's host as Details from, beside the app's name and where it returns.
Register the app
An app without a metadata document registers with dynamic client registration (RFC 7591). An app that cannot keep a secret sets token_endpoint_auth_method to none:
cURL
curl https://api.maxclicks.ai/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "Acme assistant",
"redirect_uris": ["http://127.0.0.1:8976/callback"],
"token_endpoint_auth_method": "none"
}'
Response: 201
{
"client_id": "oac_...",
"client_id_issued_at": 1792982615,
"client_name": "Acme assistant",
"redirect_uris": ["http://127.0.0.1:8976/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
A redirect URI is an https URL, an http URL on localhost, 127.0.0.1 or [::1], or an app's private-use scheme, with no fragment and no credentials. A loopback redirect may use any port at authorization time. Omitting token_endpoint_auth_method registers a confidential client and returns a client_secret, which the app then presents with HTTP Basic or as client_secret in the form body.
Send the user to approve
Open the authorization endpoint in the user's browser:
Authorization request
https://api.maxclicks.ai/oauth/authorize
?response_type=code
&client_id=oac_...
&redirect_uri=http://127.0.0.1:8976/callback
&code_challenge=BASE64URL(SHA256(code_verifier))
&code_challenge_method=S256
&scope=read%20write
&state=RANDOM_STATE
&resource=https://api.maxclicks.ai/v1
client_id is the registered oac_... id or the metadata document URL. scope lists read, write and admin; when omitted it means read write. resource is the server the token is for: https://api.maxclicks.ai/v1 for this API or https://mcp.maxclicks.ai/mcp for the MCP server; when omitted it means the MCP server. The user signs in, chooses a workspace where they hold write, and grants some or all of the requested scopes. maxclicks asks every time.
After approval the browser returns to redirect_uri with code, state and iss. Check that state is the one you sent. A code is valid for two minutes, once. A malformed request shows the user an error page instead of redirecting. A declined request returns error=access_denied only to an app on the user's device; a website receives nothing.
Exchange the code
cURL
curl https://api.maxclicks.ai/oauth/token \
-d grant_type=authorization_code \
-d client_id=oac_... \
-d code=mxc_... \
-d code_verifier=YOUR_CODE_VERIFIER \
-d redirect_uri=http://127.0.0.1:8976/callback
Response: 200
{
"access_token": "mxa_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "mxr_...",
"scope": "read write"
}
An app identified by a metadata document sends its document URL as client_id, here and when it refreshes or revokes. Send the access token the same way as an API key: Authorization: Bearer mxa_.... It works only at the resource it was issued for.
Refresh and revoke
Renew the access token before its hour runs out:
cURL
curl https://api.maxclicks.ai/oauth/token \
-d grant_type=refresh_token \
-d client_id=oac_... \
-d refresh_token=mxr_...
Every refresh returns a new refresh token. Store it and discard the old one: presenting an old refresh token again ends the connection, because it may have been copied. A refresh token unused for 30 days expires. To end a connection, revoke its refresh token (RFC 7009). The endpoint answers 200 whether or not the token existed.
cURL
curl https://api.maxclicks.ai/oauth/revoke \
-d client_id=oac_... \
-d token=mxr_...
The user can also disconnect the app in maxclicks: open a space, go to Settings, then Developers, and delete the connection on the API keys tab, where it is listed with the app's name and a code such as OAuth ยท claude.ai.
What a connection can do
A connection acts as the approving user in its one workspace. Its permissions are the scopes the user granted, limited again by the user's current role. When the role has a permission the connection was not granted, the request returns 403 with code: insufficient_scope; connect again and grant it.
The OAuth endpoints answer errors the OAuth way (RFC 6749, section 5.2): { "error": "invalid_grant", "error_description": "..." }, with invalid_client as 401.
Errors
Authentication and authorization failures use the standard error envelope. type is invalid_request_error for 4xx and api_error for 5xx. code is a stable machine string, or null when none applies.
Error envelope
{
"error": {
"type": "invalid_request_error",
"code": "invalid_api_key",
"message": "No API key provided."
}
}
| Status | code | When |
|---|---|---|
401 | invalid_api_key | No key sent (No API key provided.) or the key matched none on file (Invalid API key.). |
401 | invalid_access_token | An OAuth access token that expired or was revoked, or whose connection ended. Refresh it, or connect again. |
403 | insufficient_scope | An OAuth connection whose user holds the permission but did not grant it to this connection. |
403 | null | Valid key, but the membership lacks the permission, has no membership for the space, or the bound space is out of scope. |
400 | unknown_space | The owner has more than one membership, so the space is ambiguous. |
429 | rate_limit_exceeded | The key exceeded its rate-limit bucket. |
Rate limits
Authenticated routes are limited per key or OAuth access token (hashed Authorization header, falling back to client IP). There are three buckets:
| Bucket | Limit | Applies to |
|---|---|---|
read | 100 / second | Read endpoints. |
write | 25 / second | Mutating endpoints. |
ai | 10 / minute | LLM-backed endpoints. |
Exceeding a bucket returns 429 with code: rate_limit_exceeded and a Retry-After header naming when to try again.
Every response from a rate-limited route reports its budgets in the RateLimit-Policy and RateLimit headers (IETF draft-ietf-httpapi-ratelimit-headers). RateLimit-Policy names each limit that applies, with its quota q and window w in seconds. RateLimit gives what remains: r requests until the window resets in t seconds. Slow down when r nears zero instead of waiting for a 429.
Response headers
RateLimit-Policy: "public-v1:read";q=100;w=1
RateLimit: "public-v1:read";r=99;t=1
An endpoint metered in more than one way, such as the unauthenticated ones below, lists one item per limit.
Unauthenticated endpoints
Three public endpoints take no API key and resolve their tenant from the resource, not a key:
| Endpoint | Purpose |
|---|---|
POST /v1/forms/{formId}/submit | Anonymous form submission. |
GET /v1/forms/{formId}/confirm/{token} | Double opt-in confirmation link. |
POST /v1/emails/{emailId}/unsubscribe | RFC 8058 one-click unsubscribe. |
Form endpoints use separate limits. The one-click unsubscribe endpoint does not use a rate limiter.