Authentication
How to get a token with email and password in the community, the security check (ALTCHA) on sign-in, sign-up and recovery, send it on every call, how long and where it is valid, the public routes, acting on behalf of another member, the personal API key (X-Api-Key) with its per-module scope, IPs and per-minute limit, and how to set up a safe integration.
The Memberfy API accepts two ways in: a JWT token, which you get with an email and password, or a personal API key, created on screen and scoped by module. With the token, you exchange an email and password for it and send it on every call. Sign-in happens in a community, the one in X-CommunityId: the email and password are the person's in that community, and the token is valid only there. What the person can do depends on their role there.
Get the token
Sign-in is meant for a person in a browser: besides email and password, it carries the altcha field, the security check. A server integration does not sign in: use an API key.
curl -s -X POST https://api.memberfy.net/api/auth/login \
-H "Content-Type: application/json" \
-H "X-CommunityId: $COMMUNITY_ID" \
-d '{"email":"[email protected]","password":"your-password","altcha":"'"$ALTCHA_PAYLOAD"'"}'
Response (abridged):
{
"success": true,
"message": "Login successful.",
"data": {
"token": "eyJhbGciOi…",
"member": { "id": "…", "communityId": "…", "role": "admin", "email": "[email protected]", "fullName": "Maria Souza", "joinedAt": "…" }
}
}
Keep data.token. data.member is the session member in this community: id is the member id (the same memberId the other routes use), with communityId, role, displayName, fullName, email, avatarUrl, joinedAt and isSuperAdmin. Sign-up (POST /api/auth/register) returns the same shape.
- Without
X-CommunityId, or with an id that isn't a community:400, witherrors[0].param = "X-CommunityId". - Wrong email or password:
401· Invalid email or password. The response takes the same time whether or not an account with that email exists in the community. - Someone in more than one community has a password in each: one sign-in and one token per community.
- Without
altcha, or with an invalid one:422, witherrors[0].param = "altcha"(see below).
Security check
POST /api/auth/login, POST /api/auth/register and POST /api/auth/recovery-password require, on every call, the altcha field: the solution to an ALTCHA proof-of-work challenge, signed by the API. The browser solves it on its own, with no click and no third-party call; the Memberfy app already does this.
- Ask for a challenge:
- to sign in:
POST /api/auth/challenge, withX-CommunityIdand{"email":"[email protected]"}in the body. The challenge comes with the work that signing in with that email requires right now, which may be more than usual; - to sign up and recover the password:
GET /api/auth/challenge.
- to sign in:
- Solve it with the ALTCHA widget or library (
altcha-lib). The result is a base64 payload. - Send the payload in the call's
altchafield. Each challenge is valid for 5 minutes and works once: to try again, ask for another.
curl -s -X POST https://api.memberfy.net/api/auth/challenge \
-H "Content-Type: application/json" \
-H "X-CommunityId: $COMMUNITY_ID" \
-d '{"email":"[email protected]"}'
# { "success": true, "data": { "parameters": { "algorithm": "PBKDF2/SHA-256", … }, "signature": "…" } }
| Situation | Response |
|---|---|
altcha missing, invalid, expired or already used | 422, errors[0].param = "altcha" · Wait for the security check to finish and try again. (and the variants for invalid, expired and already used) |
| On sign-in, a lighter challenge than the one that email requires right now | 422, param = "altcha" · This sign-in needs a new security check. Try again. Ask for another challenge with POST /api/auth/challenge and resend |
| Too many attempts (challenges, sign-ins, sign-ups or recoveries) | 429 · Too many attempts…, with the Retry-After header in seconds. Wait that long before trying again |
| The check is unavailable right now | 503, param = "altcha". Try again in a moment |
A wrong password is still 401, and the right password gets in: the account is not locked by wrong attempts. The account owner gets a security email, at most one a day, when there are many attempts with the wrong password.
Use the token
On every call that isn't public:
Authorization: Bearer <token>
Just the token, after Bearer . Together with the X-CommunityId of the same community you signed in to; without it, the token's community applies.
curl -s https://api.memberfy.net/api/auth/me \
-H "Authorization: Bearer $TOKEN" \
-H "X-CommunityId: $COMMUNITY_ID"
GET /api/auth/me returns the session member right in data, in the same shape as the sign-in, plus community, fields and missingRequiredFields. There is no list of communities in this response: the person's communities come from GET /api/members/me/communities.
Lifetime
The token is valid for 7 days. There is no refresh token: when it's close to expiring, sign in again.
| Situation | Response |
|---|---|
| No token on a protected route | 401 · Authentication token is required |
| Invalid or expired token | 401 · Invalid or expired token |
Token from another community (X-CommunityId, or the body's communityId, isn't the community of the sign-in) | 401: sign in to that community |
| Token issued before the person changed the password, or in an old format | 401: sign in again |
| Insufficient role | 403 · Required role: owner, admin. Your role: member. |
These errors come in the standard envelope, with errors[] (the param is Authorization when the token is missing or invalid), and in the language of Accept-Language; without a recognized language, in English.
Public routes
These don't need a token: sign-in, sign-up, password recovery (all three with the security check), its challenges (/api/auth/challenge) and reads of what is Public in the community (public spaces, the pricing page). On many reads the token is optional: without it, you get only what is public; with it, you also get what that person can see.
Acting on behalf of another member
Owners, admins and moderators can send X-MemberId: <member id> to act as another member of the same community (to post on behalf of someone on the team, for example). Rules:
- only those three roles (
403): "You are not allowed to act as another member (X-MemberId)."; - the member has to be in the same community (
404): "Member not found in this community."; - nobody acts on behalf of an owner without being an owner;
- acting as another member doesn't lend you their role: the permissions are still yours.
API key
For an integration (a CRM, n8n, a script) to call the API without signing in, use a personal API key. It is created in Integrations & APIs, is shown only once and acts as the person who owns it, with the scope they checked.
curl -s https://api.memberfy.net/api/members \
-H "X-Api-Key: $MEMBERFY_API_KEY" \
-H "X-CommunityId: $COMMUNITY_ID"
- The key has the format
mfk_+ 8 characters +_+ the secret. Lists show only the start (mfk_7Hq2Lx9a…). - Always send
X-CommunityIdwith it, with the key's community. Without it:400,param: "X-CommunityId". With another community (in the header or in the body'scommunityId):401. - A member's key (outside the team) while Members can create API keys is off:
403. The key is not deleted and works again when the owner turns the option on. Listing or creating your own keys in that state also answers403, withparam: "memberApiKeysEnabled". - No
Authorization. Key and token on the same call:400API_KEY_WITH_AUTHORIZATION. - A key that is invalid, revoked, expired, or belongs to someone who left the community:
401. - Keys are managed through the Integrations routes (
/api/integrations/api-keys), with the sign-in token only: no key creates, edits or revokes a key.
The scope
Each operation belongs to one of the 33 modules and requires a level: read, write (create and edit) or delete (delete and destructive actions). The scope is written <module>:<level>, like members:read or bookings:delete. The reference shows each operation's scope, next to API key.
- The level follows the verb:
GETisread;POST,PUTandPATCHarewrite;DELETEisdelete. - Actions that take something offline or undo it require
deleteeven when they are aPOSTorPUT: cancel, revoke, archive, unpublish, close, stop the live stream, replace a file, decline, withdraw, discard, moderation decisions and thePUTs that replace a person's entire list of tags or fields. The full list is in Integrations & APIs. - Levels don't add up:
deletedoesn't includeread. - The effective permission is the key's scope and, at the same time, the person's current role. Each route's role check still applies: a member's key with
members:writedoesn't change anyone's role.
| Situation | Response |
|---|---|
| Route that requires sign-in, without the scope | 403 API_KEY_SCOPE_MISSING, with module, level and scope in errors[0] |
| Route that requires sign-in and belongs to no module | 403 API_KEY_ROUTE_NOT_COVERED |
| Public route (or optional sign-in), without the scope | The key is ignored and the call proceeds as anonymous: only what is public comes back |
| Public route, with the scope | Proceeds as the person who owns the key |
What no key reaches
With any scope, 403 API_KEY_BLOCKED ("This action does not accept an API key."): SuperAdmin (/api/admin), account and sign-in (/api/auth, including sign-in and password recovery, and changing a member's email and phone), OAuth and MCP, payouts, payout details and KYC, refunds, checkout and saved cards, downsell, add-on purchases (by the member and by the team), plan change (change-tier), Pay now (pay-now), platform access requests, managing the keys themselves (/api/integrations) and the X-MemberId header. The reference marks these operations with Does not accept an API key.
Private areas are also out of reach for any key: the chat and notifications (/api/chat, /api/notifications), browser and phone notifications (/api/push: devices and preferences), Feedback, development plans, Recognition, Time Tracking and Improve with AI (/api/ai).
The grid also has the channels (Channel, /api/channels) and upvotes (Upvote, /api/upvotes) modules; in upvotes, changing the state and merging topics require delete, because they can archive. And the 1:1 (/api/one-on-ones), Performance review (/api/performance-reviews) and Goals & OKRs (/api/goals) stay out of reach for any key, as private areas.
The networking module (Networking, /api/networking) requires delete to reject someone's entry to the program and to decline a session. The workflows module (Automations, /api/workflows) is read-only by key: with workflows:read, the key lists and looks up workflows, versions, run history and deliveries; any write (create, edit, turn on, pause, run, test, cancel, approve, clear) gets 403 API_KEY_BLOCKED with a key and stays in the app and the AI assistant only, which use the person's own sign-in.
Other routes belong to the module of their subject: gamification includes badges (/api/badges); members, sign-up requests (/api/signup-requests, where rejecting requires delete), contact consent (/api/contact-consent) and the member's public page (/api/public/members); campaigns, SMS and WhatsApp (/api/sms, /api/whatsapp); reports, the default emails (/api/email-overrides, next to /api/emails); events, my tickets (/api/my/tickets); and preferences, the SMS unsubscribe link.
Also assigned to a module: scheduled posts (/api/scheduled-posts) to content, with one caveat: publishing a scheduled feed post (/api/scheduled-posts/status/...) also requires feed:write, and the list only includes feed posts for keys with feed:read; member referrals (/api/referrals) to members; points (/api/points) to gamification; and the public community summary (/api/public/community-summary) to community.
Also out of reach for any key, with API_KEY_BLOCKED: the third-party secrets vault (/api/vault, with the AI limits and the data notice), the inbound address of the automations' webhook triggers (/api/workflows/{id}/triggers), the workflow hooks (/api/workflow-hooks) and the SMS provider callbacks (/api/public/sms).
Also blocked, with API_KEY_BLOCKED: the community's third-party accounts: video integrations (/api/video-integrations) and sending through your Resend account (/api/email-provider); privacy requests (/api/privacy: account deletion and data export, only from the person's own sign-in) and the app's own state (/api/menu, the side menu counters, and /api/welcome, the welcome screens).
The path is compared case-insensitively and already normalized (double slashes, trailing slash, %2F): /REFUNDS is blocked like /refunds. A path with a . or .. segment, raw or encoded (%2e), is refused with 400 API_KEY_PATH_INVALID. The same 400 applies to a path where an encoded character changes the route's module, like /api/communities/%70lans (which decoded would read plans). Accents, %20 and %2F inside a parameter (an id, a slug, a search term) still work as usual.
Allowed IPs
The key can have a list of up to 20 IPv4, IPv6 or CIDR ranges. Empty, it accepts any IP. A call from outside the list: 403 API_KEY_IP_NOT_ALLOWED, with the IP in the message; the attempt is recorded on the key (Last refused IP, on screen). The IP that counts is the one of the client reaching Memberfy; headers like X-Forwarded-For sent by the caller don't change the result.
Call limit
120 calls per minute per person in each community, adding up all their keys in that community (the same person's keys in another community count separately). Creating more keys doesn't raise the limit. Above that, 429 API_KEY_RATE_LIMITED, with the Retry-After header in seconds:
curl -si https://api.memberfy.net/api/bookings \
-H "X-Api-Key: $MEMBERFY_API_KEY" \
-H "X-CommunityId: $COMMUNITY_ID" | grep -i -E "^HTTP|retry-after"
# HTTP/2 429
# retry-after: 23
Wait the Retry-After seconds before trying again.
What counts as usage
The Usage block in Integrations & APIs counts, per month (UTC), the calls made with a valid key, including those that get a scope 403. Not counted, and not spending the limit: an invalid, revoked or expired key (401), an invalid path (400), a blocked action (API_KEY_BLOCKED), an IP outside the list and the limit refusal itself (429).
In Logs
Every write or deletion made with a key is recorded with its result, "POST /api/status via key n8n", along with creating, renaming, changing the IPs of and revoking keys. A refused write (blocked, IP, limit) shows up at most once per minute per key and reason. Read it with GET /api/activity-logs?source=apiKeys. See Logs › API keys.
Example: create a post from n8n
curl -s -X POST https://api.memberfy.net/api/status \
-H "X-Api-Key: $MEMBERFY_API_KEY" \
-H "X-CommunityId: $COMMUNITY_ID" \
-H "Content-Type: application/json" \
-d '{"spaceId":"'"$SPACE_ID"'","content":"Registration is open for the November meetup!"}'
It needs feed:write. The post is published with the key's owner as the author. Without the scope:
{
"success": false,
"message": "This API key has no create and edit permission on Feed.",
"errors": [
{ "param": "X-Api-Key", "message": "This API key has no create and edit permission on Feed.", "code": "API_KEY_SCOPE_MISSING", "module": "feed", "level": "write", "scope": "feed:write" }
]
}
Integration account
A server integration uses an API key: it has a per-module scope, allowed IPs, is revoked without changing a password and does not go through the sign-in security check. If the integration needs something no key reaches, create a member just for it:
- Invite
[email protected]with the minimum role the integration needs: Finance to read sales; Admin to create plans and products. - Keep the password in a secrets vault, never in the code.
- Sign in at the start of each run, solving the
POST /api/auth/challengechallenge withaltcha-lib, and reuse the token until it expires (7 days). Do not sign in on every call. - If the token leaks, change the account's password: tokens already issued stop working immediately.
Endpoints
GET /api/auth/challenge | Security check challenge, for sign-up and password recovery |
POST /api/auth/challenge | Challenge for signing in with an email ({ email }, with X-CommunityId) |
POST /api/auth/login | Email, password and altcha → token |
GET /api/auth/me | Who owns the token |
POST /api/auth/support-entry | Trades the link from Memberfy support's Enter for an owner session, valid only in that community (see SuperAdmin) |
POST /api/auth/register | Sign-up (fullName, email, password with at least 8 characters, an uppercase letter, a lowercase letter and a number, and altcha) |
PUT /api/auth/change-password | Change the password in this community. The person's other tokens in this community, and the AI assistant connections, stop working; the response carries a new token in data.token, so the session that changed it carries on |
POST /api/auth/recovery-password | Recover the password (email and altcha). The temporary password only opens the community where it was requested |
POST /api/auth/set-password | Set the password from the invite link (id, token, password). Answers 400 for a wrong token and 410 for a used or expired link |
Good practices
- Never put the token in the code of a public website: anyone could read it.
- Always use HTTPS (
https://api.memberfy.net). - Handle a
401by signing in again once; if it fails again, stop and raise an alert. - An integration that talks to more than one community keeps one token (or one key) per community.
- Keep the API key in a secrets vault; if it leaks, revoke it in Integrations & APIs and create another.
AI assistant (MCP) access
The owner turns the assistant on by group of roles. The fields live in the community settings:
| Field | Who uses it when true |
|---|---|
mcpMembers | members |
mcpStaff | moderator, finance and admin |
mcpAdmins | admins only |
The owner always can. All start false.
| Call | Who |
|---|---|
GET /api/communities/settings | reads: owner, admin, moderator and finance (member: 403) |
PATCH /api/communities/{id}/settings with mcpMembers, mcpStaff, mcpAdmins | writes: owner only. Others get 403; a value that is not a boolean gets 400 |
POST /api/communities/domain | returns mcpAllowed (your role may use the assistant; false when signed out) |
GET /api/oauth/authorize/info | returns mcpAllowed for each community |
With your role's switch off, authorization is refused with 403 and /mcp refuses every call, until the owner turns it back on; connections are not deleted.