4

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?

First Attempt

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.

Interpreting the Error

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:

  1. When I change my app authentication token, I get a different error:

    {
      "code": "AuthenticationFailed",
      "type": "INPUT",
      "message": null,
      "detail": null,
      "moreInfo": null
    }
    
  2. 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.

Contacting Developer Relations

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.

Another Attempt

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.

Community
  • 1
  • 1
showdev
  • 28,454
  • 37
  • 55
  • 73
  • The public key used in the encryption might be specific to your account. You could get its contents remotely or just copy it over to your environment. I don't see how that defeats the purpose. One other comment, don't assume that when you see a good looking value that you encrypted the data you expected. You could encrypt whitespace and get something that looks like that value. Just how it works. – ficuscr Jun 05 '19 at 18:00
  • I assume a card token references actual card data previously sent to Inuit. Is that not the case? If I generate a token manually, I'm not sure how that will reference the card data a user enters. Maybe I'm misunderstanding? – showdev Jun 05 '19 at 18:02
  • I looked at this and assumed end to end encryption, to get the card data relayed to Intuit. They then store that and return a "token" to you. You then reference that stored card data via that token when needing to charge in the future. – ficuscr Jun 05 '19 at 18:05
  • I think that's what I assume as well, except I'm not encrypting anything myself. I send credit card information to Inuit, they send me back a token, and I submit charges to the card using that token. The problem is that the token I get back from Inuit seems to be invalid. – showdev Jun 05 '19 at 18:08
  • Yeah. I've reread things. Agreed. When that token is returned to you, is there anything else useful in that response? Assuming their stuff actually does work... it could be: a race condition? Wait a minute before referencing the newly retuned token. Or, are you sending the card data to say *staging*, then trying to read it from *dev*. – ficuscr Jun 05 '19 at 18:11
  • Nothing much useful in the response as far as I can tell. I've edited my question to include the full response. I tried waiting 5 seconds before submitting my form, but the response is the same. The token is apparently only valid for 15 minutes and waiting too long would disrupt the user experience. I'm generating the token from a sandbox environment and submitting the charge to a sandbox endpoint, so I'm not aware of any staging/dev inconsistency. – showdev Jun 05 '19 at 18:20
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/194509/discussion-between-ficuscr-and-showdev). – ficuscr Jun 05 '19 at 18:22
  • 1
    That thing that "appears" to be a token sure looks like a UUID... Are you pulling that out of a JSON object that looks like `{value: '...'}`? If not then I think there is a problem. Their example tokens in the docs do not look like UUIDs. Sadly the attribute is simply described as a *string* in their docs. They don't even specify the max length! – ficuscr Jun 10 '19 at 22:18
  • @ficuscr The response is formatted like `{"value":"c4b3f522-6074-4f22-9052-48db1a67fcc1"}`. The sample value shown in their [API Explorer](https://developer.intuit.com/app/developer/qbpayments/docs/api/resources/all-entities/tokens) is `bFy3h7W3D2tmOfYxl2msnLbUirY=`. But when I log in and use the "Try It" button, I get a value like this: `322a1547-c4d6-42dc-aedc-1c4a44ac4370`. – showdev Jun 10 '19 at 22:51
  • Nope. But, that sure looks to me like a token is being supplied in response. Wasn't confident you were getting that far successfully. Thought you had authentication issues. These Intuit forums and docs make me want to gouge out my eyes... Here is another one. What is the [requestId](https://help.developer.intuit.com/s/article/What-is-RequestId-and-its-usage)? Was looking at this [post](https://help.developer.intuit.com/s/question/0D5G000004Dk6eaKAB/tokenization-doesnt-work). – ficuscr Jun 10 '19 at 22:51
  • 1
    @ficuscr I think that was it. For testing, I had been sending the same `RequestID` with every request. I now see that: "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**." Sending a different `RequestID` seemed to work. I'm eager to give you the bounty if you want to post an answer to that effect! – showdev Jun 10 '19 at 23:00
  • An example of implementation with Node: https://github.com/IntuitDeveloper/SampleApp-Payments-Nodejs – ferminx360 Jun 17 '19 at 15:51
  • API to try: https://developer.intuit.com/app/developer/qbpayments/docs/api/resources/all-entities/bankaccounts – ferminx360 Jun 17 '19 at 16:14

2 Answers2

1

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.

showdev
  • 28,454
  • 37
  • 55
  • 73
0

I have been testing with Postman (I understand that JS is not the problem), but the API. Maybe this helps.

Card tokenize:

  • Endpoint: https://sandbox.api.intuit.com/quickbooks/v4/payments/tokens
  • Header: Content-Type: application/json
  • Body (raw) JSON (application/json):

    { "card": { "name": "emulate=0", "number": "4111111111111111", "expMonth": "02", "address": { "postalCode": "94086", "country": "US", "region": "CA", "streetAddress": "Road Street", "city": "Sunnyvale" }, "expYear": "2020", "cvc": "123" } }

Response:

{
 "value": "7e92f015-820b-4e70-81b9-8ce840c76389"
}

Charge:

  • Endpoint: https://sandbox.api.intuit.com/quickbooks/v4/payments/charges
  • Header: Content-Type: application/json
  • request-id : RANDOM NUMBER
  • Authorization: OAuth 2.0 Access token (59 min)
  • Body (raw) JSON (application/json):

    { "currency": "USD", "amount": "10.55", "context": { "mobile": "false", "isEcommerce": "true" }, "token": "7e92f015-820b-4e70-81b9-8ce840c76389" }

Response:

{
"created": "2019-06-17T16:18:43Z",
"status": "CAPTURED",
"amount": "10.55",
"currency": "USD",
"token": "7e92f015-820b-4e70-81b9-8ce840c76389",
"card": {
    "number": "xxxxxxxxxxxx1111",
    "name": "emulate=0",
    "address": {
        "city": "Sunnyvale",
        "region": "CA",
        "country": "US",
        "streetAddress": "Road Street",
        "postalCode": "94086"
    },
    "cardType": "Visa",
    "expMonth": "02",
    "expYear": "2020",
    "cvc": "xxx"
},
"avsStreet": "Pass",
"avsZip": "Pass",
"cardSecurityCodeMatch": "NotAvailable",
"id": "E4JTLAV2QAFF",
"context": {
    "mobile": false,
    "deviceInfo": {},
    "recurring": false,
    "isEcommerce": true
},
"authCode": "546816"
}
ferminx360
  • 95
  • 8