I'm integrating the Intuit/Quickbooks Payments API into an existing eCommerce website. Due to PCI requirements, I need to tokenize card data via JavaScript before it reaches the server and then submit the charge with that token instead of actual card data.
Why am I'm getting a "token is invalid" error from the Payments API?
I followed the instructions on this page, which describe how to use a JavaScript file provided by Inuit to tokenize card data.
<script src="https://js.appcenter.intuit.com/Content/IA/intuit.ipp.payments.sandbox-0.0.3.js"></script>
intuit.ipp.payments.tokenize(
qbAppToken, {
card: {
number: $("#tokenize_cc-number").val(),
expMonth: $("#tokenize_cc-expmonth").val(),
expYear: $("#tokenize_cc-expyear").val(),
cvc: $("#tokenize_cc-cvc").val(),
address: {
streetAddress: $("#tokenize_cc-address-street").val(),
city: $("#tokenize_cc-address-city").val(),
region: $("#tokenize_cc-address-region").val(),
country: $("#tokenize_cc-address-country").val(),
postalCode: $("#tokenize_cc-address-postalcode").val()
}
}
},
function(token, response) {
console.log(response);
if (token != null) {
console.log(token);
$cardToken.val(token);
$paymentForm[0].submit();
} else {
console.log("Error during tokenization " + response.code + "; " + response.message + "; " + response.detail + "; " + response.moreinfo);
}
});
I get back what appears to be a card token:
f9e7a378-c3f2-4343-b0a8-ee376d4ed472
I insert that token into my form and submit the form to my server, which then submits a charge to the Payments API via CURL using the card token.
I'm submitting to the endpoint:
https://sandbox.api.intuit.com/quickbooks/v4/payments/charges
Array
(
[amount] => 6992.83
[currency] => USD
[capture] => true
[token] => f9e7a378-c3f2-4343-b0a8-ee376d4ed472
[context] => Array
(
[mobile] => false
[isEcommerce] => true
)
)
However, the response I get back from the Payments API says "token is invald":
{
"errors": [{
"code": "PMT-4000",
"type": "invalid_request",
"message": "token is invalid.",
"detail": "token",
"infoLink": "https://developer.intuit.com/v2/docs?redirectID=PayErrors"
}]
}
Here's the full response:
HTTP/1.1 400 Bad Request
Server: nginx
Date: Wed, 05 Jun 2019 18:13:20 GMT
Content-Type: application/json;charset=utf-8
Content-Length: 175
Connection: keep-alive
Keep-Alive: timeout=5
Strict-Transport-Security: max-age=15552000
intuit_tid: [redacted]
Set-Cookie: ADRUM_BT=R:0|clientRequestGUID:9ae895d4-44ee-4175-bb47-4e37e95162a819|btId:755|backendSnapshotType:f; Expires=Wed, 5-Jun-2019 18:13:49 GMT; Path=/
{"errors":[{"code":"PMT-4000","type":"invalid_request","message":"token is invalid.","detail":"token","infoLink":"https://developer.intuit.com/v2/docs?redirectID=PayErrors"}]}
I notice that instructions for JavaScript card tokenization say "This section applies only to OAuth 1.0 apps." This might be a problem. But I see no mention of how to tokenize card data for OAuth 2.0 apps.
I assume that the "token is invalid" error is referring to my card token and not my app authentication token. I'm basing this assumption on two things:
When I change my app authentication token, I get a different error:
{
"code": "AuthenticationFailed",
"type": "INPUT",
"message": null,
"detail": null,
"moreInfo": null
}
Intuit developer relations said that the token endpoint in Intuit's JavaScript file is "incorrect", which indicates that there's a problem with the card token I'm receiving.
Intuit developer relations said:
The reason you get that error is because that javascript file didn't create the token correctly in the correct environment.
QuickBooks Payments API has two different environments. One is called sandbox environment, the other is called production environment. For creating a token for sandbox environment, you will need to use this URL: https://sandbox.api.intuit.com/quickbooks/v4/payments/tokens
However, in that javascript, the URL for token is: https://transaction-api-e2e.payments.intuit.net/v2/tokens, which is incorrect. It is an internal testing environment we used. The token created in the e2e would not work for sandbox. That is why you get the token is invalid error.
After reviewing the API explorer and the tokens endpoint, I tried generating a card token without Intuit's JavaScript library.
This uses the same endpoint as the one in the API Explorer referenced by developer relations, although sandbox.api.intuit.com/v4/payments/tokens doesn't exist so I assume that's a typo.
POST v4/payments/tokens
FOR IE8/IE9 - POST /quickbooks/v4/payments/tokens/ie
Content type: application/json
Production Base URL: https://api.intuit.com
Sandbox Base URL: https://sandbox.api.intuit.com
jQuery.ajax({
url: "https://sandbox.api.intuit.com/quickbooks/v4/payments/tokens",
type: "POST",
contentType: 'application/json',
dataType: "json",
data: JSON.stringify(cardData)
}).done(function(msg) {
...
});
Results are the same.
I get what appears to be a card token, but when I submit the charge via CURL I still get:
{
"errors": [{
"code": "PMT-4000",
"type": "invalid_request",
"message": "token is invalid.",
"detail": "token",
"infoLink": "https://developer.intuit.com/v2/docs?redirectID=PayErrors"
}]
}
What's going wrong?
I have an open ticket with Intuit in addition to a post in the developer community forums. I'll update this post if I receive any further information from them.
If you are following instructions here for tokenizing credit card informtion using the javascript file at https://js.appcenter.intuit.com/Content/IA/intuit.ipp.payments-0.0.3.js
, note the response from developer relations:
...in that javascript, the URL for token is: https://transaction-api-e2e.payments.intuit.net/v2/tokens, which is incorrect. It is an internal testing environment we used. The token created in the e2e would not work for sandbox. That is why you get the token is invalid error.
For creating a token for sandbox environment, you will need to use this URL: https://sandbox.api.intuit.com/quickbooks/v4/payments/tokens
I had success requesting a card token from that Tokens endpoint via AJAX.
In addition, when using a card token to submit a charge, be sure to send a unique RequestID
in the headers.
If the service receives another request with the same RequestID, instead of performing the operation again or returning an error, the service sends the same response as it did for the original request. (What is RequestId and its usage)
That's why I was still getting an "invalid token" error even after switching to the correct endpoint.