// Sub-Account API operations for enterprise/agency functionality
// Updated for OpenAPI specification compliance
import { BaseApiClient } from './base-client.js';
export class SubAccountApi extends BaseApiClient {
/**
* Validate account ID based on endpoint requirements per OpenAPI spec
*/
validateAccountId(accountId, endpoint) {
// Integer endpoints: suspend, unsuspend, and PATCH operations
if (endpoint.includes('suspend') || endpoint.includes('patch') || endpoint.includes('convert-to-organization')) {
if (!/^\d+$/.test(accountId)) {
throw new Error(`Account ID must be a valid integer for ${endpoint} endpoint`);
}
const numericId = parseInt(accountId, 10);
if (endpoint.includes('suspend') && numericId < 1) {
throw new Error('Account ID must be >= 1 for suspend/unsuspend operations');
}
}
else {
// String endpoints: GET, DELETE, confirm
if (!/^[a-zA-Z0-9]+$/.test(accountId)) {
throw new Error('Account ID must contain only alphanumeric characters');
}
if (accountId.length < 1 || accountId.length > 20) {
throw new Error('Account ID must be between 1 and 20 characters');
}
}
}
/**
* Build sort parameter according to OpenAPI spec: [-|+]term
*/
buildSortParameter(sort) {
if (!sort?.sort)
return undefined;
const validTerms = ['name', 'created_on'];
if (!validTerms.includes(sort.sort)) {
throw new Error(`Invalid sort term. Valid terms: ${validTerms.join(', ')}`);
}
const prefix = sort.order === 'desc' ? '-' : '+';
return `${prefix}${sort.sort}`;
}
/**
* Build filter parameter according to OpenAPI spec: term==value;term2==value2
*/
buildFilterParameter(filters) {
if (!filters)
return undefined;
const validTerms = ['name', 'status'];
const filterParts = [];
Object.entries(filters).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
if (!validTerms.includes(key)) {
throw new Error(`Invalid filter term '${key}'. Valid terms: ${validTerms.join(', ')}`);
}
filterParts.push(`${key}==${value}`);
}
});
return filterParts.length > 0 ? filterParts.join(';') : undefined;
}
/**
* List all sub-accounts with filtering and pagination
* Compliant with OpenAPI spec: GET /accounts
*/
async listSubAccounts(params) {
const queryParams = new URLSearchParams();
// Partner account ID (integer)
if (params?.partner_account_id !== undefined) {
if (!Number.isInteger(params.partner_account_id)) {
throw new Error('partner_account_id must be an integer');
}
queryParams.append('partner_account_id', params.partner_account_id.toString());
}
// Recursive (boolean, default: false)
if (params?.recursive !== undefined) {
queryParams.append('recursive', params.recursive.toString());
}
// Pagination parameters
if (params?.pagination?.page !== undefined) {
if (!Number.isInteger(params.pagination.page) || params.pagination.page < 1) {
throw new Error('page must be an integer >= 1');
}
queryParams.append('page', params.pagination.page.toString());
}
if (params?.pagination?.per_page !== undefined) {
if (!Number.isInteger(params.pagination.per_page) || params.pagination.per_page < 1) {
throw new Error('per_page must be an integer >= 1');
}
queryParams.append('per_page', params.pagination.per_page.toString());
}
if (params?.pagination?.with_count !== undefined) {
queryParams.append('with_count', params.pagination.with_count.toString());
}
// Sort parameter (format: [-|+]term)
const sortParam = this.buildSortParameter(params?.sort);
if (sortParam) {
queryParams.append('sort', sortParam);
}
// Filter parameter (format: term==value;term2==value2)
const filterParam = this.buildFilterParameter(params?.filters);
if (filterParam) {
queryParams.append('filter', filterParam);
}
const url = `/accounts${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
return this.makeRequest(url);
}
/**
* Create a new sub-account
* Compliant with OpenAPI spec: POST /accounts
*/
async createSubAccount(data, options) {
// Validate required fields
if (!data.name || typeof data.name !== 'string') {
throw new Error('name is required and must be a string');
}
if (!data.email || typeof data.email !== 'string') {
throw new Error('email is required and must be a string');
}
if (!data.password || typeof data.password !== 'string') {
throw new Error('password is required and must be a string');
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
throw new Error('Invalid email format');
}
// Validate password length (minimum 8 characters per OpenAPI spec)
if (data.password.length < 8) {
throw new Error('Password must be at least 8 characters long');
}
const queryParams = new URLSearchParams();
if (options?.partner_account_id !== undefined) {
if (!Number.isInteger(options.partner_account_id)) {
throw new Error('partner_account_id must be an integer');
}
queryParams.append('partner_account_id', options.partner_account_id.toString());
}
if (options?.skip_verification !== undefined) {
queryParams.append('skip_verification', options.skip_verification.toString());
}
const url = `/accounts${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
return this.makeRequest(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
/**
* Get details of a specific sub-account
* Compliant with OpenAPI spec: GET /accounts/{account_id}
* account_id: string with pattern ^[a-zA-Z0-9]+$ (1-20 chars)
*/
async getSubAccount(accountId) {
this.validateAccountId(accountId, 'get');
return this.makeRequest(`/accounts/${accountId}`);
}
/**
* Update a sub-account
* Compliant with OpenAPI spec: PATCH /accounts/{account_id}
* account_id: integer
*/
async updateSubAccount(accountId, data) {
this.validateAccountId(accountId, 'patch');
// Validate email if provided
if (data.email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
throw new Error('Invalid email format');
}
}
// Convert string to number for the API call as OpenAPI expects integer
const numericAccountId = parseInt(accountId, 10);
return this.makeRequest(`/accounts/${numericAccountId}`, {
method: 'PATCH',
body: JSON.stringify(data)
});
}
/**
* Delete a sub-account
* Compliant with OpenAPI spec: DELETE /accounts/{account_id}
* account_id: string with pattern ^[a-zA-Z0-9]+$ (1-20 chars)
*/
async deleteSubAccount(accountId) {
this.validateAccountId(accountId, 'delete');
return this.makeRequest(`/accounts/${accountId}`, {
method: 'DELETE'
});
}
/**
* Suspend a sub-account
* Compliant with OpenAPI spec: POST /accounts/{account_id}/suspend
* account_id: integer with minimum 1.0
*/
async suspendSubAccount(accountId) {
this.validateAccountId(accountId, 'suspend');
const numericAccountId = parseInt(accountId, 10);
return this.makeRequest(`/accounts/${numericAccountId}/suspend`, {
method: 'POST'
});
}
/**
* Unsuspend a sub-account
* Compliant with OpenAPI spec: POST /accounts/{account_id}/unsuspend
* account_id: integer with minimum 1.0
*/
async unsuspendSubAccount(accountId) {
this.validateAccountId(accountId, 'unsuspend');
const numericAccountId = parseInt(accountId, 10);
return this.makeRequest(`/accounts/${numericAccountId}/unsuspend`, {
method: 'POST'
});
}
/**
* Confirm sub-account creation
* Compliant with OpenAPI spec: POST /accounts/{account_id}/confirm
* account_id: string with pattern ^[a-zA-Z0-9]+$ (1-20 chars)
*/
async confirmSubAccount(accountId, data) {
this.validateAccountId(accountId, 'confirm');
if (!data.confirmation_code || typeof data.confirmation_code !== 'string') {
throw new Error('confirmation_code is required and must be a string');
}
// Validate password if provided (minimum 8 characters)
if (data.password && data.password.length < 8) {
throw new Error('Password must be at least 8 characters long');
}
return this.makeRequest(`/accounts/${accountId}/confirm`, {
method: 'POST',
body: JSON.stringify(data)
});
}
/**
* Resend account verification email
* Compliant with OpenAPI spec: POST /accounts/resend-verification-email
*/
async resendVerificationEmail(data) {
if (!data.email || typeof data.email !== 'string') {
throw new Error('email is required and must be a string');
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.email)) {
throw new Error('Invalid email format');
}
return this.makeRequest('/accounts/resend-verification-email', {
method: 'POST',
body: JSON.stringify(data)
});
}
/**
* Convert a sub-account to an organization
* Compliant with OpenAPI spec: POST /accounts/{account_id}/convert-to-organization
* account_id: integer
*/
async convertSubAccountToOrganization(accountId, data) {
this.validateAccountId(accountId, 'convert-to-organization');
// Default migrate_owner to true per OpenAPI spec
const payload = data || { migrate_owner: true };
const numericAccountId = parseInt(accountId, 10);
return this.makeRequest(`/accounts/${numericAccountId}/convert-to-organization`, {
method: 'POST',
body: JSON.stringify(payload)
});
}
// ===== CONVENIENCE METHODS (not in OpenAPI spec but useful) =====
/**
* Get sub-accounts with default parameters for easier usage
*/
async getSubAccountsWithDefaults(params) {
const listParams = {
recursive: params?.recursive || false,
pagination: {
page: params?.page || 1,
per_page: params?.per_page || 50,
with_count: true
},
sort: {
sort: 'created_on',
order: 'desc'
}
};
if (params?.status) {
listParams.filters = { status: params.status };
}
return this.listSubAccounts(listParams);
}
/**
* Get the latest created sub-account
*/
async getLatestSubAccount() {
const response = await this.listSubAccounts({
pagination: {
page: 1,
per_page: 1,
with_count: false
},
sort: {
sort: 'created_on',
order: 'desc'
}
});
if (response.data && response.data.length > 0) {
return {
data: response.data[0]
};
}
return null;
}
/**
* Search sub-accounts by name
*/
async searchSubAccountsByName(name, params) {
if (!name || typeof name !== 'string') {
throw new Error('name parameter is required and must be a string');
}
return this.listSubAccounts({
filters: { name },
pagination: {
page: params?.page || 1,
per_page: params?.per_page || 50,
with_count: true
},
sort: {
sort: 'name',
order: 'asc'
}
});
}
/**
* Get sub-accounts by status
*/
async getSubAccountsByStatus(status, params) {
const validStatuses = ['pending', 'active', 'suspended', 'inactive'];
if (!validStatuses.includes(status)) {
throw new Error(`Invalid status. Valid statuses: ${validStatuses.join(', ')}`);
}
return this.listSubAccounts({
filters: { status },
pagination: {
page: params?.page || 1,
per_page: params?.per_page || 50,
with_count: true
},
sort: {
sort: 'created_on',
order: 'desc'
}
});
}
/**
* Debug sub-account access - helpful for troubleshooting
*/
async debugSubAccountAccess(accountId) {
try {
if (accountId) {
this.validateAccountId(accountId, 'get');
const account = await this.getSubAccount(accountId);
return {
access_check: 'success',
account_found: true,
account_data: account.data,
timestamp: new Date().toISOString(),
validation: 'account_id format validated',
openapi_compliance: 'verified'
};
}
else {
const accounts = await this.listSubAccounts({
pagination: { page: 1, per_page: 5, with_count: true }
});
return {
access_check: 'success',
can_list_accounts: true,
account_count: accounts.pagination?.count || 0,
first_few_accounts: accounts.data?.slice(0, 3).map(acc => ({
id: acc.id,
name: acc.name,
status: acc.status
})),
timestamp: new Date().toISOString(),
filter_validation: 'OpenAPI compliant filters supported',
openapi_compliance: 'verified'
};
}
}
catch (error) {
return {
access_check: 'failed',
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
validation_error: true,
openapi_compliance: 'validation_failed'
};
}
}
/**
* Verify sub-account email with code or resend verification
* This is a wrapper for the confirm endpoint with different behavior
*/
async verifySubAccountEmail(accountId, data) {
this.validateAccountId(accountId, 'verify');
const url = `/accounts/${accountId}/verify`;
return this.makeRequest(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
/**
* Resend sub-account verification email
* Alternative method name for resendVerificationEmail
*/
async resendSubAccountVerification(email) {
return this.resendVerificationEmail({ email });
}
/**
* Convert sub-account to organization (overloaded version)
*/
async convertToOrganization(accountId, migrateOwner = true) {
const data = { migrate_owner: migrateOwner };
return this.convertSubAccountToOrganization(accountId, data);
}
/**
* Find sub-account by email
*/
async findSubAccountByEmail(email) {
const response = await this.listSubAccounts();
const account = response.data?.find(acc => acc.account_owner?.email === email ||
acc.email === email);
return account || null;
}
/**
* Get active sub-accounts
*/
async getActiveSubAccounts() {
const response = await this.getSubAccountsByStatus('active');
return response.data || [];
}
/**
* Get sub-account statistics
*/
async getSubAccountStatistics() {
const response = await this.listSubAccounts({
pagination: { page: 1, per_page: 100, with_count: true }
});
const stats = {
total: response.pagination?.total_count || response.pagination?.count || response.data?.length || 0,
active: 0,
suspended: 0,
pending: 0,
inactive: 0
};
response.data?.forEach(account => {
if (account.status === 'active')
stats.active++;
else if (account.status === 'suspended')
stats.suspended++;
else if (account.status === 'pending')
stats.pending++;
else if (account.status === 'inactive')
stats.inactive++;
});
return stats;
}
/**
* Get all sub-accounts (pagination helper)
*/
async getAllSubAccounts() {
const allAccounts = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await this.listSubAccounts({
pagination: { page, per_page: 100, with_count: true }
});
if (response.data && response.data.length > 0) {
allAccounts.push(...response.data);
}
hasMore = response.data?.length === 100;
page++;
}
return allAccounts;
}
/**
* Export sub-accounts to CSV or JSON format
*/
async exportSubAccounts(params) {
const exportParams = {};
if (params?.status_filter !== undefined) {
exportParams.status_filter = params.status_filter;
}
if (params?.include_usage_stats !== undefined) {
exportParams.include_usage_stats = params.include_usage_stats;
}
if (params?.include_owner_details !== undefined) {
exportParams.include_owner_details = params.include_owner_details;
}
const exportData = await this.exportSubAccountsData(exportParams);
const format = params?.format || 'csv';
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = params?.filename || `sub-accounts-export-${timestamp}.${format}`;
let data;
if (format === 'json') {
data = JSON.stringify(exportData.accounts, null, 2);
}
else {
// CSV format
const headers = ['id', 'name', 'email', 'status', 'created_on'];
const rows = exportData.accounts.map(account => [
account.id,
account.name,
account.owner_email || '',
account.status,
account.created_on
]);
data = [
headers.join(','),
...rows.map(row => row.join(','))
].join('\n');
}
return {
format,
filename,
data,
totalAccounts: exportData.accounts.length
};
}
/**
* Export sub-accounts data with comprehensive information
* This is a convenience method that aggregates all sub-account data
* for export purposes (CSV, JSON, etc.)
*/
async exportSubAccountsData(params) {
// Set up parameters for comprehensive data collection
const listParams = {
recursive: params?.recursive || false,
pagination: {
page: 1,
per_page: 100,
with_count: true
},
sort: {
sort: 'created_on',
order: 'asc'
}
};
// Apply filters
if (params?.status_filter || params?.partner_account_id) {
listParams.filters = {};
if (params?.status_filter) {
listParams.filters.status = params.status_filter;
}
if (params?.partner_account_id !== undefined) {
listParams.partner_account_id = params.partner_account_id;
}
}
// Collect all sub-accounts across multiple pages
let allAccounts = [];
let currentPage = 1;
let totalPages = 1;
let totalCount = 0;
do {
listParams.pagination.page = currentPage;
const response = await this.listSubAccounts(listParams);
if (response.data && response.data.length > 0) {
allAccounts = allAccounts.concat(response.data);
}
if (response.pagination) {
totalCount = response.pagination.count || 0;
const perPage = response.pagination.per_page || 100;
totalPages = Math.ceil(totalCount / perPage);
}
currentPage++;
} while (currentPage <= totalPages && totalPages > 1);
// Process accounts for export
const processedAccounts = allAccounts.map(account => {
const exportAccount = {
id: account.id,
name: account.name || '',
status: account.status || '',
lineage: account.lineage || '',
is_partner: account.partner || false,
created_on: account.created_on || '',
expires_on: account.expires_on || ''
};
// Include owner details if requested
if (params?.include_owner_details !== false && account.account_owner) {
exportAccount.owner_name = account.account_owner.name || '';
exportAccount.owner_email = account.account_owner.email || '';
}
// Include usage statistics if requested
if (params?.include_usage_stats !== false && account.usage_limits) {
exportAccount.emails_per_month = account.usage_limits.per_month || 0;
exportAccount.emails_per_campaign = account.usage_limits.per_campaign || 0;
exportAccount.emails_remaining = account.usage_limits.remaining || 0;
exportAccount.maximum_contacts = account.usage_limits.maximum_contacts || 0;
exportAccount.lists_limit = account.usage_limits.lists || 0;
exportAccount.users_limit = account.usage_limits.users || 0;
exportAccount.use_automations = account.usage_limits.use_automations || false;
exportAccount.use_ab_testing = account.usage_limits.use_ab_split || false;
exportAccount.use_contact_export = account.usage_limits.use_contact_export || false;
exportAccount.use_email_api = account.usage_limits.use_email_api || false;
exportAccount.use_html_editor = account.usage_limits.use_html_editor || false;
exportAccount.use_tags = account.usage_limits.use_tags || false;
}
return exportAccount;
});
return {
accounts: processedAccounts,
export_info: {
generated_at: new Date().toISOString(),
total_accounts: processedAccounts.length,
filters: {
status_filter: params?.status_filter || null,
partner_account_id: params?.partner_account_id || null,
recursive: params?.recursive || false
},
export_options: {
include_usage_stats: params?.include_usage_stats !== false,
include_owner_details: params?.include_owner_details !== false
}
}
};
}
}
//# sourceMappingURL=sub-account-api.js.map