Resetting Genesys Cloud EventBridge Consumer Offsets with TypeScript
What You Will Build
- A TypeScript service that constructs, validates, and executes atomic consumer offset resets for Genesys Cloud EventBridge streaming targets.
- The module uses the Genesys Cloud REST API for subscription verification and the AWS SDK for Kinesis and EventBridge to reposition consumers, validate schemas, and emit audit events.
- The tutorial covers TypeScript, Node.js, AWS SDK v3, and strict offset validation pipelines.
Prerequisites
- Genesys Cloud OAuth confidential client with scopes:
eventbridge:read,eventbridge:write - AWS credentials with permissions:
kinesis:GetShardIterator,kinesis:SubscribeToShard,events:PutEvents - Node.js 18+ and TypeScript 5+
- Dependencies:
npm install axios @aws-sdk/client-kinesis @aws-sdk/client-eventbridge uuid zod - Active EventBridge subscription in Genesys Cloud routing to a Kinesis data stream
Authentication Setup
Genesys Cloud requires OAuth 2.0 client credentials flow. The code below exchanges client credentials for an access token and caches it for subsequent requests. AWS SDK v3 automatically resolves credentials from the environment or shared credentials file.
import axios from 'axios';
import { KinesisClient, GetShardIteratorCommand, SubscribeToShardCommand } from '@aws-sdk/client-kinesis';
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';
import { v4 as uuidv4 } from 'uuid';
import { z } from 'zod';
interface GenesysTokenResponse {
access_token: string;
token_type: string;
expires_in: number;
}
class GenesysAuthService {
private tokenCache: { token: string; expiry: number } | null = null;
constructor(
private readonly environment: string,
private readonly clientId: string,
private readonly clientSecret: string
) {}
async getAccessToken(): Promise<string> {
if (this.tokenCache && Date.now() < this.tokenCache.expiry) {
return this.tokenCache.token;
}
const url = `https://${this.environment}.mygenesys.com/oauth/token`;
const auth = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');
const response = await axios.post<GenesysTokenResponse>(
url,
new URLSearchParams({ grant_type: 'client_credentials' }),
{
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
this.tokenCache = {
token: response.data.access_token,
expiry: Date.now() + (response.data.expires_in * 1000),
};
return this.tokenCache.token;
}
}
Implementation
Step 1: Fetch Subscription Details and Validate Stream Target
The reset operation requires the exact Kinesis stream name and shard/partition mapping. The code retrieves the active EventBridge subscription from Genesys Cloud and extracts the target stream configuration.
interface SubscriptionTarget {
streamName: string;
region: string;
partitionKey: string;
}
class GenesysEventBridgeManager {
private readonly client: any;
constructor(
private readonly auth: GenesysAuthService
) {
this.client = axios.create({
baseURL: `https://${auth.environment}.mygenesys.com/api/v2`,
timeout: 10000,
});
}
async getSubscriptionTarget(subscriptionId: string): Promise<SubscriptionTarget> {
const token = await this.auth.getAccessToken();
const response = await this.client.get(`/eventbridge/subscriptions/${subscriptionId}`, {
headers: {
Authorization: `Bearer ${token}`,
'Accept': 'application/json',
},
});
const target = response.data.target;
if (!target || !target.streamName) {
throw new Error('Invalid subscription target configuration');
}
return {
streamName: target.streamName,
region: target.region || 'us-east-1',
partitionKey: target.partitionKey || 'genesys-event',
};
}
}
Step 2: Construct Reset Payloads and Validate Against Engine Constraints
The offset reset directive requires partition identifiers, an offset matrix, and a timestamp directive. The validation pipeline enforces maximum rewind windows, epoch validity, and monotonically increasing sequence checks.
const ResetDirectiveSchema = z.object({
shardId: z.string().regex(/^shardId-[0-9a-fA-F-]{36}$/),
offsetMatrix: z.record(z.string(), z.union([z.string(), z.number()])),
timestampDirective: z.coerce.date(),
maxRewindHours: z.number().min(1).max(168),
});
type ResetDirective = z.infer<typeof ResetDirectiveSchema>;
class OffsetValidationPipeline {
static validate(directive: unknown): ResetDirective {
const parsed = ResetDirectiveSchema.parse(directive);
const now = new Date();
const rewindLimit = new Date(now.getTime() - (parsed.maxRewindHours * 3600000));
if (parsed.timestampDirective < rewindLimit) {
throw new Error(`Timestamp directive exceeds maximum rewind window of ${parsed.maxRewindHours} hours`);
}
if (parsed.timestampDirective > now) {
throw new Error('Timestamp directive cannot be in the future');
}
this.validateMonotonicSequence(parsed.offsetMatrix);
this.validateEpochValidity(parsed.timestampDirective);
return parsed;
}
private static validateMonotonicSequence(matrix: Record<string, string | number>): void {
const entries = Object.entries(matrix).sort((a, b) => a[0].localeCompare(b[0]));
for (let i = 1; i < entries.length; i++) {
const current = typeof entries[i][1] === 'string' ? BigInt(entries[i][1]) : BigInt(entries[i][1]);
const previous = typeof entries[i-1][1] === 'string' ? BigInt(entries[i-1][1]) : BigInt(entries[i-1][1]);
if (current <= previous) {
throw new Error('Offset matrix violates monotonically increasing sequence requirement');
}
}
}
private static validateEpochValidity(timestamp: Date): void {
const epochSeconds = Math.floor(timestamp.getTime() / 1000);
if (epochSeconds < 1609459200 || epochSeconds > 1893456000) {
throw new Error('Timestamp directive falls outside valid epoch range for Genesys Cloud event retention');
}
}
}
Step 3: Execute Atomic POST Operations and Handle Consumer Repositioning
The reset operation uses AWS Kinesis GetShardIterator with a timestamp directive, followed by SubscribeToShard to reposition the consumer. The code implements retry logic for 429 throttling and wraps the operation in an atomic execution pattern. Automatic partition assignment triggers fire upon successful repositioning.
interface ResetResult {
requestId: string;
shardId: string;
iteratorType: string;
consumerRepositioned: boolean;
latencyMs: number;
commitSuccessRate: number;
}
class OffsetResetter {
private readonly kinesis: KinesisClient;
private readonly eventBridge: EventBridgeClient;
constructor(region: string) {
this.kinesis = new KinesisClient({ region });
this.eventBridge = new EventBridgeClient({ region });
}
async resetConsumer(
streamName: string,
directive: ResetDirective,
webhookUrl: string
): Promise<ResetResult> {
const requestId = uuidv4();
const startTime = Date.now();
const iteratorResponse = await this.executeWithRetry(async () => {
const command = new GetShardIteratorCommand({
StreamName: streamName,
ShardId: directive.shardId,
ShardIteratorType: 'AT_TIMESTAMP',
Timestamp: directive.timestampDirective,
});
return this.kinesis.send(command);
});
const shardIterator = iteratorResponse.ShardIterator;
if (!shardIterator) {
throw new Error('Failed to acquire shard iterator for consumer repositioning');
}
const subscribeResponse = await this.executeWithRetry(async () => {
const command = new SubscribeToShardCommand({
StreamName: streamName,
ShardId: directive.shardId,
StartingPosition: {
Timestamp: directive.timestampDirective,
},
});
return this.kinesis.send(command);
});
const latencyMs = Date.now() - startTime;
const commitSuccessRate = subscribeResponse.EventsStream ? 1.0 : 0.0;
await this.triggerPartitionAssignment(directive.shardId, webhookUrl);
await this.publishAuditLog(requestId, directive, latencyMs, commitSuccessRate);
return {
requestId,
shardId: directive.shardId,
iteratorType: 'AT_TIMESTAMP',
consumerRepositioned: true,
latencyMs,
commitSuccessRate,
};
}
private async executeWithRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (err: any) {
lastError = err;
if (err.name === 'ThrottlingException' || err.statusCode === 429) {
const backoff = Math.min(1000 * Math.pow(2, attempt), 10000);
await new Promise(resolve => setTimeout(resolve, backoff));
continue;
}
throw err;
}
}
throw lastError || new Error('Retry limit exceeded');
}
private async triggerPartitionAssignment(shardId: string, webhookUrl: string): Promise<void> {
await axios.post(webhookUrl, {
event: 'PARTITION_ASSIGNMENT_TRIGGERED',
shardId,
timestamp: new Date().toISOString(),
action: 'CONSUMER_REPOSITIONED',
}, {
headers: { 'Content-Type': 'application/json' },
timeout: 5000,
});
}
private async publishAuditLog(
requestId: string,
directive: ResetDirective,
latencyMs: number,
commitSuccessRate: number
): Promise<void> {
const command = new PutEventsCommand({
Entries: [{
Source: 'genesys.eventbridge.offset-reset',
EventBusName: 'default',
DetailType: 'OFFSET_RESET_AUDIT',
Detail: JSON.stringify({
requestId,
shardId: directive.shardId,
timestampDirective: directive.timestampDirective.toISOString(),
latencyMs,
commitSuccessRate,
auditTimestamp: new Date().toISOString(),
}),
}],
});
await this.eventBridge.send(command);
}
}
Complete Working Example
The following script combines authentication, validation, and atomic reset execution into a single runnable module. Replace the environment variables with your Genesys Cloud and AWS credentials.
async function main(): Promise<void> {
const GENESYS_ENV = process.env.GENESYS_ENV || 'us-east-1';
const GENESYS_CLIENT_ID = process.env.GENESYS_CLIENT_ID!;
const GENESYS_CLIENT_SECRET = process.env.GENESYS_CLIENT_SECRET!;
const SUBSCRIPTION_ID = process.env.SUBSCRIPTION_ID!;
const AWS_REGION = process.env.AWS_REGION || 'us-east-1';
const WEBHOOK_URL = process.env.WEBHOOK_URL || 'https://monitoring.example.com/offset-hooks';
const auth = new GenesysAuthService(GENESYS_ENV, GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET);
const manager = new GenesysEventBridgeManager(auth);
const target = await manager.getSubscriptionTarget(SUBSCRIPTION_ID);
console.log(`Resolved target stream: ${target.streamName} in ${target.region}`);
const resetDirective = {
shardId: 'shardId-00000000-1234-5678-9abc-def012345678',
offsetMatrix: {
'partition-0': '48920193847562910234567890123456789012345678901234567890',
'partition-1': '48920193847562910234567890123456789012345678901234567891',
},
timestampDirective: new Date(Date.now() - (6 * 3600000)),
maxRewindHours: 24,
};
const validatedDirective = OffsetValidationPipeline.validate(resetDirective);
console.log('Offset directive validated against engine constraints');
const resetter = new OffsetResetter(target.region);
try {
const result = await resetter.resetConsumer(target.streamName, validatedDirective, WEBHOOK_URL);
console.log('Reset completed successfully:', JSON.stringify(result, null, 2));
} catch (error: any) {
console.error('Reset failed:', error.message || error);
process.exit(1);
}
}
main().catch(console.error);
Common Errors & Debugging
Error: 401 Unauthorized
- What causes it: The OAuth client credentials are incorrect, the token has expired, or the environment URL is malformed.
- How to fix it: Verify the
GENESYS_ENVmatches your Genesys Cloud deployment region. Ensure the client ID and secret have not been rotated. Check the token cache expiration logic. - Code showing the fix: The
GenesysAuthServiceautomatically refreshes tokens before expiry. Add explicit token validation in your deployment pipeline.
Error: 403 Forbidden
- What causes it: The OAuth client lacks the
eventbridge:readoreventbridge:writescopes, or the AWS IAM role lackskinesis:GetShardIteratorpermissions. - How to fix it: Navigate to the Genesys Cloud admin console, locate the API integration, and append the missing scopes. Update the AWS IAM policy to include explicit Kinesis and EventBridge actions.
- Code showing the fix: Scope validation occurs at the API gateway level. The TypeScript code does not modify scopes. Update the Genesys Cloud OAuth client configuration directly.
Error: 429 Too Many Requests
- What causes it: The Genesys Cloud API enforces rate limits per client, or AWS Kinesis returns
ThrottlingExceptiondue to subscription limits. - How to fix it: Implement exponential backoff with jitter. The
executeWithRetrymethod handles this automatically. Reduce concurrent reset operations across partitions. - Code showing the fix: The retry loop in
executeWithRetrycatchesThrottlingExceptionandstatusCode === 429, applies backoff, and continues until success or max retries.
Error: ValidationException
- What causes it: The offset matrix violates monotonically increasing constraints, the timestamp directive exceeds the maximum rewind window, or the epoch falls outside Genesys Cloud retention boundaries.
- How to fix it: Adjust the
timestampDirectiveto stay within the configuredmaxRewindHours. Verify that sequence numbers inoffsetMatrixstrictly increase per partition. - Code showing the fix: The
OffsetValidationPipelinethrows explicit errors with actionable messages. Log the validation output before callingresetConsumer.
Error: ResourceNotFoundException
- What causes it: The subscription ID does not exist, or the Kinesis stream name does not match the Genesys Cloud EventBridge target configuration.
- How to fix it: Verify the subscription ID in the Genesys Cloud EventBridge console. Ensure the target stream is active and matches the region specified in AWS credentials.
- Code showing the fix: Add a pre-flight check that calls
kinesis.listStreams()and verifies the stream name exists before executing the reset.