Summary and recommendation
Cloudflare's member management API operates under two separate models that do not overlap.
The core REST API (`/accounts/{account_id}/members`) manages dashboard membership - who can log in and administer zones.
SCIM 2.0, available only on Cloudflare One Enterprise, manages Zero Trust user provisioning and session lifecycle.
Building an identity graph across both surfaces requires integrating both independently;
there is no unified endpoint that returns a complete picture of a user's access across dashboard roles and Zero Trust policies.
Authentication uses scoped API Tokens (Bearer) as the recommended method.
Legacy API Key + Email header authentication grants full user-level access without scope restrictions and is not recommended for new integrations.
Tokens are shown only once at creation;
store them immediately.
Rate limits apply globally at 1,200 requests per 5-minute rolling window per token across all endpoints.
No `Retry-After` header is documented in the API reference, so implement exponential backoff on HTTP 429 responses.
Pagination is offset-based with a maximum of 50 records per page (`page` / `per_page` params).
API quick reference
| Has user API | Yes |
| Auth method | API Token (Bearer) or API Key + Email header |
| Base URL | Official docs |
| SCIM available | Yes |
| SCIM plan required | Enterprise (Cloudflare One / Zero Trust Enterprise) |
Authentication
Auth method: API Token (Bearer) or API Key + Email header
Setup steps
- Log in to the Cloudflare dashboard at dash.cloudflare.com.
- Navigate to My Profile > API Tokens.
- Click 'Create Token' and select a template or build a custom token.
- Assign required permission scopes (e.g., Account > Members: Edit).
- Optionally restrict token by IP range and set an expiry.
- Copy the generated token; it is shown only once.
- Use the token as a Bearer token: Authorization: Bearer
. Alternatively, use legacy API Key with X-Auth-Key and X-Auth-Email headers (not recommended for new integrations).
Required scopes
| Scope | Description | Required for |
|---|---|---|
| Account > Members: Read | Read account member list and individual member details. | GET /accounts/{account_id}/members |
| Account > Members: Edit | Add, update, and remove account members and their roles. | POST/PUT/DELETE /accounts/{account_id}/members |
| Account > Roles: Read | List available roles that can be assigned to members. | GET /accounts/{account_id}/roles |
| User > User Details: Read | Read the authenticated user's own profile. | GET /user |
| User > User Details: Edit | Update the authenticated user's own profile. | PATCH /user |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| id | string (UUID) | Unique identifier for the account member. | system-assigned | immutable | Used as path parameter for member-specific operations. |
| user.id | string (UUID) | Cloudflare user ID of the member. | system-assigned | immutable | Distinct from the membership ID. |
| user.email | string | Email address of the member. | required | not updatable via member API | Used to invite a new member. |
| user.first_name | string | First name of the user. | optional | read-only via member API | Set by the user on their own profile. |
| user.last_name | string | Last name of the user. | optional | read-only via member API | Set by the user on their own profile. |
| user.two_factor_authentication_enabled | boolean | Whether the user has 2FA enabled. | read-only | read-only | Informational only; cannot be set via API. |
| roles | array of role objects | List of roles assigned to the member within the account. | required (at least one role ID) | replaceable via PUT | Role IDs must be retrieved from GET /accounts/{id}/roles. |
| status | string (enum) | Membership status: 'accepted', 'pending', 'rejected'. | system-assigned ('pending' until invite accepted) | read-only | New invites start as 'pending'. |
| policies | array | Fine-grained access policies (newer accounts may use policies instead of roles). | optional | replaceable via PUT | Policies provide more granular permission control than roles. |
Core endpoints
List Account Members
- Method: GET
- URL:
https://api.cloudflare.com/client/v4/accounts/{account_id}/members - Watch out for: Returns only members of the specified account. Pagination uses page/per_page; max per_page is 50.
Request example
GET /client/v4/accounts/abc123/members?page=1&per_page=20
Authorization: Bearer <token>
Response example
{
"result": [{"id":"mem1","user":{"email":"user@example.com"},"roles":[{"id":"role1","name":"Administrator"}],"status":"accepted"}],
"result_info": {"page":1,"per_page":20,"total_count":1}
}
Get Account Member
- Method: GET
- URL:
https://api.cloudflare.com/client/v4/accounts/{account_id}/members/{member_id} - Watch out for: member_id is the membership UUID, not the user UUID.
Request example
GET /client/v4/accounts/abc123/members/mem1
Authorization: Bearer <token>
Response example
{
"result": {"id":"mem1","user":{"email":"user@example.com","first_name":"Jane"},"roles":[{"id":"role1","name":"Administrator"}],"status":"accepted"}
}
Add Account Member (Invite)
- Method: POST
- URL:
https://api.cloudflare.com/client/v4/accounts/{account_id}/members - Watch out for: Invited user receives an email and must accept. Status remains 'pending' until accepted. Role IDs must be valid for the account.
Request example
POST /client/v4/accounts/abc123/members
Content-Type: application/json
{"email":"newuser@example.com","roles":["role-id-here"]}
Response example
{
"result": {"id":"mem2","user":{"email":"newuser@example.com"},"roles":[{"id":"role-id-here"}],"status":"pending"}
}
Update Account Member Roles
- Method: PUT
- URL:
https://api.cloudflare.com/client/v4/accounts/{account_id}/members/{member_id} - Watch out for: PUT replaces the entire roles array. Omitting a role removes it. Use GET first to preserve existing roles if needed.
Request example
PUT /client/v4/accounts/abc123/members/mem1
Content-Type: application/json
{"roles":[{"id":"new-role-id"}]}
Response example
{
"result": {"id":"mem1","roles":[{"id":"new-role-id","name":"Billing"}],"status":"accepted"}
}
Remove Account Member
- Method: DELETE
- URL:
https://api.cloudflare.com/client/v4/accounts/{account_id}/members/{member_id} - Watch out for: Removes the member from the account immediately. Does not delete the underlying Cloudflare user account.
Request example
DELETE /client/v4/accounts/abc123/members/mem1
Authorization: Bearer <token>
Response example
{
"result": {"id":"mem1"}
}
List Account Roles
- Method: GET
- URL:
https://api.cloudflare.com/client/v4/accounts/{account_id}/roles - Watch out for: Role IDs are account-specific. Always fetch roles for the target account before assigning.
Request example
GET /client/v4/accounts/abc123/roles
Authorization: Bearer <token>
Response example
{
"result": [{"id":"role1","name":"Administrator","description":"Full account access","permissions":{}}]
}
Get Current User (Self)
- Method: GET
- URL:
https://api.cloudflare.com/client/v4/user - Watch out for: Returns the profile of the token owner only. Cannot retrieve arbitrary users by ID via this endpoint.
Request example
GET /client/v4/user
Authorization: Bearer <token>
Response example
{
"result": {"id":"user-uuid","email":"me@example.com","first_name":"Jane","last_name":"Doe","two_factor_authentication_enabled":true}
}
Update Current User (Self)
- Method: PATCH
- URL:
https://api.cloudflare.com/client/v4/user - Watch out for: Only the authenticated user can update their own profile. Admins cannot update other users' profiles via this endpoint.
Request example
PATCH /client/v4/user
Content-Type: application/json
{"first_name":"Jane","last_name":"Smith"}
Response example
{
"result": {"id":"user-uuid","email":"me@example.com","first_name":"Jane","last_name":"Smith"}
}
Rate limits, pagination, and events
- Rate limits: Cloudflare enforces a global rate limit of 1,200 requests per 5-minute rolling window per API token. Exceeding this returns HTTP 429.
- Rate-limit headers: Yes
- Retry-After header: No
- Rate-limit notes: The API returns HTTP 429 when the limit is exceeded. Official docs note the 1,200/5-min limit but do not document specific rate-limit response headers or a Retry-After header in the API reference.
- Pagination method: offset
- Default page size: 20
- Max page size: 50
- Pagination pointer: page / per_page
| Plan | Limit | Concurrent |
|---|---|---|
| All plans | 1,200 requests per 5 minutes per token | 0 |
- Webhooks available: No
- Webhook notes: Cloudflare does not offer native webhooks for user/member management events via the core API. Cloudflare Notifications (for zone/account events like DDoS, health checks) exist but do not cover user provisioning or membership changes.
- Alternative event strategy: Use Cloudflare Audit Logs API (GET /accounts/{id}/audit_logs) to poll for membership change events. For real-time provisioning, use SCIM with a supported IdP.
SCIM API status
SCIM available: Yes
SCIM version: 2.0
Plan required: Enterprise (Cloudflare One / Zero Trust Enterprise)
Endpoint: Tenant-specific endpoint generated during IdP SCIM configuration in the Cloudflare Zero Trust dashboard (Settings > Authentication > Login methods > SCIM). The endpoint URL is unique per IdP connector and is not a static public URL.
Supported operations: Create user (POST /Users), Read user (GET /Users/{id}), List users (GET /Users), Update user (PATCH /Users/{id}), Deactivate/deprovision user (PATCH /Users/{id} with active:false), Create group (POST /Groups), Read group (GET /Groups/{id}), List groups (GET /Groups), Update group membership (PATCH /Groups/{id}), Delete group (DELETE /Groups/{id})
Limitations:
- Requires Enterprise plan for Cloudflare One (Zero Trust).
- SSO must be configured before SCIM can be enabled.
- Officially supported IdPs: Okta and Microsoft Entra ID (Azure AD). Other IdPs may work but are not officially documented.
- SCIM endpoint URL is dynamically generated per IdP setup; there is no static tenant-independent base URL.
- SCIM provisions users into Cloudflare Access/Zero Trust, not into the core Cloudflare dashboard account membership.
- Deprovisioning sets user to inactive in Access but does not delete the Cloudflare account.
- Groups synced via SCIM can be used in Access policies but group management is IdP-driven.
Common scenarios
Three scenarios cover the majority of programmatic use cases:
Invite a new member: Fetch role IDs first via GET /accounts/{account_id}/roles - role IDs are account-specific and must not be hardcoded.
POST /accounts/{account_id}/members with the target email and role ID array.
The returned status will be pending until the user accepts the invitation email;
poll GET /accounts/{account_id}/members/{member_id} to confirm accepted.
Remove a departing employee: Locate the member_id (membership UUID, distinct from user UUID) by filtering GET /accounts/{account_id}/members by user.email.
Issue DELETE /accounts/{account_id}/members/{member_id}.
This removes dashboard access only.
If the user has Cloudflare One access, their IdP account must be deprovisioned separately - or via SCIM if configured - to revoke application-level sessions.
API tokens belonging to the removed user remain valid until explicitly deleted from their personal profile.
SCIM provisioning with Okta (Enterprise): The SCIM base URL is dynamically generated per IdP connector in the Zero Trust dashboard (Settings > Authentication > Login methods).
It is not a static public URL and rotates if regenerated.
Configure Okta with the generated URL and Bearer token, enable Push Users and Push Groups, then assign users to the Cloudflare app.
Deactivation in Okta sends PATCH /Users/{id} with active:false, revoking Access sessions.
This does not remove the user as a core dashboard account member - that requires a separate DELETE /accounts/{id}/members call.
Invite a new team member with a specific role
- GET /accounts/{account_id}/roles to retrieve available role IDs for the account.
- Identify the desired role ID (e.g., 'Administrator Read Only').
- POST /accounts/{account_id}/members with body {"email": "newuser@example.com", "roles": ["
"]}. - Capture the returned membership ID and status ('pending').
- Notify the user to accept the invitation email from Cloudflare.
- Poll GET /accounts/{account_id}/members/{member_id} until status changes to 'accepted'.
Watch out for: The invitation email is sent by Cloudflare automatically. If the user already has a Cloudflare account, they accept via the dashboard. If not, they must create one first.
Remove a departing employee from the account
- GET /accounts/{account_id}/members?page=1&per_page=50 and filter by user.email to find the member_id.
- DELETE /accounts/{account_id}/members/{member_id}.
- Verify removal with GET /accounts/{account_id}/members and confirm the member no longer appears.
Watch out for: This removes dashboard access only. If the user also has Cloudflare One/Zero Trust access, their IdP account must be deprovisioned separately (or via SCIM if configured) to revoke application-level access.
Provision and deprovision users via SCIM with Okta (Enterprise)
- Ensure Cloudflare One Enterprise plan is active and SSO is configured with Okta as the identity provider.
- In Cloudflare Zero Trust dashboard: Settings > Authentication > Login methods, enable SCIM for the Okta connector.
- Copy the generated SCIM base URL and Bearer token from the Cloudflare dashboard.
- In Okta, configure the Cloudflare app's provisioning settings with the SCIM URL and token.
- Enable 'Push Users' and 'Push Groups' in Okta provisioning settings.
- Assign users/groups to the Cloudflare app in Okta; Okta will POST to the SCIM /Users endpoint.
- To deprovision, deactivate or unassign the user in Okta; Okta sends PATCH /Users/{id} with active:false, revoking Access sessions.
Watch out for: SCIM endpoint URL is unique per IdP configuration and rotates if regenerated. Deprovisioning via SCIM revokes Zero Trust/Access sessions but does not remove the user as an account member in the core Cloudflare dashboard - that requires a separate DELETE /accounts/{id}/members call.
Why building this yourself is a trap
The most consequential API caveat is the split between dashboard membership and Zero Trust identity. An identity graph built solely on /accounts/{account_id}/members will be incomplete for any organization using Cloudflare One - Zero Trust users who authenticate via Access are tracked in a separate user store and are not returned by the members endpoint.
Accurate identity coverage requires querying both surfaces and reconciling on user.email as the common key.
PUT /accounts/{account_id}/members/{member_id} replaces the entire roles array on every call. Omitting a role silently removes it. Always GET the current member record first and merge roles before issuing a PUT.
The /user endpoint returns only the token owner's profile. There is no admin endpoint to retrieve arbitrary user profiles by user ID - user details are only accessible in the context of account membership records.
This limits the API's utility for user lookup outside of a known account scope and is a structural constraint to account for when designing any identity graph or directory sync.
Automate Cloudflare workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.