Data API Documentation

Data API Reference

Integrate 3PM Accounts 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 Accounts 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

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://auth.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
members:writeAdd, update, and remove tenant members
invitations:readRead tenant invitation data
invitations:writeCreate, cancel, and resend invitations
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://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
}
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://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
}
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://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 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
}
PATCH/api/data/tenants/:id

Update a tenant's details. Requires tenants:write scope. At least one field must be provided. The slug is automatically regenerated when the name changes.

Request Body
FieldTypeRequiredDescription
namestringYesNew tenant name (min 2 characters)
Example Request
curl -X PATCH "https://auth.3pm.app/api/data/tenants/60d5ecf4f1b5a23456789012" \
-H "X-API-Key: 3pm_abc123.secret" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Industries" }'
Response
{
"data": {
"data": {
"id": "60d5ecf4f1b5a23456789012",
"name": "Acme Industries",
"slug": "acme-industries",
"ownerId": "507f1f77bcf86cd799439011",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-03-10T12:00:00.000Z"
}
},
"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. Requires members:read scope.

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

Add a user directly as a tenant member, skipping the invitation flow. Requires members:write scope. The user must already exist in the system.

Note: Cannot add a user with the "owner" role. Use "admin" or "member".
Request Body
FieldTypeRequiredDescription
tenantIdstringYesTenant ID to add the member to
userIdstringYesUser ID to add as member
rolestringYes"admin" or "member"
assignedAppsstring[]NoApp 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
}
GET/api/data/members/:id

Get 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
}
PATCH/api/data/members/:id

Update a member's role and/or assigned apps. Requires members:write scope. At least one field must be provided.

Important: Cannot change the role of a tenant owner, and cannot set role to "owner".
Request Body
FieldTypeRequiredDescription
rolestringNo*"admin" or "member"
assignedAppsstring[]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
}
DELETE/api/data/members/:id

Remove a member from a tenant. Requires members:write scope.

Protection: Tenant owners cannot be removed. Returns 403 if attempted.
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 Accounts.

GET/api/data/invitations

List invitations with filtering options. Requires invitations:read scope.

Query Parameters
ParameterTypeDescription
tenantIdstringFilter by tenant ID (required)
statusstring"pending", "accepted", or "cancelled"
emailstringFilter by invitee email
pagenumberPage number (default: 1)
limitnumberItems 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
}
POST/api/data/invitations

Create 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
FieldTypeRequiredDescription
tenantIdstringYesTenant to invite the user to
emailstringYesInvitee's email address
rolestringYes"admin" or "member"
assignedAppsstring[]NoApp 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
}
Security: The invitation token is never exposed in API responses. It is only included in the email sent to the invitee.
POST/api/data/invitations/bulk

Create up to 50 invitations in a single request. Requires invitations:write scope. Each invitation is processed individually - duplicates are skipped, not treated as errors.

Rate limit: 10 requests per minute (stricter than other endpoints).
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
}
GET/api/data/invitations/:id

Get 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
}
DELETE/api/data/invitations/:id

Cancel a pending invitation. Requires invitations:write scope. Only pending invitations can be cancelled.

Response
{
"data": { "message": "Invitation cancelled successfully" },
"error": null
}
POST/api/data/invitations/:id/resend

Resend the invitation email and extend the expiry date. Requires invitations:write scope. Only pending, non-expired invitations can be resent.

Note: Returns 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
}

Credits API

One AI credit wallet per app in each organisation: every call names the app with clientId, and an app only ever spends its own wallet. The monthly allowance comes from that app's plan (the trial plan during the trial); bonus credits and packs bought for the app never expire. Spending draws the allowance first, then bonus, then purchased. Which users may use AI is the app's own permission, checked before it spends. Scopes: credits:read for balance and transactions, credits:spend for spend and refund (what an app needs), credits:grant for adding credits (admin tools and migrations only).

GET/api/data/credits/balance?tenantId=…&clientId=…

