Summary
For a PayPal wallet payment created with setup_future_usage: off_session, the value reaching the connector is downgraded to on_session. The PayPal connector only requests vaulting when it sees OffSession, so payment_source.paypal.attributes is serialised as null, PayPal never creates a payment token, and the payment method cannot be reused for MIT / renewals.
The payment itself succeeds. Only the vaulting is silently skipped.
The intent keeps the requested value; the attempt does not:
| field |
value |
| payment_intent.setup_future_usage |
off_session |
| payment_attempt.setup_future_usage_applied |
on_session |
setup_future_usage_applied is what construct_payment_router_data passes to the connector, so the connector sees on_session.
Expected Behavior
setup_future_usage: off_session on a PayPal wallet payment should reach the connector as OffSession, so that paypal/transformers.rs emits:
"attributes": { "vault": { "store_in_vault": "ON_SUCCESS", "usage_type": "MERCHANT", "permit_multiple_payment_tokens": true } }
and PayPal returns a reusable payment token.
Actual Behavior
attributes is sent as null. Verified two ways.
Captured from a logging proxy placed between the router and api-m.sandbox.paypal.com:
POST /v2/checkout/orders
payment_source.paypal keys: [ 'experience_context', 'attributes' ]
attributes: null
And confirmed independently from PayPal's own API, fetching the resulting order:
GET /v2/checkout/orders/{id}
status: COMPLETED
payment_source.paypal keys: ['account_id', 'account_status', 'address', 'email_address', 'name']
attributes: absent
Subsequent MIT attempts against the stored payment method fail with IR_39 No eligible connector was found for the current payment method configuration. The saved payment method has recurring_enabled: false and no connector_mandate_details.
Steps To Reproduce
- Configure a PayPal connector (BodyKey, sandbox credentials) with wallet / paypal enabled and recurring_enabled: true.
- Create a payment:
POST /payments
{
"amount": 199,
"currency": "USD",
"customer_id": "<customer>",
"confirm": true,
"setup_future_usage": "off_session",
"return_url": "https://example.com/complete",
"payment_method": "wallet",
"payment_method_type": "paypal",
"payment_method_data": { "wallet": { "paypal_redirect": {} } }
}
- Read the response: setup_future_usage comes back as on_session.
- Inspect the outgoing PayPal order: payment_source.paypal.attributes is null.
Splitting create and confirm makes the boundary obvious:
- POST /payments with confirm: false stores setup_future_usage: off_session
- POST /payments/{id}/confirm results in setup_future_usage_applied: on_session
So the downgrade happens during confirm, not create.
What I ruled out
override_setup_future_usage_to_on_session in crates/router/src/core/payments/helpers.rs is the documented downgrade path, gated on the skip_saving_wallet_at_connector_{merchant_id} config. It does not appear to be the cause:
- the config key does not exist (GET /configs/{key} returns HE_02), so it falls back to the "[]" default
- reproduced on a new local database whose configs table holds only routing_default_* rows
- its logger::debug!("Override setup_future_usage ...") line never appears at log.console.level = "DEBUG"
Adding customer_acceptance and mandate_data with multi_use changes nothing.
Context
- version: v1.125.0 (-standalone image), also reproduced on a local v1.125.0 router with a fresh database
- RUN_ENV=sandbox
- connector: PayPal, test_mode: true
- PayPal app has Save payment methods (Vault) and Subscriptions enabled
Card payments through recurring_details.processor_payment_token work correctly for MIT on the same router, so this appears specific to the wallet / PayPal path.
Summary
For a PayPal wallet payment created with setup_future_usage: off_session, the value reaching the connector is downgraded to on_session. The PayPal connector only requests vaulting when it sees OffSession, so payment_source.paypal.attributes is serialised as null, PayPal never creates a payment token, and the payment method cannot be reused for MIT / renewals.
The payment itself succeeds. Only the vaulting is silently skipped.
The intent keeps the requested value; the attempt does not:
setup_future_usage_applied is what construct_payment_router_data passes to the connector, so the connector sees on_session.
Expected Behavior
setup_future_usage: off_session on a PayPal wallet payment should reach the connector as OffSession, so that paypal/transformers.rs emits:
and PayPal returns a reusable payment token.
Actual Behavior
attributes is sent as null. Verified two ways.
Captured from a logging proxy placed between the router and api-m.sandbox.paypal.com:
And confirmed independently from PayPal's own API, fetching the resulting order:
GET /v2/checkout/orders/{id} status: COMPLETED payment_source.paypal keys: ['account_id', 'account_status', 'address', 'email_address', 'name'] attributes: absentSubsequent MIT attempts against the stored payment method fail with IR_39 No eligible connector was found for the current payment method configuration. The saved payment method has recurring_enabled: false and no connector_mandate_details.
Steps To Reproduce
Splitting create and confirm makes the boundary obvious:
So the downgrade happens during confirm, not create.
What I ruled out
override_setup_future_usage_to_on_session in crates/router/src/core/payments/helpers.rs is the documented downgrade path, gated on the skip_saving_wallet_at_connector_{merchant_id} config. It does not appear to be the cause:
Adding customer_acceptance and mandate_data with multi_use changes nothing.
Context
Card payments through recurring_details.processor_payment_token work correctly for MIT on the same router, so this appears specific to the wallet / PayPal path.