On this page
No Headings
Last updated: August 14, 2026
Upgrade your existing PayPal Orders API v1 integration to v2.
The Orders API is a server-side API for creating a checkout order and capturing or authorizing payment after the buyer approves it. Orders v2 introduces a unified payment_source model for PayPal, cards, Google Pay, and Apple Pay.
If you currently use v1, upgrading to v2 provides safer retries with idempotency keys, webhooks in place of IPN, and support for order updates and shipment tracking after checkout. Follow this migration guide to update your endpoint URLs and request and response field shapes, and adapt to new operation semantics, status values, and notification mechanisms for v2.
Audience: This guide is for developers and integration engineers migrating an existing Orders v1 integration to v2. It assumes familiarity with the Orders API, OAuth 2.0, and your own codebase's current v1 implementation.
payment_instruction fields (platform_fees, disbursement_mode) require PayPal Complete Payments Platform approval.api-m.sandbox.paypal.com as the base URL for all testing before go-live.partner_fee_details or disbursement_mode. Most integrations do not need this.DELETE /v1/checkout/orders/{id} does not exist in v2. If your integration cancels orders, redesign that flow before migrating.POST .../pay has been split: The single v1 "pay" call becomes either POST .../capture or POST .../authorize depending on your order intent. This is a flow change, not a rename.Find in your project:
/v1/checkout/ordersPOST /v1/checkout/ordersDELETE /v1/checkout/orders//v1/checkout/orders/{id}/payintent.*SALEdisbursement_modenotify_urlpartner_fee_detailspayment_linked_groupgross_total_amountsupplementary_datarisk_correlation_idredirect_urlsapproval_urlpayment_summarypayer_info| Item | Status | Notes |
|---|---|---|
| OAuth 2.0 Bearer token | Yes | Same token endpoint, same pattern |
PayPal-Partner-Attribution-Id | Yes | Still supported |
application_context.brand_name, locale, shipping_preference | No | Moved to payment_source.{source}.experience_context |
application_context.user_action | No | Moved to experience_context.user_action; value "commit" becomes "PAY_NOW" |
purchase_units[] array structure | Partial | Array retained; field names and nesting changed |
intent: "AUTHORIZE" | Yes | Unchanged |
intent: "SALE" | No | Renamed to "CAPTURE" |
POST .../pay endpoint | No | Split into capture and authorize |
DELETE .../orders/{id} | No | No cancel endpoint in v2 |
amount.total / amount.currency | No | Renamed to value / currency_code |
amount.details sub-fields | No | Replaced by amount.breakdown Money objects |
partner_fee_details | No | Replaced by payment_instruction.platform_fees[], PayPal Complete Payments Platform only |
disbursement_mode on pay body | No | Moved to create-time payment_instruction, PayPal Complete Payments Platform only |
notify_url IPN | No | Replaced by webhooks |
gross_total_amount | No | No v2 equivalent |
payment_linked_group | No | No v2 equivalent |
Address fields (line1, city, state) | No | Renamed throughout |
The endpoint mapping and field-level mapping tables in this section map every v1 endpoint, request field, and response field to its v2 equivalent.
| v1 endpoint | v2 replacement | Category |
|---|---|---|
POST /v1/checkout/orders | POST /v2/checkout/orders | Restructured |
GET /v1/checkout/orders/{order_id} | GET /v2/checkout/orders/{id} | Direct |
DELETE /v1/checkout/orders/{order_id} | No equivalent | No replacement |
POST .../pay (SALE intent) | POST /v2/checkout/orders/{id}/capture | Split |
POST .../pay (AUTHORIZE intent) | POST /v2/checkout/orders/{id}/authorize | Split |
| No equivalent | PATCH /v2/checkout/orders/{id} | New in v2 |
| No equivalent | POST .../confirm-payment-source | New in v2 |
| No equivalent | POST .../track | New in v2 |
| v1 field | v2 field | Category |
|---|---|---|
intent: "SALE" | intent: "CAPTURE" | Behavioral change |
redirect_urls.return_url | payment_source.{source}.experience_context.return_url | Restructured |
redirect_urls.cancel_url | payment_source.{source}.experience_context.cancel_url | Restructured |
amount.currency | amount.currency_code | Restructured |
amount.total | amount.value | Restructured |
amount.details.subtotal | amount.breakdown.item_total.value | Restructured |
amount.details.tax | amount.breakdown.tax_total.value | Restructured |
amount.details.shipping | amount.breakdown.shipping.value | Restructured |
amount.details.handling_fee | amount.breakdown.handling.value | Restructured |
purchase_units[].custom | purchase_units[].custom_id | Restructured |
items[].price and items[].currency | items[].unit_amount.value and items[].unit_amount.currency_code | Restructured |
shipping_address.line1 | shipping.address.address_line_1 | Restructured |
shipping_address.city | shipping.address.admin_area_2 | Restructured |
shipping_address.state | shipping.address.admin_area_1 | Restructured |
shipping_address.recipient_name | shipping.name.full_name | Restructured |
partner_fee_details | payment_instruction.platform_fees[] (PayPal Complete Payments Platform only) | Restructured |
disbursement_mode (on pay body) | payment_instruction.disbursement_mode, on create (PayPal Complete Payments Platform only) | Restructured |
notify_url | Webhooks | Different product |
gross_total_amount | No equivalent | No replacement |
payment_linked_group | No equivalent | No replacement |
supplementary_data[name="risk_correlation_id"] | PayPal-Client-Metadata-Id request header | Restructured |
supplementary_data[name="buyer_ipaddress"] | No equivalent | No replacement |
supplementary_data[name="external_channel"] | No equivalent | No replacement |
application_context.brand_name | payment_source.{source}.experience_context.brand_name | Restructured |
application_context.locale | payment_source.{source}.experience_context.locale | Restructured |
application_context.shipping_preference | payment_source.{source}.experience_context.shipping_preference | Restructured |
application_context.user_action: "commit" | payment_source.{source}.experience_context.user_action: "PAY_NOW" | Behavioral change |
| v1 response field | v2 response field | Category |
|---|---|---|
purchase_units[].payment_summary.captures[] | purchase_units[].payments.captures[] | Restructured |
purchase_units[].payment_summary.sales[] | No equivalent | No replacement |
capture.transaction_fee | capture.seller_receivable_breakdown.paypal_fee | Restructured |
HATEOAS rel: "approval_url" | rel: "approve" (no payment_source) or rel: "payer-action" (with payment_source) | Behavioral change |
payer_info.email | payer.email_address | Restructured |
payer_info.first_name | payer.name.given_name | Restructured |
payer_info.last_name | payer.name.surname | Restructured |
order.status: "FAILED" | order.status: "VOIDED" | Behavioral change |
Update your create order request field by field, starting with the intent value.
SALE to CAPTUREMost common migration failure: Sending "intent": "SALE" to the v2 endpoint returns a 400 Bad Request validation error. This is the highest-frequency mistake in this migration.
v1:
{ "intent": "SALE" }v2:
{ "intent": "CAPTURE" }payment_source.{source}.experience_contextv1 sends return_url and cancel_url at the top level of the request. v2 nests them inside experience_context under the specific payment source.
v1, top-level (required):
{
"redirect_urls": {
"return_url": "https://.../return",
"cancel_url": "https://.../cancel"
}
}v2, per payment source (examples):
// PayPal Wallet
"payment_source": {
"paypal": {
"experience_context": {
"return_url": "https://.../return",
"cancel_url": "https://.../cancel"
}
}
}
// Google Pay
"payment_source": {
"google_pay": {
"experience_context": {
"return_url": "https://.../return",
"cancel_url": "https://.../cancel"
}
}
}
// Apple Pay
"payment_source": {
"apple_pay": {
"experience_context": {
"return_url": "https://.../return",
"cancel_url": "https://.../cancel"
}
}
}
// Card
"payment_source": {
"card": {
"experience_context": {
"return_url": "https://.../return",
"cancel_url": "https://.../cancel"
}
}
}experience_context is a per-source pattern. application_context is deprecated. In v2, return_url, cancel_url, brand_name, locale, shipping_preference, and user_action all move inside an experience_context object nested under the specific payment source being used. The top-level application_context object is deprecated. Do not use it for new integrations.
v2 renames amount.total and amount.currency to value and currency_code, and moves the breakdown fields into Money objects nested under breakdown.
v1:
{
"amount": {
"currency": "USD",
"total": "107.47",
"details": {
"subtotal": "99.99",
"tax": "5.48",
"shipping": "2.00"
}
}
}v2:
{
"amount": {
"currency_code": "USD",
"value": "107.47",
"breakdown": {
"item_total": {
"currency_code": "USD",
"value": "99.99"
},
"tax_total": {
"currency_code": "USD",
"value": "5.48"
},
"shipping": {
"currency_code": "USD",
"value": "2.00"
}
}
}
}Breakdown validation is enforced. v2 validates that value equals the sum of all breakdown components. A mismatch returns a 422 Unprocessable Entity. Validate this sum server-side before calling the API.
v2 restructures item pricing into a unit_amount object with currency_code, replacing the flat price and currency strings from v1.
v1:
{
"name": "NeoPhone",
"sku": "sku03",
"price": "99.99",
"currency": "USD",
"quantity": "1",
"tax": "5.00"
}v2:
{
"name": "NeoPhone",
"sku": "sku03",
"unit_amount": {
"currency_code": "USD",
"value": "99.99"
},
"tax": {
"currency_code": "USD",
"value": "5.00"
},
"quantity": "1",
"category": "PHYSICAL_GOODS"
}v2 renames shipping_address to shipping.address and separates recipient_name into its own shipping.name object.
v1:
{
"shipping_address": {
"recipient_name": "John Doe",
"line1": "2211 N First Street",
"line2": "Building 17",
"city": "San Jose",
"state": "CA",
"postal_code": "95131",
"country_code": "US"
}
}v2:
{
"shipping": {
"name": {
"full_name": "John Doe"
},
"address": {
"address_line_1": "2211 N First Street",
"address_line_2": "Building 17",
"admin_area_2": "San Jose",
"admin_area_1": "CA",
"postal_code": "95131",
"country_code": "US"
}
}
}The following requests show how partner_fee_details in v1 maps to the payment_instruction.platform_fees[] array in v2.
PayPal Complete Payments Platform only: The payment_instruction.platform_fees[] object is available exclusively to approved PayPal Complete Payments Platform accounts. Including this object without the required enrollment returns a permissions error. See Get Started with Multiparty to apply for access before implementing this feature.
v1:
{
"partner_fee_details": {
"receiver": {
"email": "[email protected]"
},
"amount": {
"currency": "USD",
"value": "1.00"
}
}
}v2:
{
"payment_instruction": {
"platform_fees": [
{
"amount": {
"currency_code": "USD",
"value": "1.00"
},
"payee": {
"email_address": "[email protected]"
}
}
]
}
}Timing change: In v1, you sent disbursement_mode in the body of a POST .../pay request at execute time. In v2, you need to set disbursement_mode in the purchase_units[].payment_instruction object when you create the order. If you pass disbursement_mode in the body of a v2 capture request, the API accepts the value but doesn't apply it.
PayPal Complete Payments Platform only: The payment_instruction object and disbursement_mode field are available exclusively to approved PayPal Complete Payments Platform accounts. See Get Started with Multiparty to apply for access before implementing this feature.
v2, set on create order:
{
"purchase_units": [
{
"payment_instruction": {
"disbursement_mode": "DELAYED"
}
}
]
}supplementary_data (risk_correlation_id), moved to request headerv1, request body:
{
"application_context": {
"supplementary_data": [
{
"name": "risk_correlation_id",
"value": "9N8554567F903282T"
},
{
"name": "buyer_ipaddress",
"value": "109.20.212.116"
},
{
"name": "external_channel",
"value": "WEB"
}
]
}
}v2, request header:
PayPal-Client-Metadata-Id: 9N8554567F903282TBody to header. The risk_correlation_id value moves out of the request body entirely. Pass it as the PayPal-Client-Metadata-Id HTTP request header, a GUID from Fraudnet or Dyson used by PayPal's risk systems to correlate calls and reduce decline rates.
buyer_ipaddress and external_channel have no v2 equivalent. These two supplementary_data entries are not replicated anywhere in the v2 API. Remove them from your integration.
The following requests apply every field-level change from this step together, from a complete v1 create order call to its v2 equivalent.
v1, create order:
POST /v1/checkout/orders
{
"intent": "SALE",
"redirect_urls": {
"return_url": "https://example.com/return",
"cancel_url": "https://example.com/cancel"
},
"purchase_units": [
{
"reference_id": "order_1234",
"custom": "my-internal-ref",
"invoice_number": "INV-001",
"amount": {
"currency": "USD",
"total": "107.47",
"details": {
"subtotal": "99.99",
"tax": "5.48",
"shipping": "2.00"
}
},
"items": [
{
"name": "NeoPhone",
"sku": "sku03",
"price": "99.99",
"currency": "USD",
"quantity": "1"
}
],
"shipping_address": {
"recipient_name": "John Doe",
"line1": "2211 N First Street",
"city": "San Jose",
"state": "CA",
"postal_code": "95131",
"country_code": "US"
}
}
]
}v2, create order:
Replace "paypal" with "google_pay", "apple_pay", or "card" as needed. The experience_context object nests the same way for each.
POST /v2/checkout/orders
{
"intent": "CAPTURE",
"payment_source": {
"paypal": {
"experience_context": {
"return_url": "https://example.com/return",
"cancel_url": "https://example.com/cancel"
}
},
// Replace "paypal" with "google_pay", "apple_pay", or "card"
// as needed. experience_context nests the same way for each.
},
"purchase_units": [
{
"reference_id": "order_1234",
"custom_id": "my-internal-ref",
"invoice_id": "INV-001",
"amount": {
"currency_code": "USD",
"value": "107.47",
"breakdown": {
"item_total": {
"currency_code": "USD",
"value": "99.99"
},
"tax_total": {
"currency_code": "USD",
"value": "5.48"
},
"shipping": {
"currency_code": "USD",
"value": "2.00"
}
}
},
"items": [
{
"name": "NeoPhone",
"sku": "sku03",
"unit_amount": {
"currency_code": "USD",
"value": "99.99"
},
"quantity": "1",
"category": "PHYSICAL_GOODS"
}
],
"shipping": {
"name": {
"full_name": "John Doe"
},
"address": {
"address_line_1": "2211 N First Street",
"admin_area_2": "San Jose",
"admin_area_1": "CA",
"postal_code": "95131",
"country_code": "US"
}
}
}
]
}The single v1 pay call splits into two dedicated v2 endpoints, capture or authorize, depending on your order's intent.
| v1 intent | v1 endpoint | v2 endpoint |
|---|---|---|
SALE (now CAPTURE) | POST .../pay | POST /v2/checkout/orders/{id}/capture |
AUTHORIZE | POST .../pay | POST /v2/checkout/orders/{id}/authorize |
SALE to payv1 finalizes a SALE-intent order with a POST .../pay call. v2 replaces it with a dedicated capture endpoint that takes an empty body.
v1:
POST /v1/checkout/orders/{id}/pay
{
"disbursement_mode": "INSTANT"
}v2:
POST /v2/checkout/orders/{id}/capture
{}The capture body is empty, or contains only payment_source for advanced flows. Set the disbursement_mode at order creation time instead.
AUTHORIZE to payv1 finalizes an AUTHORIZE-intent order with the same POST .../pay call. v2 replaces it with a dedicated authorize endpoint, followed by a separate call to capture the authorization.
v1:
POST /v1/checkout/orders/{id}/pay
{
"disbursement_mode": "INSTANT"
}v2:
POST /v2/checkout/orders/{id}/authorize
{}
# Then capture the authorization:
POST /v2/payments/authorizations/{auth_id}/captureFinding the authorization ID. After authorizing, the authorization ID is at purchase_units[].payments.authorizations[0].id. Use it to call POST /v2/payments/authorizations/{auth_id}/capture using the Payments v2 API.
DELETE /v1/checkout/orders/{id} has no v2 equivalent. v2 does not expose a cancel or delete order endpoint. Calling DELETE /v2/checkout/orders/{id} returns 404 Not Found.
Available strategies:
capture or authorize. The order expires without any further API calls.CANCELED status references: This status does not exist in v2. Update any business logic or database state machines that rely on a CANCELED order status.If you include notify_url in a v2 create order request, the API accepts the field but never fires an IPN notification for it.
POST /v1/notifications/webhooks
{
"url": "https://your-server.com/paypal-webhook",
"event_types": [
{ "name": "CHECKOUT.ORDER.APPROVED" },
{ "name": "PAYMENT.CAPTURE.COMPLETED" },
{ "name": "PAYMENT.CAPTURE.PENDING" },
{ "name": "PAYMENT.CAPTURE.DENIED" },
{ "name": "PAYMENT.CAPTURE.REFUNDED" },
{ "name": "PAYMENT.AUTHORIZATION.CREATED" },
{ "name": "PAYMENT.AUTHORIZATION.VOIDED" }
]
}| v1 IPN event | v2 webhook event |
|---|---|
| Order approval | CHECKOUT.ORDER.APPROVED |
| Payment completed (sale) | PAYMENT.CAPTURE.COMPLETED |
| Payment pending | PAYMENT.CAPTURE.PENDING |
| Payment denied | PAYMENT.CAPTURE.DENIED |
| Refund completed | PAYMENT.CAPTURE.REFUNDED |
| Authorization created | PAYMENT.AUTHORIZATION.CREATED |
| Authorization voided | PAYMENT.AUTHORIZATION.VOIDED |
Always verify webhook payloads before processing. Do not reuse IPN validation logic.
POST /v1/notifications/verify-webhook-signature
{
"auth_algo": "SHA256withRSA",
"cert_url": "...",
"transmission_id": "...",
"transmission_sig": "...",
"transmission_time": "...",
"webhook_id": "your-webhook-id",
"webhook_event": { /* full event payload */ }
}Several response fields change shape or value between v1 and v2, starting with how the API returns payer information.
v2 renames the payer_info object to payer and nests the buyer's name under a name object instead of separate first_name and last_name fields.
v1, payer_info:
{
"payer_info": {
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"payer_id": "HPQAUP5WC3J8U"
}
}v2, payer:
{
"payer": {
"email_address": "[email protected]",
"name": {
"given_name": "John",
"surname": "Doe"
},
"payer_id": "HPQAUP5WC3J8U"
}
}The v1 rel: "approval_url" link is replaced in v2, but the exact replacement rel depends on how the order was created:
| Create order request | v2 HATEOAS rel returned | Meaning |
|---|---|---|
Without payment_source | "approve" | Standard redirect. PayPal redirects the buyer to approve. |
With payment_source | "payer-action" | Buyer may need to complete an additional step, such as 3D Secure or wallet confirmation. |
The following responses show the exact rel value returned in each scenario from preceding table.
v1:
{ "rel": "approval_url" }v2, without payment_source:
{ "rel": "approve" }v2, with payment_source:
{ "rel": "payer-action" }Don't hardcode a specific rel value. Because the rel returned by v2 depends on whether payment_source was included at create time, hardcoding a filter for "approval_url", "approve", or "payer-action" alone will break for at least one integration pattern. Navigate HATEOAS links dynamically. Check for whichever rel is present in the response and redirect the buyer to that URL.
| v1 status | v2 status | Notes |
|---|---|---|
CREATED | CREATED | Same |
APPROVED | APPROVED | Same |
COMPLETED | COMPLETED | Same |
FAILED | VOIDED | Terminal failure renamed |
IN_PROGRESS | No equivalent | Not surfaced in v2 |
PARTIALLY_COMPLETED | No equivalent | Not surfaced in v2 |
CANCELED | No equivalent | No cancel endpoint, so no CANCELED state |
| No equivalent | PAYER_ACTION_REQUIRED | New v2 state (3D Secure or additional action needed) |
| Feature | Endpoint or mechanism | Why it matters |
|---|---|---|
| Update order | PATCH /v2/checkout/orders/{id} | Modify amount, items, or shipping post-create, pre-capture. Supports dynamic pricing. |
| Idempotency | PayPal-Request-Id header | Safely retry create, capture, and authorize calls without duplicate charges. |
| Minimal compared to full response | Prefer: return=representation | Returns the full order resource in one call, avoiding a follow-up GET request. |
| Shipment tracking | POST .../track | Attach carrier tracking to captured orders. Improves seller protection eligibility. |
| Order update callback | Callback URL on create | Real-time shipping address and option updates during the buyer checkout flow. |
seller_receivable_breakdown | In capture response | Full fee transparency: gross amount, PayPal fee, net. Replaces the bare transaction_fee field. |
Sandbox setup: Use api-m.sandbox.paypal.com. Create sandbox buyer and seller accounts at developer.paypal.com. Sandbox OAuth credentials do not work in the live environment.
| Scenario | What to verify |
|---|---|
| Create with CAPTURE intent | 201 Created; no intent validation error |
| Create with AUTHORIZE intent | 201 Created; HATEOAS authorize link present |
| Buyer approval via HATEOAS link | Order status moves to APPROVED. Confirm rel: "approve" is returned when there's no payment_source, and rel: "payer-action" is returned when payment_source is present. |
| Capture approved order | 201; purchase_units[].payments.captures[0].id present; status COMPLETED |
| Authorize approved order | Authorization ID present in purchase_units[].payments.authorizations[0].id |
| Amount breakdown mismatch | 422 returned when value does not equal the sum of breakdown |
| Address with v1 field names | Validation error; confirm admin_area_1 and admin_area_2 are accepted |
PATCH order amount | 204 No Content; GET reflects the updated amount |
| Webhook delivery | PAYMENT.CAPTURE.COMPLETED fires; signature verification passes |
notify_url absent | No IPN delivered; confirm webhook is used instead |
disbursement_mode: DELAYED on create | Capture succeeds; funds held per disbursement model. PayPal Complete Payments Platform only. |
custom_id round-trip | Value present in create, GET, and capture response |
HATEOAS rel conditional behavior | Confirm rel: "approve" when there's no payment_source, rel: "payer-action" when payment_source is included, and that redirect logic handles both |
api-m.paypal.com.intent: "SALE" replaced with intent: "CAPTURE".POST .../pay calls replaced with capture or authorize.disbursement_mode set at create time, not capture time. Skip this item if it doesn't apply to your integration. PayPal Complete Payments Platform only.notify_url removed from all create order requests.debug_id from all non-2xx responses for PayPal support escalation./v1/checkout/orders to /v2/checkout/orders.intent: "SALE" with intent: "CAPTURE".redirect_urls.return_url and cancel_url into payment_source.{source}.experience_context where {source} is paypal, google_pay, apple_pay, or card.application_context.brand_name to payment_source.{source}.experience_context.brand_name.application_context.locale to payment_source.{source}.experience_context.locale.application_context.shipping_preference to payment_source.{source}.experience_context.shipping_preference.application_context.user_action to payment_source.{source}.experience_context.user_action. Rename the value "commit" to "PAY_NOW".application_context from create order requests entirely. Use experience_context per payment source instead.supplementary_data[name="risk_correlation_id"] body entry with the PayPal-Client-Metadata-Id request header.supplementary_data entries for buyer_ipaddress and external_channel. No v2 equivalent exists.amount.currency to amount.currency_code.amount.total to amount.value.amount.details to amount.breakdown. Convert all sub-fields to Money objects.price and currency to unit_amount.value and unit_amount.currency_code.tax string to a Money object with currency_code.shipping_address to shipping.address. Rename all address sub-fields to match.recipient_name to shipping.name.full_name.custom to custom_id in all purchase units.POST .../pay with POST .../capture for CAPTURE intent.POST .../pay with POST .../authorize for AUTHORIZE intent.disbursement_mode from the capture body. Set it on create in payment_instruction instead. This requires PayPal Complete Payments Platform enrollment. See Get Started with Multiparty.partner_fee_details with payment_instruction.platform_fees[]. This requires PayPal Complete Payments Platform enrollment. See Get Started with Multiparty.payment_summary to payments and payer_info to payer.rel navigation: change approval_url to approve when there's no payment_source, or to payer-action when payment_source is present. Navigate dynamically, and never hardcode either value.FAILED to VOIDED).notify_url fields. Register and verify webhooks.PayPal-Request-Id header on create, capture, and authorize calls.These are the mistakes developers make most often during this migration. Check your integration against each one.
intent: "SALE" to v2Mistake: Passing "intent": "SALE" unchanged from v1 to the v2 create order endpoint.
Why it happens: The field name is identical; only the value changed.
Resolution: v2 returns a 400 Bad Request validation error. Replace with "CAPTURE". This is the most frequent migration failure in live cutover.
POST .../payMistake: Updating the base URL to v2 but keeping the /pay path.
Why it happens: Developers update the host but not the operation-specific path.
Resolution: The pay endpoint does not exist in v2. Route to /capture or /authorize based on the order's intent. v2 returns 404 for the old path.
disbursement_mode on captureMistake: Sending disbursement_mode in the capture request body as in v1.
Resolution: Set disbursement_mode in purchase_units[].payment_instruction.disbursement_mode on create order or using PATCH before capture. If you send disbursement_mode on the capture call instead, the API either accepts the value without applying it or returns a validation error, depending on the request. The payment_instruction and disbursement_mode objects are available to PayPal Complete Payments Platform accounts only. If PayPal hasn't approved your account, this field is not available regardless of where you send it. See Get Started with Multiparty.
rel value to extract the approval URLMistake: Filtering HATEOAS links for a hardcoded rel string, whether "approval_url" (v1), "approve", or "payer-action".
Why it happens: v1 always returned "approval_url". In v2, the rel is conditional: "approve" is returned when the order is created without a payment_source, and "payer-action" is returned when a payment_source is included. Developers who only test one path miss the other.
Resolution: Navigate HATEOAS links dynamically. If your integration creates orders both with and without a payment_source, your redirect logic needs to handle both "approve" and "payer-action". If your redirect logic filters for only one rel value, it fails to find a match for the other integration pattern, and no error surfaces to indicate why.
DELETE to cancel an orderMistake: Calling DELETE /v2/checkout/orders/{id} expecting a 204.
Resolution: v2 returns 404. Remove or reroute cancel logic. Orders expire naturally; explicit cancellation is not required and not supported.
payment_summary instead of paymentsMistake: Reading capture IDs from purchase_units[].payment_summary.captures[0].id.
Resolution: In v2, captures are at purchase_units[].payments.captures[0].id. Orders v2 renames the payment_summary object to payments. When you pass the wrong path, the API returns undefined without throwing an error, making this easy to miss.
Mistake: Setting amount.value to a number that doesn't equal the sum of breakdown components.
Why it happens: v1's amount.details was informational and not validated server-side. v2 enforces this sum.
Resolution: v2 returns 422 Unprocessable Entity on mismatch. Validate item_total + tax_total + shipping + handling + insurance - shipping_discount - discount = value before calling the API.
notify_urlMistake: Including notify_url in v2 create order requests and waiting for IPN callbacks.
Resolution: If you include notify_url in a v2 request, the API accepts the field but never sends an IPN. Register webhooks and validate that PAYMENT.CAPTURE.COMPLETED events are received before removing v1 IPN listeners from live traffic.
payer_info in v2 responsesMistake: Reading payer details from response.payer_info.first_name in v2.
Resolution: v2 uses payer.email_address, payer.name.given_name, and payer.name.surname. The payer_info object does not exist in v2 responses.
Mistake: Sending shipping_address.city, shipping_address.state, and shipping_address.line1 to v2.
Resolution: v2 address fields are shipping.address.address_line_1, admin_area_2 (city), and admin_area_1 (state or province). If you send v1 field names instead, the API either discards the unrecognized fields without applying them or returns a validation error for any field it requires.