The app's wallet right now. clientId is required (400 without it). When the org has no live subscription to the app — not subscribed, suspended, or a free trial that ended — the answer is entitled: false.

{
"data": {
"entitled": true,
"period": "2026-09",
"allowance": 100000,
"used": 120,
"freeRemaining": 99880,
"bonus": 0,
"purchased": 2000,
"totalBalance": 101880,
"totalUsed": 120,
"plan": { "id": "pro", "name": "Pro" }
},
"error": null
}
POST/api/data/credits/spend

Takes credits for one action. Exactly once per idempotencyKey: a retry returns the original booking with replayed: true. Responds 402 with data.remaining when the wallet can't cover it and 403 when the org has no live subscription.

curl -X POST https://accounts.3pm.app/api/data/credits/spend \
-H "X-API-Key: 3pm_abc123.secret" \
-H "Content-Type: application/json" \
-d '{ "tenantId": "60d5ecf4f1b5a23456789012", "cost": 1, "feature": "chat", "idempotencyKey": "9f1c…", "clientId": "3pm_app1", "actor": { "userId": "…" } }'
{
"data": {
"spendId": "66f0…",
"split": { "fromFree": 1, "fromBonus": 0, "fromPurchased": 0 },
"remaining": 101879,
"replayed": false
},
"error": null
}
POST/api/data/credits/refund

Body { "tenantId", "spendId" }. Gives a spend back to the buckets it came from — call it when the charged action failed. Safe to repeat.

POST/api/data/credits/grant

Body { "tenantId", "clientId", "amount", "description", "idempotencyKey", "bucket"?, "actor"? }. Adds non-expiring credits to that app's wallet: bucket "bonus" (default, e.g. earned in a setup guide) or "purchased" (carrying an old local balance over). Exactly once per key. Packs are bought on the pricing page, not here.

GET/api/data/credits/transactions?tenantId=…&limit=20

The ledger, newest first: allowances, packs, usage, refunds, expirations.

{
"data": {
"data": [
{ "id": "66f0…", "type": "usage", "description": "chat (1 credit)", "amount": -1, "balanceAfter": 101879, "feature": "chat", "actor": { "userId": "…" }, "createdAt": "2026-09-25T10:00:00.000Z" },
{ "id": "66ef…", "type": "purchase", "description": "Pack pack (2,000 credits)", "amount": 2000, "balanceAfter": 102000, "packId": "pack", "price": 50, "paid": false, "createdAt": "2026-09-25T09:58:00.000Z" }
]
},
"error": null
}
GET/api/data/credits/packs

The AI credit packs 3PM Accounts sells, and the one currency they are priced in (scope credits:read). Read this instead of keeping your own copy.

{
"data": {
"currency": "NZD",
"packs": [
{ "id": "topup", "name": "Top up", "credits": 500, "price": 20 },
{ "id": "pack", "name": "Pack", "credits": 2000, "price": 50 }
]
},
"error": null
}

Errors

Every Credits API error has a stable code to branch on and a plain message the app can show its users as it is. error keeps its earlier text for older apps — don't parse it.

{
"data": null,
"error": "APP_NOT_SUBSCRIBED",
"code": "APP_NOT_SUBSCRIBED",
"message": "Your organisation doesn't have this app, so it has no AI credits for it."
}
codeStatusWhen
INSUFFICIENT_CREDITS402Spend: the wallet can't cover the cost. data.remaining says what is left.
APP_NOT_SUBSCRIBED403 / 404The organisation doesn't have this app (spend, grant).
TENANT_NOT_FOUND403 / 404No such organisation.
APP_NOT_FOUND403The clientId isn't a registered app.
SPEND_NOT_FOUND404Refund: no such spend for this organisation.
SPEND_NOT_REFUNDABLE404Refund: the spend has no app to give it back to.
INVALID_REQUEST400A field is missing or has a wrong value (error has the detail).
PRICING_NOT_SET_UP503Packs: pricing isn't set up in 3PM Accounts yet.
RATE_LIMITED429Too many requests — try again shortly.
INTERNAL_ERROR500Something went wrong on our side — safe to retry with the same idempotencyKey.

