1. Overview
The TimelinesAI Partner API enables B2B partners to programmatically provision and manage workspaces, users, and WhatsApp account connections. This server-to-server API allows partners to integrate TimelinesAI capabilities directly into their own platforms without requiring end-users to interact with the TimelinesAI UI.
Key Capabilities
Create and manage workspaces with configurable seat allocations
Provision users as placeholders for WhatsApp account connections
Generate QR code links for WhatsApp device linking
Manage Public API tokens for workspace integrations
Receive webhook notifications for quota and account events
2. Authentication
The Partner API uses JWT (JSON Web Token) bearer authentication. Each request must include a valid JWT token generated using your Partner Secret.
Credentials
You will receive two credentials from TimelinesAI:
Partner ID: Your unique partner identifier (e.g., 'your-company-name')
Partner Secret: A secure key used to sign JWT tokens (keep this confidential)
JWT Token Generation
Generate a JWT token with the following payload structure. The token must be signed using HS256 algorithm.
JavaScript / Node.js
const moment = require('moment');
const jwt = require('jsonwebtoken');
// Configuration
const PARTNER_ID = 'your-partner-id';
const PARTNER_SECRET = 'your-partner-secret';
const BASE_URL = 'https://app.timelines.ai/partner/api';
// Generate JWT token
function generateToken() {
const exp = moment().add(3, 'm').unix(); // Expires in 3 minutes
const nbf = moment().subtract(3, 'm').unix(); // Valid from 3 min ago
const payload = {
exp: exp,
nbf: nbf,
partner_id: PARTNER_ID
};
return jwt.sign(payload, PARTNER_SECRET);
}
// Make authenticated request
async function apiRequest(method, endpoint, body = null) {
const token = generateToken();
const options = {
method: method,
headers: {
'Authorization': `Bearer ${token}`,
'X-TL-Partner-Id': PARTNER_ID,
'Content-Type': 'application/json'
}
};
if (body) options.body = JSON.stringify(body);
const response = await fetch(`${BASE_URL}${endpoint}`, options);
return response.json();
}
Python
import jwt
import requests
from datetime import datetime, timedelta
PARTNER_ID = 'your-partner-id'
PARTNER_SECRET = 'your-partner-secret'
BASE_URL = 'https://app.timelines.ai/partner/api'
def generate_token():
now = datetime.utcnow()
payload = {
'partner_id': PARTNER_ID,
'nbf': int((now - timedelta(minutes=3)).timestamp()),
'exp': int((now + timedelta(minutes=3)).timestamp())
}
return jwt.encode(payload, PARTNER_SECRET, algorithm='HS256')
def api_request(method, endpoint, json_body=None):
token = generate_token()
headers = {
'Authorization': f'Bearer {token}',
'X-TL-Partner-Id': PARTNER_ID,
'Content-Type': 'application/json'
}
url = f'{BASE_URL}{endpoint}'
response = requests.request(method, url, headers=headers, json=json_body)
return response.json()
Request Headers
Include the following headers in all API requests:
Header | Value |
Authorization | Bearer <jwt_token> |
X-TL-Partner-Id | your-partner-id |
Content-Type | application/json |
3. Base URL
Production: https://app.timelines.ai/partner/api/v1
4. API Endpoints with Code Examples
4.1 Create Workspace
Creates a new partner-managed workspace with specified seats.
Endpoint: POST /v1/workspaces/
cURL
curl -X POST 'https://app.timelines.ai/partner/api/v1/workspaces/' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'X-TL-Partner-Id: your-partner-id' \
-H 'Content-Type: application/json' \
-d '{
"display_name": "my-workspace",
"seats_purchased": 5
}'
JavaScript
const workspace = await apiRequest('POST', '/v1/workspaces/', {
display_name: 'my-workspace',
seats_purchased: 5
});
console.log('Created workspace:', workspace.workspace_id);Python
workspace = api_request('POST', '/v1/workspaces/', {
'display_name': 'my-workspace',
'seats_purchased': 5
})
print(f"Created workspace: {workspace['workspace_id']}")Response
{
"workspace_id": "my-workspace",
"display_name": "my-workspace",
"seats_total": 5,
"seats_available": 4,
"owner_user_id": 12345,
"created_at": "2024-12-01T10:00:00Z"
}Important: Save the workspace_id - you'll need it for all subsequent operations.
4.2 Update Workspace
Updates workspace display name and/or seat count.
Endpoint: PATCH /v1/workspaces/{workspace_id}/
cURL
curl -X PATCH 'https://app.timelines.ai/partner/api/v1/workspaces/my-workspace/' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'X-TL-Partner-Id: your-partner-id' \
-H 'Content-Type: application/json' \
-d '{
"display_name": "updated-workspace",
"seats_purchased": 10
}'
JavaScript
const updated = await apiRequest('PATCH', '/v1/workspaces/my-workspace/', {
display_name: 'updated-workspace',
seats_purchased: 10
});Python
updated = api_request('PATCH', '/v1/workspaces/my-workspace/', {
'display_name': 'updated-workspace',
'seats_purchased': 10
})Note: Reducing seats below current usage may suspend users and disconnect their WhatsApp accounts.
4.3 Get Workspace Summary
Retrieves detailed workspace status including quotas, users, and WhatsApp accounts.
Endpoint: GET /v1/workspaces/{workspace_id}/
cURL
curl -X GET 'https://app.timelines.ai/partner/api/v1/workspaces/my-workspace/' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'X-TL-Partner-Id: your-partner-id'
JavaScript
const summary = await apiRequest('GET', '/v1/workspaces/my-workspace/');
console.log('Seats used:', summary.seats.used, '/', summary.seats.total);
console.log('Connected WA accounts:', summary.whatsapp_accounts.length);Python
summary = api_request('GET', '/v1/workspaces/my-workspace/')
print(f"Seats: {summary['seats']['used']}/{summary['seats']['total']}")
print(f"WhatsApp accounts: {len(summary['whatsapp_accounts'])}")Response
{
"workspace_id": "my-workspace",
"display_name": "my-workspace",
"seats": { "total": 5, "used": 2, "available": 3, "full": false },
"messages_quota": { "total": 100000, "used": 45000 },
"api_calls_quota": { "total": 1000000, "used": 25000 },
"whatsapp_accounts": [
{ "user_id": 100, "connected": true, "phone_number_e164": "+14155551234" }
],
"users": [...]
}
4.4 Create Users (Batch)
Creates placeholder users that can have WhatsApp accounts connected. Each user consumes one seat.
Endpoint: POST /v1/workspaces/{workspace_id}/users/
cURL
curl -X POST 'https://app.timelines.ai/partner/api/v1/workspaces/my-workspace/users/' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'X-TL-Partner-Id: your-partner-id' \
-H 'Content-Type: application/json' \
-d '{"count": 3}'
JavaScript
const users = await apiRequest('POST', '/v1/workspaces/my-workspace/users/', {
count: 3
});
// Store user IDs for QR generation
const userIds = users.users.map(u => u.user_id);
console.log('Created users:', userIds);Python
users = api_request('POST', '/v1/workspaces/my-workspace/users/', {
'count': 3
})
user_ids = [u['user_id'] for u in users['users']]
print(f'Created users: {user_ids}')Response
{
"workspace_id": "my-workspace",
"seats_total": 5,
"seats_available": 1,
"users": [
{ "user_id": 100, "display_name": "Agent", "role": "Agent", "status": "Active" },
{ "user_id": 101, "display_name": "Agent", "role": "Agent", "status": "Active" },
{ "user_id": 102, "display_name": "Agent", "role": "Agent", "status": "Active" }
]
}
4.5 Generate QR Code Link
Generates a QR code link for a user to connect their WhatsApp account.
Endpoint: POST /v1/workspaces/{workspace_id}/users/{user_id}/qr
cURL
curl -X POST 'https://app.timelines.ai/partner/api/v1/workspaces/my-workspace/users/100/qr' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'X-TL-Partner-Id: your-partner-id' \
-H 'Content-Type: application/json' \
-d '{"expires_in_hours": 48}'
JavaScript
// Generate QR for each user
async function generateQRLinks(workspaceId, userIds) {
const qrLinks = [];
for (const userId of userIds) {
const qr = await apiRequest(
'POST',
`/v1/workspaces/${workspaceId}/users/${userId}/qr`,
{ expires_in_hours: 48 }
);
qrLinks.push({ userId, qrLink: qr.qr_link, expiresAt: qr.expires_at });
}
return qrLinks;
}
const links = await generateQRLinks('my-workspace', [100, 101, 102]);
Python
def generate_qr_links(workspace_id, user_ids):
qr_links = []
for user_id in user_ids:
qr = api_request(
'POST',
f'/v1/workspaces/{workspace_id}/users/{user_id}/qr',
{'expires_in_hours': 48}
)
qr_links.append({
'user_id': user_id,
'qr_link': qr['qr_link'],
'expires_at': qr['expires_at']
})
return qr_links
links = generate_qr_links('my-workspace', [100, 101, 102])
Response
{
"workspace_id": "my-workspace",
"user_id": 100,
"qr_link": "https://qr.timelines.ai/connect/abc123xyz...",
"expires_at": "2024-12-03T10:00:00Z",
"was_disconnected": false,
"previous_qr_revoked": false
}Warning: Generating a new QR disconnects any existing WhatsApp account for that user.
When opening the QR code connection flow, logout behavior depends on how the QR page is displayed:
Embedded mode (
display_mode=embedding, iframe)
Opening the QR code inside an iframe does not log the user out of the partner’s application.Standalone mode (separate page)
Opening the QR code as a standalone page will trigger a logout from the current session.
Embedding the QR code
By default, the qr_link opens a standalone QR code connection page.
If you want to embed the QR code connection block (for example, inside an iframe or an existing page layout) instead of using the standalone QR page, please add the following query parameter to the generated QR link: display_mode=embedding
When display_mode=embedding is provided, the QR page is optimized for embedded usage and does not assume full-page navigation.
QR Completion event
To verify that the QR onboarding page is communicating with the host application, the host must listen for message events on window. When the WhatsApp connection reaches a key state (successful pairing), the QR page sends a postMessage to its parent window (iframe) or opener window (new tab). The host can confirm receipt by inspecting the message event, validating the sender’s origin, and reacting to the payload (e.g. closing the modal or proceeding to the next step). A successful UI transition on the QR page does not guarantee message delivery; verification must be done on the receiving side.
4.6 Disconnect WhatsApp Account
Forcibly disconnects a WhatsApp account from a user.
Endpoint: POST /v1/workspaces/{workspace_id}/users/{user_id}/whatsapp/disconnect/
cURL
curl -X POST 'https://app.timelines.ai/partner/api/v1/workspaces/my-workspace/users/100/whatsapp/disconnect/' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'X-TL-Partner-Id: your-partner-id'
JavaScript
const result = await apiRequest(
'POST',
'/v1/workspaces/my-workspace/users/100/whatsapp/disconnect/'
);
console.log('Disconnected:', result.disconnected);
Python
result = api_request(
'POST',
'/v1/workspaces/my-workspace/users/100/whatsapp/disconnect/'
)
print(f"Disconnected: {result['disconnected']}")
Response
{
"workspace_id": "my-workspace",
"user_id": 100,
"whatsapp_account_id": 5001,
"phone_number_e164": "+14155551234",
"disconnected": true,
"disconnected_at": "2024-12-01T15:30:00Z"
}
4.7 Get Public API Token
Retrieves the Public API token for a workspace (creates one if it doesn't exist).
Endpoint: GET /v1/workspaces/{workspace_id}/api_token
cURL
curl -X GET 'https://app.timelines.ai/partner/api/v1/workspaces/my-workspace/api_token' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'X-TL-Partner-Id: your-partner-id'
JavaScript
const tokenData = await apiRequest('GET', '/v1/workspaces/my-workspace/api_token');
console.log('Public API Token:', tokenData.token);Python
token_data = api_request('GET', '/v1/workspaces/my-workspace/api_token')
print(f"Public API Token: {token_data['token']}")
4.8 Rotate Public API Token
Rotates (invalidates old, creates new) the Public API token for a workspace.
Endpoint: POST /v1/workspaces/{workspace_id}/api_token
cURL
curl -X POST 'https://app.timelines.ai/partner/api/v1/workspaces/my-workspace/api_token' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'X-TL-Partner-Id: your-partner-id'
JavaScript
const newToken = await apiRequest('POST', '/v1/workspaces/my-workspace/api_token');
console.log('New token:', newToken.token);
console.log('Rotated at:', newToken.rotated_at);Python
new_token = api_request('POST', '/v1/workspaces/my-workspace/api_token')
print(f"New token: {new_token['token']}")
print(f"Rotated at: {new_token['rotated_at']}")Warning: The old token is invalidated immediately. Update your integrations right away.
5. Complete Integration Example
Here's a complete JavaScript example that provisions a workspace with WhatsApp connections:
JavaScript - Full Setup Flow
const moment = require('moment');
const jwt = require('jsonwebtoken');
const PARTNER_ID = 'your-partner-id';
const PARTNER_SECRET = 'your-partner-secret';
const BASE_URL = 'https://app.timelines.ai/partner/api';
function generateToken() {
return jwt.sign({
partner_id: PARTNER_ID,
nbf: moment().subtract(3, 'm').unix(),
exp: moment().add(3, 'm').unix()
}, PARTNER_SECRET);
}
async function api(method, endpoint, body = null) {
const res = await fetch(`${BASE_URL}${endpoint}`, {
method,
headers: {
'Authorization': `Bearer ${generateToken()}`,
'X-TL-Partner-Id': PARTNER_ID,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
});
return res.json();
}
async function provisionCustomer(customerName, waAccountCount) {
// Step 1: Create workspace
const workspace = await api('POST', '/v1/workspaces/', {
display_name: customerName,
seats_purchased: waAccountCount
});
console.log(`Created workspace: ${workspace.workspace_id}`);
// Step 2: Create users
const users = await api('POST', `/v1/workspaces/${workspace.workspace_id}/users/`, {
count: waAccountCount
});
console.log(`Created ${users.users.length} users`);
// Step 3: Generate QR codes
const qrLinks = [];
for (const user of users.users) {
const qr = await api('POST',
`/v1/workspaces/${workspace.workspace_id}/users/${user.user_id}/qr`,
{ expires_in_hours: 72 }
);
qrLinks.push({ userId: user.user_id, link: qr.qr_link });
}
// Step 4: Get Public API token
const apiToken = await api('GET', `/v1/workspaces/${workspace.workspace_id}/api_token`);
return {
workspaceId: workspace.workspace_id,
qrLinks,
publicApiToken: apiToken.token
};
}
// Usage
provisionCustomer('acme-corp', 3).then(result => {
console.log('Provisioning complete:', result);
});
6. Webhook Events
Configure a webhook endpoint to receive real-time notifications. Webhooks are delivered via HTTP POST with JSON body.
Webhook Headers
X-TL-Partner-Id: Your partner identifier
X-TL-Signature: JWT token for verification
Verifying Webhook Signatures
JavaScript
const jwt = require('jsonwebtoken');
function verifyWebhook(req) {
const signature = req.headers['x-tl-signature'];
const partnerId = req.headers['x-tl-partner-id'];
try {
const decoded = jwt.verify(signature, PARTNER_SECRET);
return decoded.partner_id === partnerId;
} catch (err) {
return false;
}
}Python
import jwt
def verify_webhook(headers):
signature = headers.get('X-TL-Signature')
partner_id = headers.get('X-TL-Partner-Id')
try:
decoded = jwt.decode(signature, PARTNER_SECRET, algorithms=['HS256'])
return decoded['partner_id'] == partner_id
except jwt.InvalidTokenError:
return False
Event Types
Event Type | Description |
workspace:seats_full | All purchased seats are utilized |
workspace:quota_near_full:messaging | Messaging quota at 90%+ utilization |
workspace:quota_full:messaging | Messaging quota at 100% |
workspace:quota_full:api_calls | Public API calls quota at 100% |
whatsapp_account:connected | WhatsApp account connected |
whatsapp_account:disconnected | WhatsApp account disconnected |
api_token:rotated | Public API token was rotated |
7. Frequently Asked Questions
Authentication
Q: How do I generate the bearer token?
A: Generate a JWT signed with your Partner Secret containing partner_id, nbf, and exp claims. See Section 2 for code examples in JavaScript and Python.
Q: Can the Partner Secret be replaced?
A: Yes, coordinate with TimelinesAI support. The old secret stops working immediately.
Workspaces
Q: If I lose a workspace_id, can I retrieve it?
A: Currently requires a support ticket. A list-workspaces endpoint is planned for Phase 2.
Q: Price difference between extra seat vs new workspace?
A: No difference. Choose based on your organizational needs.
Billing & Trials
Q: When are we invoiced?
A: Monthly in arrears based on connected WhatsApp accounts, rather than provisioned seats.
Q: Can we offer free trials?
A: Yes, initial X-day periods can be configured. Important: Actively disconnect WA accounts if trial doesn't convert to prevent billing.
WhatsApp Accounts
Q: Can I connect a different number to an existing user?
A: Yes. Call /qr endpoint and scan with the new number.
Q: What is the correct endpoint for API tokens?
A: /v1/workspaces/{workspace_id}/api_token (no trailing slash)
Q: How to prevent WhatsApp bans?
A: Warm up new accounts (exchange messages with 3-5 contacts first), poll delivery statuses before automation, avoid mass messaging new contacts. See: help.timelines.ai/en/articles/12383621-whatsapp-mass-messaging-best-practices
8. Best Practices
Store workspace_id and user_id mappings — No list-workspaces endpoint yet
Verify webhook signatures — Validate JWT in X-TL-Signature
Use seats as a rail guard — Prevent over-provisioning
Warm up new WhatsApp accounts — Exchange messages with a couple of accounts you control, before connecting to TimelinesAI
Disconnect WA accounts when trials don't convert — Prevents billing
Monitor quota webhooks proactively — React before limits hit
Poll message delivery statuses — Before automation sequences
