- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
I'm trying to save a user's PayPal for off-session transactions. I'm following the docs here (https://developer.paypal.com/docs/checkout/save-payment-methods/purchase-later/js-sdk/paypal/).
I'm using the docs as closely as possible, though I have to modify the client code to work in node.
I'm getting the token ID from my server, but that doesn't seem to be the format or kind of input required.
I'm stuck on step 6. I'm getting smart_api_vault_ectoken_contingency_error
buttonCorrelationID
:
"f10601934aa66"
buttonSessionID
:
"uid_f9dd725eba_mjm6mtk6mjy"
clientID
:
"AdvBzA3XST3GAEp5MdTk5PqRDjVXZYON0jNyaQfs7P90BQcQqMiIS9HMEjRe3qrehxVArMvE1LnaHVqH"
env
:
"sandbox"
referer
:
"www.sandbox.paypal.com"
sdkCorrelationID
:
"0819775552895"
sessionID
:
"uid_8c172cf2fe_mjm6mdy6mzu"
timestamp
:
"1701991169324"
token
:
null
Here is my next.js component:
import React, { useEffect, useState } from 'react';
const PayPalButton = () => {
const [scriptLoaded, setScriptLoaded] = useState(false);
useEffect(() => {
// Function to fetch token and load PayPal script
const fetchTokenAndLoadScript = async () => {
try {
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization:
'Basic ' +
Buffer.from(
process.env.NEXT_PUBLIC_CLIENT_ID +
':' +
process.env.NEXT_PUBLIC_CLIENT_SECRET
).toString('base64'),
};
const body = new URLSearchParams();
body.append('grant_type', 'client_credentials');
body.append('response_type', 'id_token');
// Replace with your token fetch request
const tokenResponse = await fetch(
'https://api-m.sandbox.paypal.com/v1/oauth2/token',
{
method: 'POST',
headers: headers,
body: body,
}
);
const tokenData = await tokenResponse.json();
const userToken = tokenData.id_token; // Adjust according to the actual response structure
console.log('User token:', userToken);
// Construct the script URL
const srcUrl = `https://www.paypal.com/sdk/js?client-id=${process.env.NEXT_PUBLIC_CLIENT_ID}&merchant-id=${process.env.NEXT_PUBLIC_MERCHANT_ID}`;
console.log('Script URL:', srcUrl);
// Check if the script is already present
if (!document.querySelector(`script[src="${srcUrl}"]`)) {
console.log('Script already loaded once...');
const script = document.createElement('script');
script.src=srcUrl;
script.setAttribute('data-user-id-token', userToken); // Add the 'data-user-id-token' property
script.addEventListener('load', () => {
setScriptLoaded(true);
});
document.body.appendChild(script);
}
} catch (error) {
console.error(
'Error fetching token or loading PayPal script:',
error
);
}
};
fetchTokenAndLoadScript();
}, []);
useEffect(() => {
if (scriptLoaded) {
console.log("The script's loaded, let's render the button!");
window.paypal
.Buttons({
createVaultSetupToken: async () => {
// Call your server API to generate a setup token
// and return it here as a string
const response = await fetch(
'http://localhost:3000/api/v1/paypal/create-setup-token',
{
method: 'POST',
credentials: 'include',
}
);
const result = await response.json();
console.log('Made it here.');
console.log(`Token here be: ${result.id}`);
return result.id;
},
onApprove: async ({ vaultSetupToken }) => {
return fetch(
'http://localhost:3000/api/v1/paypal/create-payment-token',
{
body: JSON.stringify({ vaultSetupToken }),
}
);
},
onError: (error) => {
console.log('An error occurred: ', error);
},
})
.render('#paypal-buttons-container');
}
}, [scriptLoaded]);
return <div id="paypal-buttons-container"></div>;
};
export default PayPalButton;
And here is my backend in express.js:
import 'dotenv/config';
import express from 'express';
const { PORT = 3011, ACCESS_TOKEN } = process.env;
const app = express();
app.set('view engine', 'ejs');
app.use(express.static('public'));
app.post('/api/v1/paypal/create-user-token', async (req, res) => {
console.log('Creating user token...');
res.status(200).json({ message: 'User token created' });
});
// Create setup token
app.post('/api/v1/paypal/create-setup-token', async (req, res) => {
console.log(ACCESS_TOKEN);
console.log('Creating setup token...');
try {
// Use your access token to securely generate a setup token
// with an empty payment_source
const vaultResponse = await fetch(
'https://api-m.sandbox.paypal.com/v3/vault/setup-tokens',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${ACCESS_TOKEN}`,
'PayPal-Request-Id': Date.now(),
},
body: JSON.stringify({
payment_source: {
paypal: {},
},
}),
}
);
const result = await vaultResponse.json();
res.json(result);
} catch (err) {
console.log('Error creating setup token:', err);
res.status(500).send(err.message);
}
});
// Create payment token from a setup token
app.post('/api/v1/paypal/create-payment-token/', async (req, res) => {
try {
const paymentTokenResult = await fetch(
'https://api-m.sandbox.paypal.com/v3/vault/payment-tokens',
{
method: 'POST',
body: {
payment_source: {
token: {
id: req.body.vaultSetupToken,
type: 'SETUP_TOKEN',
},
},
},
headers: {
Authorization: 'Bearer ${ACCESS-TOKEN}',
'PayPal-Request-Id': Date.now(),
},
}
);
const paymentMethodToken = paymentTokenResult.id;
const customerId = paymentTokenResult.customer.id;
await save(paymentMethodToken, customerId);
res.json(captureData);
} catch (err) {
res.status(500).send(err.message);
}
});
const save = async function (paymentMethodToken, customerId) {
// Specify where to save the payment method token
};
app.listen(PORT, () => {
console.log(`Server listening at http://localhost:${PORT}/`);
});
Solved! Go to Solution.
Accepted Solutions
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
I got it figured out. The JSON body needs to specify the following (this is not properly documented in the documentation link I shared. It says to use the empty paypal:{} object:
paypal: {
usage_type: "MERCHANT",
experience_context: {
vault_instruction: "ON_PAYER_APPROVAL",
return_url: "https://example.com/return",
cancel_url: "https://example.com/cancel"
}
}

- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Good day @radspirit88
Thank you for posting to the PayPal community.
I would suggest to please contact your website developer to cross check your using same LIVE/SANDBOX REST API Credentials(Client ID & Secret) while performing the below API calls.
https://developer.paypal.com/api/rest/authentication/
https://developer.paypal.com/api/rest/postman/
For additional details on saving payment methods, please refer to the link below:
https://developer.paypal.com/docs/checkout/save-payment-methods/
https://developer.paypal.com/docs/checkout/save-payment-methods/purchase-later/js-sdk/paypal/
To create a setup token for your card, please follow the steps provided in the link below:
Please verify eligibility criteria by referring to the following link - https://developer.paypal.com/docs/checkout/save-payment-methods/purchase-later/js-sdk/paypal/#link-c...
I would like to suggest that you try again following the steps outlined above.
If you are still experiencing issues, please create an MTS ticket via the following URL - https://www.paypal-support.com/s/?language=en_US . Please ensure that you provide detailed information and error details when submitting the ticket.
Sincerely,
Kavya
PayPal MTS
If this post or any other was helpful, please enrich the community by giving kudos or accepting it as a solution.
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
Thank you for the reply @Kavyar
I followed all your steps.
I ensured that we are using the same client information to create the access token as we are in the app.
The app and the account are both eligible and properly set up to save payments.
I am able to properly save PayPal when I use the API with CURL directly through my terminal. But it won't work in my application.
I see this in the console when I load my page: GET https://b.sbox.stats.paypal.com/v2/counter.cgi?p=uid_4074ae3ab0_mtc6mzy6nty&s=SMART_PAYMENT_BUTTONS net::ERR_NAME_NOT_RESOLVED
And then when I click the button, I get a window that pops up and immediately closes. Then I see the contingency error in the console.
On my server, the set up token is properly created:
Setup token created: {
id: '9BL40893PE668182L',
customer: { id: 'JLTwBbEZFr' },
status: 'CREATED',
payment_source: { paypal: {} },
links: [
{
href: 'https://api.sandbox.paypal.com/v3/vault/setup-tokens/9BL40893PE668182L',
rel: 'self',
method: 'GET',
encType: 'application/json'
}
]
}
And that ID is making it to the vaultResponse, but the console is showing a 400 status when trying to hit this URL: POST https://www.sandbox.paypal.com/smart/api/vault/9BL40893PE668182L/ectoken 400 (Bad Request)
As you can see, it is using the right token ID in that URL.
I do see that the scope returned for my access token shows vault for credit cards but not paypal? I don't know if it's the same thing:
"https://uri.paypal.com/services/checkout/one-click-with-merchant-issued-token https://uri.paypal.com/services/invoicing https://uri.paypal.com/services/vault/payment-tokens/read https://uri.paypal.com/services/disputes/read-buyer https://uri.paypal.com/services/payments/realtimepayment https://uri.paypal.com/services/disputes/update-seller https://uri.paypal.com/services/payments/payment/authcapture openid https://uri.paypal.com/services/disputes/read-seller Braintree:Vault https://uri.paypal.com/services/payments/refund https://api.paypal.com/v1/vault/credit-card https://uri.paypal.com/services/pricing/quote-exchange-rates/read https://uri.paypal.com/services/billing-agreements https://api.paypal.com/v1/payments/.* https://uri.paypal.com/payments/payouts https://uri.paypal.com/services/vault/payment-tokens/readwrite https://api.paypal.com/v1/vault/credit-card/.* https://uri.paypal.com/services/shipping/trackers/readwrite https://uri.paypal.com/services/subscriptions https://uri.paypal.com/services/applications/webhooks"
- Mark as New
- Bookmark
- Subscribe
- Mute
- Subscribe to RSS Feed
- Permalink
- Report Inappropriate Content
I got it figured out. The JSON body needs to specify the following (this is not properly documented in the documentation link I shared. It says to use the empty paypal:{} object:
paypal: {
usage_type: "MERCHANT",
experience_context: {
vault_instruction: "ON_PAYER_APPROVAL",
return_url: "https://example.com/return",
cancel_url: "https://example.com/cancel"
}
}

Haven't Found your Answer?
It happens. Hit the "Login to Ask the community" button to create a question for the PayPal community.
- No subscription details shown to buyer in their PayPal account in REST APIs
- PayPal Express checkout button not working in my shopify in PayPal Payments Standard
- Error: "Unable to change shipping method. Please try again." in PayPal popup in REST APIs
- Need Help Upgrading to PayPal complete payments in PayPal Upgrade Community
- Subscription purchases w/sandbox do not work due to setup fee issues. Javascript SDK + REST APIs in Sandbox Environment