PayPal integration with Next, Lambda and API Gateway - 400 Invalid Request

logix92
New Community Member

Hi all,

 

I am integrating PayPal into a Next JS front-end, hosted on AWS Amplify using Lambda and API Gateway to communicate with PayPal API.

 

For now this is Sandbox to test integration.


No matter what I try, I keep getting 400 Invalid Request (Which is an else statement in my Lambda function). I am sure I have followed PayPal documentation as closely as possible.

In terms of API gateway config I have ensured CORS is setup correctly and the request is definitely hitting the Lambda but failing for some reason I can not figure out.

My Front-end code is like so:

 

// src/utils/paypalUtils.js

const API_ENDPOINT = 'https://1uwgpcbn4f.execute-api.eu-west-1.amazonaws.com';

export const createOrder = async (course) => {
    try {
        console.log('Creating order for course:', JSON.stringify(course, null, 2));
        const response = await fetch(`${API_ENDPOINT}/create-paypal-order`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                cart: [
                    {
                        id: course.slug,
                        name: course.title,
                        price: course.price,
                        quantity: "1",
                        deliveryOption: course.deliveryOption
                    }
                ]
            }),
        });

        if (!response.ok) {
            const errorData = await response.json();
            throw new Error(`Failed to create order: ${response.status} ${response.statusText}. ${JSON.stringify(errorData)}`);
        }

        const orderData = await response.json();
        if (!orderData.id) {
            throw new Error(`Order ID is missing from the response: ${JSON.stringify(orderData)}`);
        }
        console.log('Order created successfully:', orderData.id);
        return orderData.id;
    } catch (error) {
        console.error('Error creating order:', error);
        throw error;
    }
};

export const onApprove = async (data, actions) => {
    try {
        const response = await fetch(`${API_ENDPOINT}/capture-paypal-order?orderID=${data.orderID}`, {
            method: 'POST',
        });

        if (!response.ok) {
            throw new Error('Failed to capture order');
        }

        const captureData = await response.json();

        const captureStatus = captureData.status;
        if (captureStatus === "COMPLETED") {
            console.log('Payment completed successfully', captureData);
            return { success: true, data: captureData };
        } else {
            throw new Error(`Payment not completed. Status: ${captureStatus}`);
        }
    } catch (error) {
        console.error('Error capturing order:', error);
        throw error;
    }
};

 


My Lambda function is like so:

 

import https from 'https';
import { v4 as uuidv4 } from 'uuid'; // Import the uuid function

const PAYPAL_API_BASE = 'api-m.sandbox.paypal.com'; // Use 'api-m.paypal.com' for production
const PAYPAL_CLIENT_ID = process.env.PAYPAL_CLIENT_ID;
const PAYPAL_CLIENT_SECRET = process.env.PAYPAL_CLIENT_SECRET;

const headers = {
    "Access-Control-Allow-Origin": "*", // Adjust this to your frontend URL for production
    "Access-Control-Allow-Headers": "Content-Type",
    "Access-Control-Allow-Methods": "OPTIONS,POST,GET"
};

async function getAccessToken() {
    const auth = Buffer.from(`${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`).toString('base64');
    const options = {
        hostname: PAYPAL_API_BASE,
        path: '/v1/oauth2/token',
        method: 'POST',
        headers: {
            'Authorization': `Basic ${auth}`,
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    };

    return new Promise((resolve, reject) => {
        const req = https.request(options, (res) => {
            let data = '';
            res.on('data', (chunk) => data += chunk);
            res.on('end', () => {
                const jsonData = JSON.parse(data);
                resolve(jsonData.access_token);
            });
        });

        req.on('error', reject);
        req.write('grant_type=client_credentials');
        req.end();
    });
}

async function createPayPalOrder(accessToken, orderData) {
    const options = {
        hostname: PAYPAL_API_BASE,
        path: '/v2/checkout/orders',
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Content-Type': 'application/json',
            'PayPal-Request-Id': uuidv4() // Generate a unique ID for idempotency
        }
    };

    return new Promise((resolve, reject) => {
        const req = https.request(options, (res) => {
            let data = '';
            res.on('data', (chunk) => data += chunk);
            res.on('end', () => {
                if (res.statusCode >= 200 && res.statusCode < 300) {
                    resolve(JSON.parse(data));
                } else {
                    reject(new Error(`Failed to create order: ${res.statusCode} ${data}`));
                }
            });
        });

        req.on('error', reject);
        req.write(JSON.stringify(orderData));
        req.end();
    });
}