A missing or wrong API key, or a key without the scope, is answered by the shared API-key check (see Error Handling). To hear about credits added to your app's wallet without asking, use the credits.purchased / credits.granted webhooks.

Plans & Entitlements

Admins create any number of plans per app in the dashboard and set, per plan, a number for each limit and on/off for each feature. The app says what those are, and checks them where things are created. The ready-made helper is lib/sdk/entitlements.ts in 3PM Accounts — copy it into the app.

PUT/api/data/applications/capabilities

The app publishes what its plans can limit. It authenticates with its own client ID and secret (no API key), so it can only change its own list. Each call replaces what it published before. Set enforced once the app really checks the key — the dashboard shows unchecked ones with a warning. aiCredits is built in.

{
"clientId": "3pm_app1",
"clientSecret": "…",
"capabilities": [
{ "key": "projects", "label": "Projects", "type": "limit", "enforced": true },
{ "key": "customWorkflows", "label": "Custom workflows", "type": "feature", "enforced": true }
]
}
GET/api/data/entitlements?tenantId=…&clientId=…

What the org's plan allows in this app right now — the trial plan until the trial ends, then the plan it bought. Every declared limit is a number or null (unlimited); every feature is on or off. Scope: subscriptions:read. Block creating once the count reaches the limit; never delete what already exists.

{
"data": {
"entitled": true,
"plan": { "id": "starter", "name": "Starter" },
"onTrial": false,
"trialEndsAt": null,
"limits": { "aiCredits": 5000, "projects": 20, "workspaces": 2 },
"features": { "customWorkflows": false }
},
"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://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 hours
curl "https://auth.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
409ConflictDuplicate resource (e.g., user already a member, pending invitation exists)
410GoneResource expired (e.g., expired invitation on resend)
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://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(),
};
}
// 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://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_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://auth.3pm.app/api/data/users" \
-H "X-API-Key: 3pm_abc123.secret"
# Get users updated since yesterday
curl "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 members
curl "https://auth.3pm.app/api/data/tenants/60d5ecf4f1b5a23456789012" \
-H "X-API-Key: 3pm_abc123.secret"
# Update a tenant's name
curl -X PATCH "https://auth.3pm.app/api/data/tenants/60d5ecf4f1b5a23456789012" \
-H "X-API-Key: 3pm_abc123.secret" \
-H "Content-Type: application/json" \
-d '{"name": "Acme Industries"}'
# Get active subscriptions for a tenant
curl "https://auth.3pm.app/api/data/subscriptions?tenantId=60d5ecf4f1b5a23456789012&status=active" \
-H "X-API-Key: 3pm_abc123.secret"
# Get all admins across tenants
curl "https://auth.3pm.app/api/data/members?role=admin" \
-H "X-API-Key: 3pm_abc123.secret"
# Add a member directly to a tenant
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": "member"}'
# Update a member's role
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"}'
# Remove a member
curl -X DELETE "https://auth.3pm.app/api/data/members/68a1b2c3d4e5f6a7b8c9d0e1" \
-H "X-API-Key: 3pm_abc123.secret"
# Invite a user to a tenant
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": "new@example.com", "role": "member", "assignedApps": ["3pm_app1"]}'
# Bulk invite users
curl -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 tenant
curl "https://auth.3pm.app/api/data/invitations?tenantId=60d5ecf4f1b5a23456789012&status=pending" \
-H "X-API-Key: 3pm_abc123.secret"
# Resend an invitation
curl -X POST "https://auth.3pm.app/api/data/invitations/68a1b2c3d4e5f6a7b8c9d0e1/resend" \
-H "X-API-Key: 3pm_abc123.secret"
# Cancel an invitation
curl -X DELETE "https://auth.3pm.app/api/data/invitations/68a1b2c3d4e5f6a7b8c9d0e1" \
-H "X-API-Key: 3pm_abc123.secret"

Data API v1.0 - January 2026