How to Migrate from PayPal REST API Calls to the Official Server SDK in Next.js
3 min read
Learn why and how I migrated from manual PayPal REST API calls to the official Server SDK in a Next.js + TypeScript project, gaining type safety and more stable payments.Contents
- Motivations
- Migration to PayPal Server SDK
- Previous Approach (PayPal REST API)
- New Approach (PayPal Server SDK)
- Results
- Limitation
- Conclusion
- References
Motivations
I had a PayPal integration running smoothly for over a year (using REST API). A month ago, customers suddenly reported payment failures. Logs showed that calls to PayPal REST endpoints were being blocked by Cloudflare. I didn’t have a clear root cause yet, so instead of debugging at the edge, I tried the newly stable @paypal/paypal-server-sdk
to simplify my stack and reduce moving parts.
Stack context: Next.js + TanStack Query + tRPC + Drizzle ORM.
Migration to PayPal Server SDK
Switching was surprisingly straightforward:
- Initialize client with existing PayPal Client ID and Secret.
- Use the Order Controller from the SDK.
- Replace my manual
axios
calls with:createOrder
captureOrder
Previous Approach (PayPal REST API)
- Custom axios instance
- Interceptor requested an OAuth access token and attached it to each request
- Manual DTO handling and error logging
import type { AxiosError } from 'axios'import { type CreateOrderRequestBody, type OrderResponseBody,} from '@paypal/paypal-js'import axios from 'axios'import { z } from 'zod'
import { env } from '~/env'
export const paypalClient = axios.create({ baseURL: env.PAYPAL_API_BASE_URL, headers: { 'Content-Type': 'application/json', },})
paypalClient.interceptors.request.use( async function (config) { switch (config.url) { case '/v1/oauth2/token': return config
default: const accessToken = await generateAccessToken() config.headers.setAuthorization(`Bearer ${accessToken}`) return config } }, function (error) { return Promise.reject(error) })
paypalClient.interceptors.response.use( function (response) { return response }, function (error) { console.log(error) return Promise.reject(error) })
async function generateAccessToken() { const { data } = await paypalClient.post<{ access_token: string }>( '/v1/oauth2/token', 'grant_type=client_credentials', { auth: { username: env.NEXT_PUBLIC_PAYPAL_CLIENT_ID, password: env.PAYPAL_CLIENT_SECRET, }, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, } )
return data.access_token}
export async function createOrder(data: CreateOrderRequestBody) { const res = await paypalClient.post<OrderResponseBody>( '/v2/checkout/orders', data ) return res.data}
export async function capturePayment(orderID: string) { const res = await paypalClient.post<OrderResponseBody>( `/v2/checkout/orders/${orderID}/capture` ) return res.data}
New Approach (PayPal Server SDK)
- Official SDK client manages auth and DTOs
- OrdersController exposes type-safe methods
- Cleaner tRPC routes and less boilerplate
import { Client, Environment, LogLevel, OrdersController,} from '@paypal/paypal-server-sdk'
import { env } from '~/env'
const client = new Client({ clientCredentialsAuthCredentials: { oAuthClientId: env.NEXT_PUBLIC_PAYPAL_CLIENT_ID, oAuthClientSecret: env.PAYPAL_CLIENT_SECRET, }, timeout: 0, environment: env.NODE_ENV === 'production' ? Environment.Production : Environment.Sandbox, logging: { logLevel: LogLevel.Info, logRequest: { logBody: true, }, logResponse: { logHeaders: true, }, },})
export const orderController = new OrdersController(client)
// In a tRPC create order route:const res = await orderController.createOrder({ body })
// In a tRPC capture order route:const res = await orderController.captureOrder({ id: input.paypalOrderID,})
Results
- Payments stable again, no Cloudflare blocks observed so far.
- Type-safe methods and clearer error handling.
- Less boilerplate for authentication and request building.
Under the hood, the SDK still talks to the same REST API endpoints, but now it also handles DTO parsing, type safety, and error formatting.
Limitation
The PayPal Server SDK provides integration access to the PayPal REST APIs. The API endpoints are divided into distinct controllers:
- Orders Controller: Orders API v2
- Payments Controller: Payments API v2
- Vault Controller: Payment Method Tokens API v3 Available in the US only.
For me, this means my main payment flows (creating and capturing orders) are fully supported, which is enough for most of my day-to-day needs. But if I ever need other PayPal features outside these controllers, I’ll still fall back to my old REST API approach, which I already have working.
Conclusion
Since my workflow is mostly about creating and capturing orders, the PayPal Server SDK fits perfectly. It cuts down boilerplate, adds type safety, and handles the low-level REST details for me. For anything else, I can just keep using my existing REST integration. This way, I get the best of both worlds — cleaner code for my main payment logic, and flexibility for the rest.