async function capturePayPalOrder(accessToken, orderId) {
    const options = {
        hostname: PAYPAL_API_BASE,
        path: `/v2/checkout/orders/${orderId}/capture`,
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${accessToken}`,
            'Content-Type': 'application/json'
        }
    };

    return new Promise((resolve, reject) => {
        const req = https.request(options, (res) => {
            let data = '';
            res.on('data', (chunk) => data += chunk);
            res.on('end', () => {
                if (res.statusCode >= 200 && res.statusCode < 300) {
                    resolve(JSON.parse(data));
                } else {
                    reject(new Error(`Failed to capture order: ${res.statusCode} ${data}`));
                }
            });
        });

        req.on('error', reject);
        req.end();
    });
}

export const handler = async (event) => {
    console.log('Received event:', JSON.stringify(event, null, 2));

    const { httpMethod, path, body, queryStringParameters } = event;

    try {
        const accessToken = await getAccessToken();

        if (httpMethod === 'POST' && path === '/create-paypal-order') {
            const cartData = JSON.parse(body);
            console.log('Parsed cart data:', cartData);

            // Construct the order data
            const orderData = {
                intent: 'CAPTURE',
                purchase_units: cartData.cart.map(item => ({
                    amount: {
                        currency_code: 'GBP',
                        value: item.price,
                        breakdown: {
                            item_total: {
                                currency_code: 'GBP',
                                value: item.price
                            }
                        }
                    },
                    items: [
                        {
                            name: item.name,
                            description: `${item.name} - ${item.deliveryOption} delivery`,
                            sku: item.id,
                            unit_amount: {
                                currency_code: 'GBP',
                                value: item.price
                            },
                            quantity: item.quantity
                        }
                    ]
                }))
            };

            console.log('PayPal order data:', orderData);

            const result = await createPayPalOrder(accessToken, orderData);
            console.log('PayPal API response:', result);

            return {
                statusCode: 200,
                headers: headers,
                body: JSON.stringify({ id: result.id }),
            };
        } else if (httpMethod === 'POST' && path === '/capture-paypal-order') {
            const orderId = queryStringParameters.orderID;
            const result = await capturePayPalOrder(accessToken, orderId);
            return {
                statusCode: 200,
                headers: headers,
                body: JSON.stringify(result),
            };
        } else {
            return {
                statusCode: 400,
                headers: headers,
                body: JSON.stringify({ message: 'Invalid request' }),
            };
        }
    } catch (error) {
        console.error('Error:', error);
        return {
            statusCode: 500,
            headers: headers,
            body: JSON.stringify({ error: 'Internal server error', details: error.message })
        };
    }
};

 

 

A cloudwatch log of said event:

 

2024-10-01T21:59:28.272Z	26115af9-00e1-4d66-a278-52a3e903684a	INFO	Received event: {
    "version": "2.0",
    "routeKey": "POST /create-paypal-order",
    "rawPath": "/create-paypal-order",
    "rawQueryString": "",
    "headers": {
        "accept": "*/*",
        "accept-encoding": "gzip, deflate, br, zstd",
        "accept-language": "en-GB,en-US;q=0.9,en;q=0.8",
        "content-length": "136",
        "content-type": "application/json",
        "host": "1uwgpcbn4f.execute-api.eu-west-1.amazonaws.com",
        "origin": "https://main.d2ige300djz6d5.amplifyapp.com",
        "priority": "u=1, i",
        "referer": "https://main.d2ige300djz6d5.amplifyapp.com/",
        "sec-ch-ua": "\"Google Chrome\";v=\"129\", \"Not=A?Brand\";v=\"8\", \"Chromium\";v=\"129\"",
        "sec-ch-ua-mobile": "?0",
        "sec-ch-ua-platform": "\"macOS\"",
        "sec-fetch-dest": "empty",
        "sec-fetch-mode": "cors",
        "sec-fetch-site": "cross-site",
        "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
        "x-amzn-trace-id": "Root=1-66fc70bf-04fc2b5c0ea975cd522342ed",
        "x-forwarded-for": "154.60.94.168",
        "x-forwarded-port": "443",
        "x-forwarded-proto": "https"
    },
    "requestContext": {
        "accountId": "682033468988",
        "apiId": "1uwgpcbn4f",
        "domainName": "1uwgpcbn4f.execute-api.eu-west-1.amazonaws.com",
        "domainPrefix": "1uwgpcbn4f",
        "http": {
            "method": "POST",
            "path": "/create-paypal-order",
            "protocol": "HTTP/1.1",
            "sourceIp": "154.60.94.168",
            "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
        },
        "requestId": "e_aOCj50DoEEJwA=",
        "routeKey": "POST /create-paypal-order",
        "stage": "$default",
        "time": "01/Oc[Removed. Phone #s not permitted]+0000",
        "timeEpoch": 1727819967904
    },
    "body": "{\"cart\":[{\"id\":\"ecg-course-for-beginners\",\"name\":\"ECG Course for Beginners\",\"price\":\"299.99\",\"quantity\":\"1\",\"deliveryOption\":\"remote\"}]}",
    "isBase64Encoded": false
}

 


I have a package.json for the uuid component setup so there is no issue there, and I have setup environment variables within my Lambda that have the paypal client and secret.

Any insights would be appreciated.

Login to Me Too
1 ACCEPTED SOLUTION

Accepted Solutions
Solved

logix92
New Community Member

OK I have found a solution to this - I could delete post, but figured someone else may find it useful if coming across - the issue was that when using Lambda integration vs Lambda proxy integration (I was using direct lambda integration, not proxy) - the httpMethod and path needed to be accessed using the `event.` handle i.e.

 

As the failure point was the `if` statement for httpmethod and path, I used a combination of cloudwatch logs to observe the `event` object being sent through to verify the structure of it. 

 

The way to access the lambda method and path is like below:

event.requestContext.http.method

event.requestContext.http.path

 

Once I changed this, the if was accessed properly and I was able to successfully proceed to entering email/password within the PayPal window as the order was successfully created.

 

Please mark this as resolved.

 

 

View solution in original post

Login to Me Too
1 REPLY 1
Solved

logix92
New Community Member

OK I have found a solution to this - I could delete post, but figured someone else may find it useful if coming across - the issue was that when using Lambda integration vs Lambda proxy integration (I was using direct lambda integration, not proxy) - the httpMethod and path needed to be accessed using the `event.` handle i.e.

 

As the failure point was the `if` statement for httpmethod and path, I used a combination of cloudwatch logs to observe the `event` object being sent through to verify the structure of it. 

 

The way to access the lambda method and path is like below:

event.requestContext.http.method

event.requestContext.http.path

 

Once I changed this, the if was accessed properly and I was able to successfully proceed to entering email/password within the PayPal window as the order was successfully created.

 

Please mark this as resolved.

 

 

Login to Me Too

Haven't Found your Answer?

It happens. Hit the "Login to Ask the community" button to create a question for the PayPal community.