Skip to main content

TimelinesAI Partner API Setup Guide & Integration Reference

Updated this week

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

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

  1. Store workspace_id and user_id mappings — No list-workspaces endpoint yet

  2. Verify webhook signatures — Validate JWT in X-TL-Signature

  3. Use seats as a rail guard — Prevent over-provisioning

  4. Warm up new WhatsApp accounts — Exchange messages with a couple of accounts you control, before connecting to TimelinesAI

  5. Disconnect WA accounts when trials don't convert — Prevents billing

  6. Monitor quota webhooks proactively — React before limits hit

  7. Poll message delivery statuses — Before automation sequences

Did this answer your question?