Node.js lambda handling gc webhook payload structure confusion

Can anyone clarify the correct way to parse the nested event data when consuming a Genesys Cloud webhook in a Node.js Lambda function? i am writing a consumer for the routing.queue.member.updated event. the documentation shows the payload structure, but when i log the event object in the lambda handler, the structure seems different than expected.

i am using the standard aws lambda nodejs runtime. here is my current handler code:

exports.handler = async (event) => {
 console.log('received event:', JSON.stringify(event, null, 2));
 
 // trying to access the queue id
 const queueId = event.body.queue.id;
 
 // this throws typeerror: cannot read property 'queue' of undefined
 console.log('queue id:', queueId);
 
 return {
 statusCode: 200,
 body: 'processed'
 };
};

the error i am getting is TypeError: Cannot read property 'queue' of undefined. when i check the cloudwatch logs, the event.body is a string, not an object. i have to parse it first. but even after parsing JSON.parse(event.body), the structure is still not matching the api documentation examples. the documentation says the payload should be:

{
 "id": "abc-123",
 "queue": {
 "id": "queue-xyz",
 "name": "support"
 }
}

but in the lambda, after parsing the body, the object looks like:

{
 "event_id": "def-456",
 "event_type": "routing.queue.member.updated",
 "data": {
 "queue": {
 "id": "queue-xyz"
 }
 }
}

so the actual data is nested under data. is this specific to the webhook delivery format vs the api response format? i am trying to build a robust parser that can handle this without hardcoding paths. should i be using the event.data property instead? or is there a standard library i should use to normalize this? i am building a typed python client for other services, but for this integration, i am stuck on the node side. any advice on how to reliably extract the queue.id from this payload structure?

The best way to fix this is to ensure you are using base64url encoding without padding. the docs say “the code challenge must be base64url encoded” but most js libraries add padding which breaks it…

It depends, but generally… Lambda wraps the body in base64 if you don’t disable it.

  1. Check event.isBase64Encoded.
  2. Decode with Buffer.from(event.body, 'base64').toString().
  3. Parse JSON.

The best way to fix this is to handle the base64 decoding explicitly before parsing the JSON payload. I encountered similar confusion while building my .NET reporting dashboard, where the SDK handled some serialization but the raw webhook payload required manual parsing.

Cause:
AWS Lambda automatically base64-encodes the body for binary content or when the payload size exceeds certain thresholds. If event.isBase64Encoded is true, the event.body is not valid JSON string. Attempting to JSON.parse() the raw base64 string results in a syntax error or unexpected structure.

Solution:
Validate the encoding flag first. If true, decode using Buffer. Then parse the resulting string. Here is the robust pattern I use:

exports.handler = async (event) => {
 let bodyString;

 if (event.isBase64Encoded) {
 // Decode base64 to get the actual JSON string
 bodyString = Buffer.from(event.body, 'base64').toString('utf-8');
 } else {
 bodyString = event.body;
 }

 try {
 const payload = JSON.parse(bodyString);
 
 // Access the nested event data
 // For routing.queue.member.updated, the data is usually in payload.data
 const memberData = payload.data;
 
 console.log('Parsed Member ID:', memberData.id);
 
 return {
 statusCode: 200,
 body: JSON.stringify({ message: 'Webhook processed' })
 };
 } catch (error) {
 console.error('Parse error:', error.message);
 return {
 statusCode: 500,
 body: JSON.stringify({ error: 'Failed to parse webhook payload' })
 };
 }
};

This approach ensures you always work with a valid JSON object regardless of how Lambda encoded the input. It avoids the structure confusion by normalizing the input stream before accessing properties.

The decoding step is correct, but you are missing the event envelope unwrapping. Genesys Cloud webhooks wrap the actual payload in an events array. If you parse event.body directly, you get a list, not the specific routing.queue.member.updated object.

import { APIGatewayProxyEvent, Context, APIGatewayProxyResult } from 'aws-lambda';

export const handler = async (event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> => {
 let body: any;
 if (event.isBase64Encoded) {
 body = JSON.parse(Buffer.from(event.body, 'base64').toString('utf-8'));
 } else {
 body = JSON.parse(event.body);
 }

 // GC Webhook Envelope
 const gcEvent = body.events[0]; 
 
 if (!gcEvent) {
 return { statusCode: 400, body: 'No events in payload' };
 }

 console.log('Member ID:', gcEvent.data.memberId);
 return { statusCode: 200, body: 'OK' };
};

Always validate body.events exists before indexing. See the webhook payload structure guide for details.