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

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
}

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 Auth.

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
}

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"
# 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