Data API Documentation

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

Get tenant membership data with roles and app assignments.

Key Features

API KeysSecure authentication with configurable scopes
updatedSinceFilter by update time for incremental syncs
PaginationEfficient data retrieval with page/limit controls
Rate Limiting100 requests per minute per API key

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
Important: Store API keys securely. They are only shown once when created.

Example Request

curl -X GET "https://idp.3pm.app/api/data/users" \
-H "X-API-Key: 3pm_abc123.yourSecretKeyHere"

Creating API Keys

1

Navigate to API Keys

Go to the admin dashboard and click on "API Keys" in the sidebar.

2

Select Scopes

Choose the permissions your integration needs:

ScopeAccess
users:readRead user profiles and account information
users:writeImport/create users (for migrations)
tenants:readRead tenant/organization details
subscriptions:readRead tenant app subscriptions
members:readRead tenant membership data
3

Save Your API Key

Copy and store the API key securely. It will only be shown once.

.envenv
# External Integration
DATA_API_KEY=3pm_abc123.yourSecretKeyHere

Users API

GET/api/data/users

List all users with pagination and optional filtering.

Query Parameters
ParameterTypeDescription
updatedSinceISO 8601Filter users updated after this date
pagenumberPage number (default: 1)
limitnumberItems per page (default: 50, max: 100)
Example Request
curl "https://idp.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
}
GET/api/data/users/:id

Get 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
}
POST/api/data/users

Import a single user from an external system. Requires users:write scope. Imported users will need to verify their email via OTP on first login.

Note: Duplicate emails are skipped, not treated as errors. Check the status field in the response.
Request Body
FieldTypeRequiredDescription
emailstringYesUser's email address
firstNamestringYesUser's first name
lastNamestringYesUser's last name
mobilestringNoPhone number (E.164 format)
profilePicUrlstringNoURL to profile picture
externalIdstringNoYour system's user ID (for reference)
Example Request
curl -X POST "https://idp.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
}
POST/api/data/users/import

Bulk import up to 100 users at once. Requires users:write scope. Ideal for migrating users from another authentication system.

Rate limit: 10 requests per minute (stricter than other endpoints). Plan your migration batches accordingly.
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://idp.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 externalId to map users after import
  • Error handling: Check each result's status field (created, skipped, or failed)
  • User notification: Inform imported users they'll need to verify their email on first login

Tenants API

GET/api/data/tenants

List all tenants with member counts.

Query Parameters
ParameterTypeDescription
updatedSinceISO 8601Filter tenants updated after this date
pagenumberPage number (default: 1)
limitnumberItems 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
}
GET/api/data/tenants/:id

Get 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

GET/api/data/subscriptions

List tenant app subscriptions with filtering options.

Query Parameters
ParameterTypeDescription
tenantIdstringFilter by tenant ID
clientIdstringFilter by application client ID
statusstring"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

GET/api/data/members

List tenant memberships with user details.

Query Parameters
ParameterTypeDescription
updatedSinceISO 8601Filter members updated after this date
tenantIdstringFilter by tenant ID
rolestring"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
}

Pagination & Filtering

Pagination

All list endpoints support pagination with page and limit parameters.

# Get page 2 with 25 items per page
curl "https://idp.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 hours
curl "https://idp.3pm.app/api/data/users?updatedSince=2026-01-22T00:00:00Z" \
-H "X-API-Key: 3pm_abc123.secret"
Pro tip: Store the 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: 100
X-RateLimit-Remaining: 95
X-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

StatusErrorDescription
400Bad RequestInvalid parameters (e.g., invalid date format)
401UnauthorizedMissing or invalid API key
403ForbiddenAPI key lacks required scope
404Not FoundResource not found
429Too Many RequestsRate limit exceeded
500Server ErrorInternal server error

Error Response Format

{
"data": null,
"error": "Insufficient permissions. Required scope: users:read"
}

Code Examples

Node.js / TypeScript

data-sync.tstypescript
const API_URL = 'https://idp.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(),
};
}
// Usage
const { users, lastSync } = await syncUsers('2026-01-22T00:00:00Z');
console.log(`Synced ${users.length} users`);

Python

data_sync.pypython
import os
import requests
from datetime import datetime
API_URL = 'https://idp.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_since
users = []
has_more = True
while 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'] += 1
return users, datetime.utcnow().isoformat() + 'Z'
# Usage
users, last_sync = sync_users('2026-01-22T00:00:00Z')
print(f'Synced {len(users)} users')

cURL Examples

# List all users
curl "https://idp.3pm.app/api/data/users" \
-H "X-API-Key: 3pm_abc123.secret"
# Get users updated since yesterday
curl "https://idp.3pm.app/api/data/users?updatedSince=2026-01-22T00:00:00Z" \
-H "X-API-Key: 3pm_abc123.secret"
# Get a specific tenant with members
curl "https://idp.3pm.app/api/data/tenants/60d5ecf4f1b5a23456789012" \
-H "X-API-Key: 3pm_abc123.secret"
# Get active subscriptions for a tenant
curl "https://idp.3pm.app/api/data/subscriptions?tenantId=60d5ecf4f1b5a23456789012&status=active" \
-H "X-API-Key: 3pm_abc123.secret"
# Get all admins across tenants
curl "https://idp.3pm.app/api/data/members?role=admin" \
-H "X-API-Key: 3pm_abc123.secret"

Data API v1.0 - January 2026