Data API Reference
Integrate 3PM Auth data with your external systems. Access users, tenants, subscriptions, and members via a secure API key-authenticated REST API.
Overview
The Data API provides access to 3PM Auth data for external integrations. Use it to sync user data, tenant information, and subscription details with your CRM, analytics platform, or any other business system. You can also import users from external applications for seamless migrations.
Users
Access user profiles, emails, and authentication providers.
Tenants
Retrieve organization details, member counts, and settings.
Subscriptions
Query tenant app subscriptions and their status.
Members
Manage tenant memberships - add, update roles, assign apps, and remove members.
Invitations
Programmatically invite users to tenants via email, with bulk support.
Key Features
Authentication
All Data API endpoints require authentication via an API key. Include the key in the X-API-Key header.
API Key Format
X-API-Key: 3pm_abc123def456.xyzSecretKey789...
The API key consists of two parts separated by a dot:
- Key ID (
3pm_abc123def456) - Public identifier shown in dashboard - Secret (
xyzSecretKey789...) - Only shown once when created
Example Request
curl -X GET "https://auth.3pm.app/api/data/users" \-H "X-API-Key: 3pm_abc123.yourSecretKeyHere"
Creating API Keys
Select Scopes
Choose the permissions your integration needs:
| Scope | Access |
|---|---|
| users:read | Read user profiles and account information |
| users:write | Import/create users (for migrations) |
| tenants:read | Read tenant/organization details |
| subscriptions:read | Read tenant app subscriptions |
| members:read | Read tenant membership data |
| members:write | Add, update, and remove tenant members |
| invitations:read | Read tenant invitation data |
| invitations:write | Create, cancel, and resend invitations |
Save Your API Key
Copy and store the API key securely. It will only be shown once.
# External IntegrationDATA_API_KEY=3pm_abc123.yourSecretKeyHere
Users API
/api/data/usersList all users with pagination and optional filtering.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| updatedSince | ISO 8601 | Filter users updated after this date |
| page | number | Page number (default: 1) |
| limit | number | Items per page (default: 50, max: 100) |
Example Request
curl "https://auth.3pm.app/api/data/users?updatedSince=2026-01-01T00:00:00Z&limit=50" \-H "X-API-Key: 3pm_abc123.secret"
Response
{"data": {"data": [{"id": "507f1f77bcf86cd799439011","firstName": "John","lastName": "Doe","email": "john@example.com","mobile": "+1234567890","profilePicUrl": "https://example.com/avatar.jpg","providers": [{ "provider": "google", "providerId": "123456789" }],"lastLoginAt": "2026-01-15T10:30:00.000Z","createdAt": "2026-01-01T00:00:00.000Z","updatedAt": "2026-01-15T10:30:00.000Z"}],"pagination": {"page": 1,"limit": 50,"total": 150,"hasMore": true},"meta": {"updatedSince": "2026-01-01T00:00:00Z","requestedAt": "2026-01-23T12:00:00.000Z"}},"error": null}
/api/data/users/:idGet a single user by ID, including their tenant memberships.
Response
{"data": {"data": {"id": "507f1f77bcf86cd799439011","firstName": "John","lastName": "Doe","email": "john@example.com","tenantMemberships": [{"tenantId": "60d5ecf4f1b5a23456789012","role": "admin","assignedApps": ["3pm_app1", "3pm_app2"],"joinedAt": "2026-01-05T00:00:00.000Z"}]}},"error": null}
/api/data/usersImport a single user from an external system. Requires users:write scope. Imported users will need to verify their email via OTP on first login.
status field in the response.Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| string | Yes | User's email address | |
| firstName | string | Yes | User's first name |
| lastName | string | Yes | User's last name |
| mobile | string | No | Phone number (E.164 format) |
| profilePicUrl | string | No | URL to profile picture |
| externalId | string | No | Your system's user ID (for reference) |
Example Request
curl -X POST "https://auth.3pm.app/api/data/users" \-H "X-API-Key: 3pm_abc123.secret" \-H "Content-Type: application/json" \-d '{"email": "john@example.com","firstName": "John","lastName": "Doe","mobile": "+1234567890","externalId": "usr_12345"}'
Success Response (201 Created)
{"data": {"id": "507f1f77bcf86cd799439011","email": "john@example.com","firstName": "John","lastName": "Doe","mobile": "+1234567890","externalId": "usr_12345","status": "created","createdAt": "2026-01-24T12:00:00.000Z"},"error": null}
Duplicate Skipped Response (200 OK)
{"data": {"email": "john@example.com","status": "skipped","reason": "Email already exists","existingUserId": "507f1f77bcf86cd799439011"},"error": null}
/api/data/users/importBulk import up to 100 users at once. Requires users:write scope. Ideal for migrating users from another authentication system.
Request Body
{"users": [{"email": "user1@example.com","firstName": "John","lastName": "Doe","mobile": "+1234567890","externalId": "usr_001"},{"email": "user2@example.com","firstName": "Jane","lastName": "Smith","externalId": "usr_002"}]}
Example Request
curl -X POST "https://auth.3pm.app/api/data/users/import" \-H "X-API-Key: 3pm_abc123.secret" \-H "Content-Type: application/json" \-d '{"users": [{ "email": "user1@example.com", "firstName": "John", "lastName": "Doe" },{ "email": "user2@example.com", "firstName": "Jane", "lastName": "Smith" }]}'
Response (200 OK)
{"data": {"summary": {"total": 2,"created": 1,"skipped": 1,"failed": 0},"results": [{"email": "user1@example.com","id": "507f1f77bcf86cd799439011","status": "created","externalId": "usr_001"},{"email": "user2@example.com","status": "skipped","reason": "Email already exists","id": "507f1f77bcf86cd799439012","externalId": "usr_002"}],"meta": {"requestedAt": "2026-01-24T12:00:00.000Z"}},"error": null}
Migration Best Practices
- Batch size: Send up to 100 users per request for optimal performance
- External IDs: Include your system's user IDs in
externalIdto map users after import - Error handling: Check each result's
statusfield (created, skipped, or failed) - User notification: Inform imported users they'll need to verify their email on first login
Tenants API
/api/data/tenantsList all tenants with member counts.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| updatedSince | ISO 8601 | Filter tenants updated after this date |
| page | number | Page number (default: 1) |
| limit | number | Items per page (default: 50, max: 100) |
Response
{"data": {"data": [{"id": "60d5ecf4f1b5a23456789012","name": "Acme Corporation","slug": "acme-corp","ownerId": "507f1f77bcf86cd799439011","memberCount": 15,"createdAt": "2026-01-01T00:00:00.000Z","updatedAt": "2026-01-20T00:00:00.000Z"}],"pagination": { "page": 1, "limit": 50, "total": 10, "hasMore": false }},"error": null}
/api/data/tenants/:idGet a single tenant with full member list and subscriptions.
Response
{"data": {"data": {"id": "60d5ecf4f1b5a23456789012","name": "Acme Corporation","slug": "acme-corp","owner": {"id": "507f1f77bcf86cd799439011","firstName": "Jane","lastName": "Smith","email": "jane@acme.com"},"members": [{"userId": "507f1f77bcf86cd799439011","role": "owner","assignedApps": ["3pm_app1"],"joinedAt": "2026-01-01T00:00:00.000Z","user": { "firstName": "Jane", "lastName": "Smith", "email": "jane@acme.com" }}],"subscriptions": [{"clientId": "3pm_app1","status": "active","subscribedAt": "2026-01-01T00:00:00.000Z"}],"memberCount": 15}},"error": null}
Subscriptions API
/api/data/subscriptionsList tenant app subscriptions with filtering options.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| tenantId | string | Filter by tenant ID |
| clientId | string | Filter by application client ID |
| status | string | "active" or "suspended" |
Response
{"data": {"data": [{"id": "subscription_id","tenantId": "60d5ecf4f1b5a23456789012","tenant": { "name": "Acme Corporation", "slug": "acme-corp" },"clientId": "3pm_app1","application": { "name": "My App" },"status": "active","subscribedAt": "2026-01-01T00:00:00.000Z"}],"pagination": { "page": 1, "limit": 50, "total": 25, "hasMore": false }},"error": null}
Members API
/api/data/membersList tenant memberships with user details. Requires members:read scope.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| updatedSince | ISO 8601 | Filter members updated after this date |
| tenantId | string | Filter by tenant ID |
| role | string | "owner", "admin", or "member" |
Response
{"data": {"data": [{"id": "member_id","tenantId": "60d5ecf4f1b5a23456789012","tenant": { "name": "Acme Corporation", "slug": "acme-corp" },"userId": "507f1f77bcf86cd799439011","user": { "firstName": "John", "lastName": "Doe", "email": "john@example.com" },"role": "admin","assignedApps": ["3pm_app1", "3pm_app2"],"joinedAt": "2026-01-05T00:00:00.000Z","updatedAt": "2026-01-20T00:00:00.000Z"}],"pagination": { "page": 1, "limit": 50, "total": 100, "hasMore": true }},"error": null}
/api/data/membersAdd a user directly as a tenant member, skipping the invitation flow. Requires members:write scope. The user must already exist in the system.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| tenantId | string | Yes | Tenant ID to add the member to |
| userId | string | Yes | User ID to add as member |
| role | string | Yes | "admin" or "member" |
| assignedApps | string[] | No | App clientIds to assign |
Example Request
curl -X POST "https://auth.3pm.app/api/data/members" \-H "X-API-Key: 3pm_abc123.secret" \-H "Content-Type: application/json" \-d '{"tenantId": "60d5ecf4f1b5a23456789012","userId": "507f1f77bcf86cd799439011","role": "admin","assignedApps": ["3pm_app1"]}'
Response (201 Created)
{"data": {"id": "68a1b2c3d4e5f6a7b8c9d0e1","tenantId": "60d5ecf4f1b5a23456789012","userId": "507f1f77bcf86cd799439011","role": "admin","assignedApps": ["3pm_app1"],"joinedAt": "2026-02-10T12:00:00.000Z","updatedAt": "2026-02-10T12:00:00.000Z"},"error": null}
/api/data/members/:idGet a single member by ID with tenant and user details. Requires members:read scope.
Response
{"data": {"data": {"id": "68a1b2c3d4e5f6a7b8c9d0e1","tenantId": "60d5ecf4f1b5a23456789012","tenant": { "name": "Acme Corporation", "slug": "acme-corp" },"userId": "507f1f77bcf86cd799439011","user": { "firstName": "John", "lastName": "Doe", "email": "john@example.com" },"role": "admin","assignedApps": ["3pm_app1", "3pm_app2"],"joinedAt": "2026-01-05T00:00:00.000Z","updatedAt": "2026-01-20T00:00:00.000Z"},"meta": { "requestedAt": "2026-02-10T12:00:00.000Z" }},"error": null}
/api/data/members/:idUpdate a member's role and/or assigned apps. Requires members:write scope. At least one field must be provided.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| role | string | No* | "admin" or "member" |
| assignedApps | string[] | No* | App clientIds to assign (replaces existing) |
* At least one field is required.
Example Request
curl -X PATCH "https://auth.3pm.app/api/data/members/68a1b2c3d4e5f6a7b8c9d0e1" \-H "X-API-Key: 3pm_abc123.secret" \-H "Content-Type: application/json" \-d '{ "role": "admin", "assignedApps": ["3pm_app1", "3pm_app2"] }'
Response
{"data": {"id": "68a1b2c3d4e5f6a7b8c9d0e1","role": "admin","assignedApps": ["3pm_app1", "3pm_app2"],"updatedAt": "2026-02-10T12:00:00.000Z","changes": ["role", "assignedApps"]},"error": null}
/api/data/members/:idRemove a member from a tenant. Requires members:write scope.
Response
{"data": { "message": "Member removed successfully" },"error": null}
Invitations API
Programmatically invite users to tenants. Invitations are sent via email and expire after 7 days (configurable). Use this API to integrate your onboarding flow with 3PM Auth.
/api/data/invitationsList invitations with filtering options. Requires invitations:read scope.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| tenantId | string | Filter by tenant ID (required) |
| status | string | "pending", "accepted", or "cancelled" |
| string | Filter by invitee email | |
| page | number | Page number (default: 1) |
| limit | number | Items per page (default: 50, max: 100) |
Response
{"data": {"data": [{"id": "68a1b2c3d4e5f6a7b8c9d0e1","tenantId": "60d5ecf4f1b5a23456789012","tenant": { "name": "Acme Corporation", "slug": "acme-corp" },"email": "newuser@example.com","role": "member","assignedApps": ["3pm_app1"],"invitedBy": "000000000000000000000000","inviterName": "API","status": "pending","isExpired": false,"expiresAt": "2026-02-17T12:00:00.000Z","createdAt": "2026-02-10T12:00:00.000Z"}],"pagination": { "page": 1, "limit": 50, "total": 5, "hasMore": false }},"error": null}
/api/data/invitationsCreate and send a single invitation. Requires invitations:write scope. An email will be sent to the invitee with a link to accept the invitation.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| tenantId | string | Yes | Tenant to invite the user to |
| string | Yes | Invitee's email address | |
| role | string | Yes | "admin" or "member" |
| assignedApps | string[] | No | App clientIds to auto-assign when invitation is accepted |
Example Request
curl -X POST "https://auth.3pm.app/api/data/invitations" \-H "X-API-Key: 3pm_abc123.secret" \-H "Content-Type: application/json" \-d '{"tenantId": "60d5ecf4f1b5a23456789012","email": "newuser@example.com","role": "member","assignedApps": ["3pm_app1"]}'
Response (201 Created)
{"data": {"id": "68a1b2c3d4e5f6a7b8c9d0e1","tenantId": "60d5ecf4f1b5a23456789012","email": "newuser@example.com","role": "member","assignedApps": ["3pm_app1"],"status": "pending","expiresAt": "2026-02-17T12:00:00.000Z","createdAt": "2026-02-10T12:00:00.000Z"},"error": null}
/api/data/invitations/bulkCreate up to 50 invitations in a single request. Requires invitations:write scope. Each invitation is processed individually - duplicates are skipped, not treated as errors.
Request Body
{"tenantId": "60d5ecf4f1b5a23456789012","invitations": [{ "email": "user1@example.com", "role": "member", "assignedApps": ["3pm_app1"] },{ "email": "user2@example.com", "role": "admin" },{ "email": "user3@example.com", "role": "member", "assignedApps": ["3pm_app1", "3pm_app2"] }]}
Response
{"data": {"summary": { "total": 3, "created": 2, "skipped": 1, "failed": 0 },"results": [{"email": "user1@example.com","status": "created","id": "68a1b2c3d4e5f6a7b8c9d0e1"},{"email": "user2@example.com","status": "skipped","reason": "Already a tenant member"},{"email": "user3@example.com","status": "created","id": "68a1b2c3d4e5f6a7b8c9d0e2"}]},"error": null}
/api/data/invitations/:idGet details of a single invitation. Requires invitations:read scope. Includes a computed isExpired boolean.
Response
{"data": {"data": {"id": "68a1b2c3d4e5f6a7b8c9d0e1","tenantId": "60d5ecf4f1b5a23456789012","tenant": { "name": "Acme Corporation", "slug": "acme-corp" },"email": "newuser@example.com","role": "member","assignedApps": ["3pm_app1"],"invitedBy": "000000000000000000000000","inviterName": "API","status": "pending","isExpired": false,"expiresAt": "2026-02-17T12:00:00.000Z","createdAt": "2026-02-10T12:00:00.000Z"},"meta": { "requestedAt": "2026-02-10T12:00:00.000Z" }},"error": null}
/api/data/invitations/:idCancel a pending invitation. Requires invitations:write scope. Only pending invitations can be cancelled.
Response
{"data": { "message": "Invitation cancelled successfully" },"error": null}
/api/data/invitations/:id/resendResend the invitation email and extend the expiry date. Requires invitations:write scope. Only pending, non-expired invitations can be resent.
410 Gone if the invitation has expired. Cancel it and create a new one instead.Response
{"data": {"message": "Invitation email resent","expiresAt": "2026-02-17T12:00:00.000Z"},"error": null}
Pagination & Filtering
Pagination
All list endpoints support pagination with page and limit parameters.
# Get page 2 with 25 items per pagecurl "https://auth.3pm.app/api/data/users?page=2&limit=25" \-H "X-API-Key: 3pm_abc123.secret"
Incremental Sync with updatedSince
Use updatedSince to fetch only records updated after a specific time. This is ideal for syncing data to external systems.
# Get users updated in the last 24 hourscurl "https://auth.3pm.app/api/data/users?updatedSince=2026-01-22T00:00:00Z" \-H "X-API-Key: 3pm_abc123.secret"
requestedAt timestamp from the response and use it as updatedSince for your next sync.Rate Limiting
Data API endpoints are rate limited to prevent abuse. The current limits are:
100 requests per minute
Per API key
Rate Limit Headers
Every response includes rate limit information in the headers:
X-RateLimit-Limit: 100X-RateLimit-Remaining: 95X-RateLimit-Reset: 60
Handling Rate Limits
When rate limited, you'll receive a 429 response. Implement exponential backoff:
async function fetchWithRetry(url, options, maxRetries = 3) {for (let i = 0; i < maxRetries; i++) {const response = await fetch(url, options);if (response.status === 429) {const resetIn = response.headers.get('X-RateLimit-Reset') || 60;console.log(`Rate limited. Waiting ${resetIn} seconds...`);await new Promise(resolve => setTimeout(resolve, resetIn * 1000));continue;}return response;}throw new Error('Max retries exceeded');}
Error Handling
Error Responses
| Status | Error | Description |
|---|---|---|
| 400 | Bad Request | Invalid parameters (e.g., invalid date format) |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | API key lacks required scope |
| 404 | Not Found | Resource not found |
| 409 | Conflict | Duplicate resource (e.g., user already a member, pending invitation exists) |
| 410 | Gone | Resource expired (e.g., expired invitation on resend) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Internal server error |
Error Response Format
{"data": null,"error": "Insufficient permissions. Required scope: users:read"}
Code Examples
Node.js / TypeScript
const API_URL = 'https://auth.3pm.app';const API_KEY = process.env.DATA_API_KEY!;interface SyncResult {users: any[];lastSync: string;}async function syncUsers(lastSync?: string): Promise<SyncResult> {const url = new URL(`${API_URL}/api/data/users`);if (lastSync) {url.searchParams.set('updatedSince', lastSync);}url.searchParams.set('limit', '100');const users: any[] = [];let page = 1;let hasMore = true;while (hasMore) {url.searchParams.set('page', page.toString());const response = await fetch(url, {headers: { 'X-API-Key': API_KEY },});if (!response.ok) {throw new Error(`API error: ${response.status}`);}const result = await response.json();users.push(...result.data.data);hasMore = result.data.pagination.hasMore;page++;}return {users,lastSync: new Date().toISOString(),};}// Usageconst { users, lastSync } = await syncUsers('2026-01-22T00:00:00Z');console.log(`Synced ${users.length} users`);
Python
import osimport requestsfrom datetime import datetimeAPI_URL = 'https://auth.3pm.app'API_KEY = os.environ['DATA_API_KEY']def sync_users(updated_since=None):headers = {'X-API-Key': API_KEY}params = {'limit': 100, 'page': 1}if updated_since:params['updatedSince'] = updated_sinceusers = []has_more = Truewhile has_more:response = requests.get(f'{API_URL}/api/data/users',headers=headers,params=params)response.raise_for_status()data = response.json()['data']users.extend(data['data'])has_more = data['pagination']['hasMore']params['page'] += 1return users, datetime.utcnow().isoformat() + 'Z'# Usageusers, last_sync = sync_users('2026-01-22T00:00:00Z')print(f'Synced {len(users)} users')
cURL Examples
# List all userscurl "https://auth.3pm.app/api/data/users" \-H "X-API-Key: 3pm_abc123.secret"# Get users updated since yesterdaycurl "https://auth.3pm.app/api/data/users?updatedSince=2026-01-22T00:00:00Z" \-H "X-API-Key: 3pm_abc123.secret"# Get a specific tenant with memberscurl "https://auth.3pm.app/api/data/tenants/60d5ecf4f1b5a23456789012" \-H "X-API-Key: 3pm_abc123.secret"# Get active subscriptions for a tenantcurl "https://auth.3pm.app/api/data/subscriptions?tenantId=60d5ecf4f1b5a23456789012&status=active" \-H "X-API-Key: 3pm_abc123.secret"# Get all admins across tenantscurl "https://auth.3pm.app/api/data/members?role=admin" \-H "X-API-Key: 3pm_abc123.secret"# Add a member directly to a tenantcurl -X POST "https://auth.3pm.app/api/data/members" \-H "X-API-Key: 3pm_abc123.secret" \-H "Content-Type: application/json" \-d '{"tenantId": "60d5ecf4f1b5a23456789012", "userId": "507f1f77bcf86cd799439011", "role": "member"}'# Update a member's rolecurl -X PATCH "https://auth.3pm.app/api/data/members/68a1b2c3d4e5f6a7b8c9d0e1" \-H "X-API-Key: 3pm_abc123.secret" \-H "Content-Type: application/json" \-d '{"role": "admin"}'# Remove a membercurl -X DELETE "https://auth.3pm.app/api/data/members/68a1b2c3d4e5f6a7b8c9d0e1" \-H "X-API-Key: 3pm_abc123.secret"# Invite a user to a tenantcurl -X POST "https://auth.3pm.app/api/data/invitations" \-H "X-API-Key: 3pm_abc123.secret" \-H "Content-Type: application/json" \-d '{"tenantId": "60d5ecf4f1b5a23456789012", "email": "new@example.com", "role": "member", "assignedApps": ["3pm_app1"]}'# Bulk invite userscurl -X POST "https://auth.3pm.app/api/data/invitations/bulk" \-H "X-API-Key: 3pm_abc123.secret" \-H "Content-Type: application/json" \-d '{"tenantId": "60d5ecf4f1b5a23456789012", "invitations": [{"email": "a@example.com", "role": "member", "assignedApps": ["3pm_app1"]}, {"email": "b@example.com", "role": "admin"}]}'# List pending invitations for a tenantcurl "https://auth.3pm.app/api/data/invitations?tenantId=60d5ecf4f1b5a23456789012&status=pending" \-H "X-API-Key: 3pm_abc123.secret"# Resend an invitationcurl -X POST "https://auth.3pm.app/api/data/invitations/68a1b2c3d4e5f6a7b8c9d0e1/resend" \-H "X-API-Key: 3pm_abc123.secret"# Cancel an invitationcurl -X DELETE "https://auth.3pm.app/api/data/invitations/68a1b2c3d4e5f6a7b8c9d0e1" \-H "X-API-Key: 3pm_abc123.secret"
Data API v1.0 - January 2026