# 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](#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](/api/x-community-id): 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](#security-check). **A server integration does not sign in**: use an [API key](#api-key).

```bash
curl -s -X POST https://api.memberfy.net/api/auth/login \
  -H "Content-Type: application/json" \
  -H "X-CommunityId: $COMMUNITY_ID" \
  -d '{"email":"maria@memberfy.net","password":"your-password","altcha":"'"$ALTCHA_PAYLOAD"'"}'
```

Response (abridged):

```json
{
  "success": true,
  "message": "Login successful.",
  "data": {
    "token": "eyJhbGciOi…",
    "member": { "id": "…", "communityId": "…", "role": "admin", "email": "maria@memberfy.net", "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`, with `errors[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`, with `errors[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](https://altcha.org) 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.

1. Ask for a challenge:
   - to **sign in**: `POST /api/auth/challenge`, with `X-CommunityId` and `{"email":"maria@memberfy.net"}` 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`.
2. Solve it with the ALTCHA widget or library (`altcha-lib`). The result is a base64 payload.
3. Send the payload in the call's `altcha` field. Each challenge is valid for 5 minutes and works **once**: to try again, ask for another.

```bash
curl -s -X POST https://api.memberfy.net/api/auth/challenge \
  -H "Content-Type: application/json" \
  -H "X-CommunityId: $COMMUNITY_ID" \
  -d '{"email":"maria@memberfy.net"}'
# { "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](/api/x-community-id) of the same community you signed in to; without it, the token's community applies.

```bash
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](/api/respostas-e-erros), 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](#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](/configuracoes/integracoes-e-apis), is shown **only once** and acts as the person who owns it, with the scope they checked.

```bash
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-CommunityId` with it**, with the key's community. Without it: `400`, `param: "X-CommunityId"`. With another community (in the header or in the body's `communityId`): `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 answers `403`, with `param: "memberApiKeysEnabled"`.
- **No `Authorization`.** Key and token on the same call: `400` `API_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](/api/referencia/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](/api/referencia) shows each operation's scope, next to **API key**.

- The level follows the verb: `GET` is `read`; `POST`, `PUT` and `PATCH` are `write`; `DELETE` is `delete`.
- Actions that take something offline or undo it require `delete` even when they are a `POST` or `PUT`: cancel, revoke, archive, unpublish, close, stop the live stream, replace a file, decline, withdraw, discard, moderation decisions and the `PUT`s that replace a person's entire list of tags or fields. The full list is in [Integrations & APIs](/configuracoes/integracoes-e-apis#what-requires-delete-and-destructive-actions).
- Levels don't add up: `delete` doesn't include `read`.
- **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:write` doesn'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](/espacos/canal), `/api/channels`) and `upvotes` ([Upvote](/espacos/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](/espacos/networking), `/api/networking`) requires `delete` to reject someone's entry to the program and to decline a session. The `workflows` module ([Automations](/notificacoes/workflows), `/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](/integracoes/integracoes-de-video) (`/api/video-integrations`) and [sending through your Resend account](/notificacoes/envio-pela-sua-conta-resend) (`/api/email-provider`); privacy requests (`/api/privacy`: [account deletion](/membros/excluir-sua-conta) and [data export](/membros/baixar-seus-dados), 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:

```bash
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](/configuracoes/integracoes-e-apis#the-usage-block) 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](/membros/registros-de-atividades#api-keys).

### Example: create a post from n8n

```bash
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:

```json
{
  "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](#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:

1. Invite `integration@memberfy.net` with the **minimum role** the integration needs: **Finance** to read sales; **Admin** to create plans and products.
2. Keep the password in a secrets vault, never in the code.
3. Sign in at the start of each run, solving the `POST /api/auth/challenge` challenge with `altcha-lib`, and reuse the token until it expires (7 days). Do not sign in on every call.
4. 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`](/api/referencia/auth/post-auth-login) | Email, password and `altcha` → token |
| [`GET /api/auth/me`](/api/referencia/auth/get-auth-me) | Who owns the token |
| [`POST /api/auth/support-entry`](/api/referencia/auth/post-auth-support-entry) | Trades the link from Memberfy support's **Enter** for an owner session, valid only in that community (see [SuperAdmin](/conceitos/papeis-e-permissoes#superadmin)) |
| [`POST /api/auth/register`](/api/referencia/auth/post-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`](/api/referencia/auth/put-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`](/api/referencia/auth/post-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 `401` by 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.

## Related

- [X-CommunityId](/api/x-community-id)
- [Integrations & APIs](/configuracoes/integracoes-e-apis)
- [Roles and permissions](/conceitos/papeis-e-permissoes)
- [JavaScript SDK](/api/sdk-js)
