Rebalancing Genesys Cloud Outbound Campaign Number Pools with TypeScript
What You Will Build
- A TypeScript module that safely rebalances number pool allocations across Genesys Cloud Outbound campaigns by validating carrier capacity constraints, executing atomic PATCH requests, and verifying call success rates.
- The implementation uses the Genesys Cloud API v2 (
/api/v2/outbound/numberpoolsand/api/v2/analytics/conversations/details/query) and the@genesyscloud/api-clientSDK. - The tutorial covers TypeScript with Node.js 18+, demonstrating production-grade error handling, retry logic, pagination, and structured audit logging.
Prerequisites
- OAuth Client Credentials grant type configured in Genesys Cloud with the following scopes:
outbound:pool:write,outbound:campaign:read,analytics:conversation:read,outbound:analytics:read - SDK:
@genesyscloud/api-clientversion 5.0.0 or higher - Runtime: Node.js 18.0+
- Dependencies:
npm install @genesyscloud/api-client axios winston uuid - TypeScript compiler:
tscversion 5.0+
Authentication Setup
Genesys Cloud uses OAuth 2.0 for API authentication. The PlatformClient class handles token acquisition, caching, and automatic refresh. You must initialize it with your environment URL, client ID, and client secret.
import { PlatformClient } from '@genesyscloud/api-client';
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
export class GenesysAuth {
private client: PlatformClient;
private http: AxiosInstance;
constructor(envUrl: string, clientId: string, clientSecret: string) {
this.client = new PlatformClient();
this.client.setEnvironmentUrl(envUrl);
this.client.setClientId(clientId);
this.client.setClientSecret(clientSecret);
this.http = axios.create({
baseURL: `${envUrl}/api/v2`,
headers: { 'Content-Type': 'application/json' }
});
// Interceptor to attach fresh access tokens to every request
this.http.interceptors.request.use(async (config) => {
const token = await this.client.getAccessToken();
config.headers.Authorization = `Bearer ${token}`;
return config;
});
}
getHttpClient(): AxiosInstance {
return this.http;
}
}
The interceptor guarantees that every outbound request carries a valid bearer token. If the token expires during a long-running rebalance operation, the SDK refreshes it transparently.
Implementation
Step 1: Fetch Current Pool State and Validate Constraints
Before modifying allocations, you must retrieve the current pool configuration and validate against carrier capacity limits and maximum allocation shift thresholds. This prevents API rejections and carrier filtering.
import { NumberPoolEntity, AllocationEntity } from '@genesyscloud/api-client';
interface RebalanceConstraints {
maxAllocationShiftPercentage: number;
maxConcurrentCallsPerNumber: number;
carrierCapacityLimit: number;
}
export class PoolValidator {
constructor(
private http: AxiosInstance,
private constraints: RebalanceConstraints
) {}
async validatePool(poolId: string): Promise<{ isValid: boolean; reason?: string }> {
try {
const { data: pool } = await this.http.get<NumberPoolEntity>(`/outbound/numberpools/${poolId}`);
const currentAllocations = pool.allocations || [];
const totalCurrentAllocation = currentAllocations.reduce((sum, a) => sum + (a.allocationPercentage || 0), 0);
if (totalCurrentAllocation > this.constraints.carrierCapacityLimit) {
return { isValid: false, reason: `Current allocation ${totalCurrentAllocation}% exceeds carrier capacity ${this.constraints.carrierCapacityLimit}%` };
}
return { isValid: true };
} catch (error) {
const axiosError = error as any;
if (axiosError.response?.status === 404) {
return { isValid: false, reason: 'Number pool not found' };
}
throw error;
}
}
validateShiftLimit(currentPercentage: number, targetPercentage: number): boolean {
const shift = Math.abs(targetPercentage - currentPercentage);
return shift <= this.constraints.maxAllocationShiftPercentage;
}
}
The validator checks the existing allocations array and enforces business rules before any write operation occurs.
Step 2: Construct Rebalance Payload and Execute Atomic PATCH
Genesys Cloud accepts pool updates via PATCH /api/v2/outbound/numberpools/{numberPoolId}. The request body must include the pool UUID, updated allocation percentages, ANI distribution matrices, and geographic region directives.
HTTP Request Cycle
PATCH /api/v2/outbound/numberpools/{numberPoolId} HTTP/1.1
Host: mycompany.mygen.com
Authorization: Bearer <access_token>
Content-Type: application/json
{
"allocations": [
{
"campaignId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"allocationPercentage": 45,
"aniMatrix": {
"primaryAni": "+14155550100",
"fallbackAni": ["+14155550101", "+14155550102"],
"rotationStrategy": "roundRobin"
},
"geoDirectives": {
"regionCode": "US-CA",
"localPresenceEnabled": true,
"timezoneAlignment": "America/Los_Angeles"
}
}
],
"dialerSettings": {
"autoRebalanceEnabled": false,
"utilizationThreshold": 0.85
}
}
Expected Response
{
"id": "pool-uuid-123",
"name": "WestCoast_Outbound_Pool",
"allocations": [
{
"campaignId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"allocationPercentage": 45,
"aniMatrix": {
"primaryAni": "+14155550100",
"fallbackAni": ["+14155550101", "+14155550102"],
"rotationStrategy": "roundRobin"
},
"geoDirectives": {
"regionCode": "US-CA",
"localPresenceEnabled": true,
"timezoneAlignment": "America/Los_Angeles"
}
}
],
"dialerSettings": {
"autoRebalanceEnabled": false,
"utilizationThreshold": 0.85
},
"selfUri": "/api/v2/outbound/numberpools/pool-uuid-123"
}
The TypeScript implementation includes exponential backoff for 429 responses and verifies the response schema before proceeding.
import { v4 as uuidv4 } from 'uuid';
interface AnIMatrix {
primaryAni: string;
fallbackAni: string[];
rotationStrategy: 'roundRobin' | 'leastUsed' | 'random';
}
interface GeoDirective {
regionCode: string;
localPresenceEnabled: boolean;
timezoneAlignment: string;
}
interface PoolAllocationPayload {
campaignId: string;
allocationPercentage: number;
aniMatrix: AnIMatrix;
geoDirectives: GeoDirective;
}
export class PoolRebalancer {
private requestId: string;
private startTime: number;
constructor(private http: AxiosInstance) {
this.requestId = uuidv4();
this.startTime = Date.now();
}
async executeAtomicPatch(poolId: string, allocations: PoolAllocationPayload[]): Promise<boolean> {
const payload = {
allocations,
dialerSettings: {
autoRebalanceEnabled: false,
utilizationThreshold: 0.85
}
};
const config: AxiosRequestConfig = {
headers: { 'X-Request-Id': this.requestId }
};
// Retry logic for 429 Too Many Requests
let attempts = 0;
const maxAttempts = 3;
const baseDelay = 1000;
while (attempts < maxAttempts) {
try {
const response = await this.http.patch(`/outbound/numberpools/${poolId}`, payload, config);
if (response.status >= 200 && response.status < 300) {
console.log(`[SUCCESS] Pool ${poolId} updated. Status: ${response.status}`);
return true;
}
throw new Error(`Unexpected status: ${response.status}`);
} catch (error: any) {
const status = error.response?.status;
if (status === 429 && attempts < maxAttempts - 1) {
const delay = baseDelay * Math.pow(2, attempts) + Math.random() * 500;
console.warn(`[RATE_LIMIT] 429 received. Retrying in ${Math.round(delay)}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
attempts++;
} else {
throw error;
}
}
}
return false;
}
getLatencyMs(): number {
return Date.now() - this.startTime;
}
}
Step 3: Verify Success Rates and Spam Trap Signals
After the PATCH operation commits, you must validate delivery health. Genesys Cloud exposes conversation analytics via /api/v2/analytics/conversations/details/query. You must implement pagination and filter for disposition codes that indicate spam traps or carrier filtering.
interface AnalyticsQueryResult {
pageSize: number;
pageNumber: number;
pageCount: number;
totalCount: number;
entities: Array<{
id: string;
wrapupCode: string;
dispositionCode: string;
direction: string;
metrics: {
call: {
answered: boolean;
totalDuration: number;
};
};
}>;
}
export class DeliveryVerifier {
constructor(private http: AxiosInstance) {}
async checkCallSuccessRate(campaignId: string, startTime: string, endTime: string): Promise<number> {
let successCount = 0;
let totalCount = 0;
let page = 1;
let pageCount = 1;
const queryBody = {
interval: `${startTime}/${endTime}`,
pageSize: 100,
pageNumber: page,
query: `conversationType:outbound AND campaignId:${campaignId}`,
groupBy: [],
aggregations: []
};
while (page <= pageCount) {
queryBody.pageNumber = page;
const { data } = await this.http.post<AnalyticsQueryResult>(
'/analytics/conversations/details/query',
queryBody
);
pageCount = data.pageCount;
totalCount += data.entities.length;
successCount += data.entities.filter(e =>
e.metrics?.call?.answered === true &&
!['spam_trap', 'filtered', 'blocked'].includes(e.dispositionCode?.toLowerCase() || '')
).length;
page++;
}
return totalCount > 0 ? (successCount / totalCount) : 0;
}
async verifySpamTrapSignals(campaignId: string, startTime: string, endTime: string): Promise<boolean> {
const { data } = await this.http.post<AnalyticsQueryResult>(
'/analytics/conversations/details/query',
{
interval: `${startTime}/${endTime}`,
pageSize: 50,
pageNumber: 1,
query: `conversationType:outbound AND campaignId:${campaignId} AND dispositionCode:spam_trap`,
groupBy: [],
aggregations: []
}
);
return data.totalCount === 0;
}
}
The verifier paginates through conversation records, calculates the answered rate, and explicitly checks for spam trap dispositions. If the success rate falls below a threshold or spam traps are detected, the rebalance iteration should halt.
Step 4: Emit Callbacks, Track Latency, and Generate Audit Logs
Production integrations require synchronization with external carrier reporting tools and immutable audit trails. The following module handles webhook dispatch, latency tracking, and structured logging.
import winston from 'winston';
interface AuditLogEntry {
timestamp: string;
requestId: string;
poolId: string;
action: string;
status: 'success' | 'failure' | 'validation_error';
latencyMs: number;
details: Record<string, any>;
}
export class RebalanceOrchestrator {
private logger: winston.Logger;
private commitSuccessCount: number = 0;
private totalAttempts: number = 0;
constructor(
private http: AxiosInstance,
private webhookUrl: string
) {
this.logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [new winston.transports.Console()]
});
}
async emitCarrierCallback(poolId: string, status: string, metrics: Record<string, any>): Promise<void> {
try {
await this.http.post(this.webhookUrl, {
event: 'pool.rebalance.completed',
poolId,
status,
metrics,
timestamp: new Date().toISOString()
});
} catch (err) {
this.logger.error({ message: 'Webhook delivery failed', error: err });
}
}
logAudit(entry: AuditLogEntry): void {
this.logger.info({ type: 'audit', ...entry });
}
getCommitSuccessRate(): number {
return this.totalAttempts > 0 ? (this.commitSuccessCount / this.totalAttempts) : 0;
}
recordCommit(success: boolean): void {
this.totalAttempts++;
if (success) this.commitSuccessCount++;
}
}
The orchestrator tracks allocation commit success rates, dispatches JSON payloads to external carrier endpoints, and writes structured audit logs for governance compliance.
Complete Working Example
The following TypeScript module combines all components into a single, runnable rebalancer class. Replace the placeholder credentials and pool identifiers before execution.
import { PlatformClient } from '@genesyscloud/api-client';
import axios from 'axios';
import { GenesysAuth } from './auth';
import { PoolValidator } from './validator';
import { PoolRebalancer, PoolAllocationPayload } from './rebalancer';
import { DeliveryVerifier } from './verifier';
import { RebalanceOrchestrator } from './orchestrator';
export class OutboundPoolRebalancer {
private auth: GenesysAuth;
private http: any;
private validator: PoolValidator;
private rebalancer: PoolRebalancer;
private verifier: DeliveryVerifier;
private orchestrator: RebalanceOrchestrator;
constructor(
envUrl: string,
clientId: string,
clientSecret: string,
webhookUrl: string
) {
this.auth = new GenesysAuth(envUrl, clientId, clientSecret);
this.http = this.auth.getHttpClient();
this.validator = new PoolValidator(this.http, {
maxAllocationShiftPercentage: 15,
maxConcurrentCallsPerNumber: 50,
carrierCapacityLimit: 100
});
this.rebalancer = new PoolRebalancer(this.http);
this.verifier = new DeliveryVerifier(this.http);
this.orchestrator = new RebalanceOrchestrator(this.http, webhookUrl);
}
async runRebalance(poolId: string, allocations: PoolAllocationPayload[]): Promise<void> {
console.log(`[INIT] Starting rebalance for pool ${poolId}`);
// 1. Validate constraints
const validation = await this.validator.validatePool(poolId);
if (!validation.isValid) {
this.orchestrator.logAudit({
timestamp: new Date().toISOString(),
requestId: this.rebalancer['requestId'],
poolId,
action: 'validate',
status: 'validation_error',
latencyMs: 0,
details: { reason: validation.reason }
});
throw new Error(`Validation failed: ${validation.reason}`);
}
// 2. Validate shift limits
for (const alloc of allocations) {
if (!this.validator.validateShiftLimit(0, alloc.allocationPercentage)) {
throw new Error(`Allocation shift ${alloc.allocationPercentage}% exceeds maximum allowed shift`);
}
}
// 3. Execute atomic PATCH
const patchSuccess = await this.rebalancer.executeAtomicPatch(poolId, allocations);
this.orchestrator.recordCommit(patchSuccess);
if (!patchSuccess) {
throw new Error('Atomic PATCH operation failed after retries');
}
// 4. Wait for utilization check trigger (simulate polling)
await new Promise(resolve => setTimeout(resolve, 2000));
// 5. Verify delivery health
const now = new Date();
const startTime = new Date(now.getTime() - 3600000).toISOString();
const endTime = now.toISOString();
const successRate = await this.verifier.checkCallSuccessRate(allocations[0].campaignId, startTime, endTime);
const isSpamFree = await this.verifier.verifySpamTrapSignals(allocations[0].campaignId, startTime, endTime);
const healthStatus = (successRate >= 0.70 && isSpamFree) ? 'healthy' : 'degraded';
// 6. Emit callback and audit log
await this.orchestrator.emitCarrierCallback(poolId, healthStatus, {
successRate,
isSpamFree,
commitSuccessRate: this.orchestrator.getCommitSuccessRate(),
latencyMs: this.rebalancer.getLatencyMs()
});
this.orchestrator.logAudit({
timestamp: new Date().toISOString(),
requestId: this.rebalancer['requestId'],
poolId,
action: 'rebalance.complete',
status: patchSuccess ? 'success' : 'failure',
latencyMs: this.rebalancer.getLatencyMs(),
details: { allocationsCount: allocations.length, healthStatus }
});
console.log(`[COMPLETE] Rebalance finished. Latency: ${this.rebalancer.getLatencyMs()}ms`);
}
}
// Execution entry point
(async () => {
const rebalancer = new OutboundPoolRebalancer(
'https://mycompany.mygen.com',
'your_client_id',
'your_client_secret',
'https://your-carrier-reporting.internal/api/v1/pool-callbacks'
);
try {
await rebalancer.runRebalance('pool-uuid-123', [
{
campaignId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
allocationPercentage: 45,
aniMatrix: { primaryAni: '+14155550100', fallbackAni: ['+14155550101'], rotationStrategy: 'roundRobin' },
geoDirectives: { regionCode: 'US-CA', localPresenceEnabled: true, timezoneAlignment: 'America/Los_Angeles' }
}
]);
} catch (error) {
console.error('Rebalance failed:', error);
process.exit(1);
}
})();
Common Errors & Debugging
Error: 409 Conflict
- What causes it: The allocation percentage array sums to more than 100 percent, or the pool is locked due to an active campaign pause.
- How to fix it: Verify that the
allocationPercentagevalues across all campaigns in the payload equal exactly 100. Ensure the campaign status isactivebefore submitting the PATCH request. - Code showing the fix: Add a pre-flight check
if (allocations.reduce((s, a) => s + a.allocationPercentage, 0) !== 100) throw new Error('Allocation sum must equal 100');
Error: 429 Too Many Requests
- What causes it: Genesys Cloud enforces rate limits per OAuth client and per API endpoint. Rapid rebalance iterations trigger throttling.
- How to fix it: The provided
executeAtomicPatchmethod implements exponential backoff with jitter. Ensure your calling code does not spawn parallel rebalance requests for the same pool. - Code showing the fix: The retry loop in Step 2 automatically handles 429 responses by calculating
delay = baseDelay * Math.pow(2, attempts) + Math.random() * 500and resubmitting.
Error: 400 Bad Request
- What causes it: Malformed JSON, missing required fields in
aniMatrixorgeoDirectives, or invalid UUID format. - How to fix it: Validate the payload structure against the TypeScript interfaces before serialization. Ensure all UUIDs follow the RFC 4122 standard.
- Code showing the fix: Use
zodor runtime type guards to validatePoolAllocationPayloadbefore passing it toexecuteAtomicPatch.
Error: 401 Unauthorized / 403 Forbidden
- What causes it: Expired access token or missing OAuth scopes.
- How to fix it: Verify the client credentials grant has
outbound:pool:writeandanalytics:conversation:readscopes. ThePlatformClientinterceptor automatically refreshes tokens, but initial authorization failures require scope correction in the Genesys Cloud admin console. - Code showing the fix: Check the
error.response.headers['www-authenticate']header for scope violation details and update the OAuth client configuration accordingly.