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.
... View more