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.
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" }
}
}
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.
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.). |
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 (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.
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.