In today’s blog post we will be looking at how Pinfire Labs utilizes our Payment Method Delivery functionality to be able to offer PCI-compliant card-not-present online sales as part of their RecHub product, which helps outdoor recreation organizations such as sailing clubs be able to offer online class signups, recurring invoicing and online reservations. This post is guest authored by Justin Cherniak, founder and CEO of Pinfire Labs.
Environment Setup / Tools Needed
To begin, you’ll need a Spreedly account. Since we are not working with the Spreedly Test payment method, you will need a production-level account to proceed. Sign up for one here.
Next, you’ll need to create a Clover developer account. You can do that here.
Once you’ve set up your Clover account, you’ll either need to create a Clover web app or a Clover “semi-integration”. In most cases you would want to create a web app, but if you plan on accepting card-present transactions with your Clover, you’ll want to create a semi-integration. We originally wrote a semi-integration, however these instructions apply to either type of Clover integration.
To begin, we’ll go and create a web app. You can follow the instructions here to do so. When setting up your app, make sure you have at least the following permissions:
Clover uses OAuth2 for linking with merchants, so the next step we’ll need to do is acquire an access token to link with Clover. They speak more about that here. It is a standard OAuth2 flow, so you can utilize the library of your choice to automate much of this work for you.
Processing Clover Payments with Spreedly
Once we have a merchant id and access token from Clover, we are finally ready to start collecting payments!
Clover provides their “Developer Pay API” for accepting card-not-present payments. While they provide a way to encrypt data before sending it to them (reducing PCI scope), unfortunately their methods are not compliant with PCI DSS v3, meaning that if you link with them directly, you’ll be in SAQ-A at best and SAQ-D at worst, depending how it’s done. Since we use Spreedly, we are able to stay within SAQ A-EP scope, saving us many thousands per year in compliance costs.
Additionally, Clover does not have a method to store cards without making a transaction first, limiting use with recurring payments. We process recurring memberships for our merchants, so it’s essential that we can keep cards on file.
To run payments through Clover with Spreedly, we are going to use the Payment Method Distribution API. We use Spreedly’s “Receiver Functions” so that they can encrypt the card number and send it directly to Clover, completely removing us from PCI scope, yet still giving us the full power of stored payments.
To begin, we need a Spreedly card token! The quickest and easiest way to get one is to use Spreedly Express. Once we have our card token, we’ll create an Order in Clover, then pay for it using PMD.
As per the Developer Pay docs, we need a Clover Order id to create a payment. "An order Id of an existing order from the Clover API must be included in the payload sent to this endpoint."
To create a Clover order, we at a minimum need to POST to the Create Order api with an amount to charge. To create a basic order for $100, we would do the following:
POST https://apisandbox.dev.clover.com/v3/merchants/{MERCHANT_ID_FROM_OAUTH}/orders
Authorization: Bearer ACCESS_TOKEN_YOU_GOT_FROM_OAUTH
Content-Type: application/json
{
“total”: 10000
}
This will return an object similar to the following (and a status code of 200):
{
"href": "https://sandbox.dev.clover.com/v3/merchants/XXXXXXXXXXXX/orders/VYWYV0V46XFHP",
"id": "VYWYV0V46XFHP",
"currency": "USD",
"employee": {
"id": "FMKQRSSMV981J"
},
"taxRemoved": false,
"isVat": false,
"manualTransaction": false,
"groupLineItems": true,
"testMode": false,
"createdTime": 1538192078000,
"clientCreatedTime": 1538192078000,
"modifiedTime": 1538192078000
}
Depending on the details your merchants would like on their receipts, you can also add line items to your order. We skipped this in our integration (since we produce our own invoices), but if you would like to add that level of detail, you can find directions on doing so here.
Once we’ve created our Clover order, we need to obtain the encryption parameters to use for the Developer Pay API. To do so, we make a call to /v2/merchants/{MERCHANT_ID}/pay/key. Note the v2 in the URL here. While Clover has most of their REST API under v3, the Developer API is under v2. Make sure to note this as the Developer Pay docs are not included in the main REST API docs.
GET https://apisandbox.dev.clover.com/v2/merchant/XXXXXXXXXX/pay/key
Returns (truncated for brevity):
{
"modulus": "2413028797502124...",
"exponent": "415029",
"prefix": "00008099",
"id": "66257982250",
"pem": "-----BEGIN CERTIFICATE-----\nMIICpTCCAY0CBQFOr...-----END CERTIFICATE-----\n"
}
With our Clover order id and our encryption credentials, we’re ready to send the payment request.
The examples below are written in PHP, since that is what we used in our integration. The biggest issue we ran into with using PMD is that there were JSON double encoding issues between Spreedly and Clover. You might not have the same issues if working in another language, but I’ve provided our original PHP code here to demonstrate how we worked around them.
We start by building out our basic request:
$cloverRequest = [
'orderId' => $cloverOrderId,
'expMonth' => '{{ credit_card_month }}',
'amount' => AMOUNT_TO_CHARGE, // Needs to equal the “total” you passed to Create Order
'currency' => 'usd',
'expYear' => '{{ credit_card_year }}',
'first6' => '{{#truncate}}6,{{ credit_card_number }}{{/truncate}}',
'cardEncrypted' => 'ENCRYPTEDCARD',
'last4' => '{{#last}}4,{{ credit_card_number }}{{/last}}'
// 'cvv' => '{{ credit_card_verification_value }}',
// 'zip' => '{{ credit_card_zip }}',
];
A few notes:
- Since we don’t have access to the credit card number or expiration date, we use Receiver Variables. Once sent to Spreedly, they are replaced with the actual values before sending off to Clover. Notice the use of the truncate and last receiver functions for giving Clover the first 6 and last 4 digits of the card.
- In our testing, we found that Clover would routinely reject payments if we passed in the CVV value or the zip code. We aren’t sure if this is a universal issue or just one we ran into, but since our merchants all have very low risk profiles, we skipped passing them. If you have merchants with any significant amount of chargebacks, it is more important to fill in these values.
- As I mentioned above, we ran into a lot of problems with JSON escaping of slashes, so in the base object, we don’t include the token for the credit card number, but a placeholder we can replace later on.
With those out of the way, we build the JSON string that will be sent to Clover. Please note that it will be double-encoded before you send it to Spreedly, as they will decode the overall PMD request, then send the “body” onto Clover.
The important key here is that you need to encode your JSON without escaping slashes. If you escape them, you’ll run into issues, since it seems that Clover doesn’t handle escaped slashes in their calls.
// There are all sorts of issues since this gets double encoded // (it's encoded a second time before it is spent to Spreedly).
// This took hours to figure out, but in the end, we need to
// avoid encoding slashes here and handle the PEM below.
$encodedCloverRequest = json_encode($cloverRequest, JSON_UNESCAPED_SLASHES);
With our base JSON built, we now need to tell Spreedly to encrypt the credit card number with RSA and then base64-encode it. To do that, we use the base64 and rsa receiver functions. Again, we insert this to the JSON string AFTER we encode it normally since it isn’t encoded properly otherwise.
// JSON encoding breaks the insertion of the PEM apparently,
// so we need to replace it after we encode, as per
$encodedCloverRequest = str_replace('ENCRYPTEDCARD', "{{#base64}}{{#rsa}}{$rsaCert->pem},oaep,{$rsaCert->prefix}{{ credit_card_number }}{{/rsa}}{{/base64}}", $encodedCloverRequest);
Note the use of $rsaCert variable. That is just the JSON-decoded result of the call to /pay/key we made above.
Finally, we build out our final request to send to the PMD endpoint
// Spreedly payment-method-distribution request
$deliveryRequest = [
'delivery' => [
'payment_method_token' => $spreedly_card_token,
'url' => ‘https://apisandbox.dev.clover.com/v2/merchant/' . $clover->merchantId . '/pay',
'headers' => "Content-Type: application/json\r\nAuthorization: Bearer {{api_token}}",
'body' => $encodedCloverRequest
]
];
$receiverToken = ‘PMD Token you created earlier during setup’;
Finally, we make our request to Spreedly’s Deliver endpoint. We’ve included a full example of the request here so you can ensure you have the encoding correct. Notice the double encoding on the “body” of the request.
POST https://core.spreedly.com/v1/receivers/{RECEIVER_TOKEN}/deliver.json
Content-Type: application/json
Authorization: …
{
"delivery": {
"payment_method_token": "XXXXXXXXXXXXXXXXXXXX",
"url": "https:\/\/apisandbox.dev.clover.com\/v2\/merchant\/XXXXXXXXXXX\/pay",
"headers": "Content-Type: application\/json\r\nAuthorization: Bearer {{api_token}}",
"body": "{\"orderId\":\"H4C8XN40VY28E\",\"expMonth\":\"{{ credit_card_month }}\",\"amount\":\"10000\",\"currency\":\"usd\",\"expYear\":\"{{ credit_card_year }}\",\"first6\":\"{{#truncate}}6,{{ credit_card_number }}{{\/truncate}}\",\"cardEncrypted\":\"{{#base64}}{{#rsa}}-----BEGIN CERTIFICATE-----\nMIICpTCCAY0CBQFOrYqoMA0GCSqGSIb3DQEBBQUAMBExDzANBgNVBAMTBlRBQ0FU\nMTAeFw0xMzEwMTcyMDM0MzhaFw0yMzEwMTUyMDM0MzhaMBwxGjAYBgNVBAMTEVRB\nQ0FUMTY2MjU3OTgyMjUwMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA\nvyYRQA3cS4wV9yk+6bFzA7KLDmE+D\/SOP+Q5bNOPG9nUDkAPalRBz12KA5SDxTw2\nvO1BIeSFUQlYTpzEDb\/XkfNNm5e6nqf12M4kdHP1F2EXW4WArilUZegAVw\/Y7FvA\nkA8PQFbfgmBirSa5GS\/fuAHjemqEf0DxIgq552IDeFw3nB0vccK6ePue5sVB9Sm2\nvWpKm\/lj2UE4P6z2ngZr5V31cSAVN08USxHvz+MEnoUBKt6aKvfRUAp4iFyIpxlp\n4eylxY8zizPekS29lcRMsI9hGug2CoKFhhUJ1gD8G280zIoWCxysNvl40k\/l8OTt\nPKrnlhAzQcIyy\/RB0lwb6QIDBlU1MA0GCSqGSIb3DQEBBQUAA4IBAQBK28H6\/gdW\ncYmpy49DljgvnN7rYF0cQP5RIrtB7zopudHAi3OswNRHzUjCY\/6HaNOAPJZN1SuP\nj\/zrvIG30WaJuI5669wkRvpZ3ICtYbZhtLe9Gpj8iE5MwfSqJTWYFzdQGYYKNd0z\ngCNIJKjeULCNPEy1pfwSVatkrkusqmMbr8eMJn4Z\/ODe+YCwO5Du8gvqVRV1idcW\ntvRr8wS43tZE3AGRbZxnkTejorCT+yFbZCj7sVUZonlhEdz5IKMr0t8wIfcy+7Jz\nONcrsegDMjlj7UDu4N8A7S9Ls24m08G+Hkhs\/3kqAXem0++bq3CFf0ceGJDCGTeU\nNwp0D4nVh\/PF\n-----END CERTIFICATE-----\n,oaep,00008099{{ credit_card_number }}{{\/rsa}}{{\/base64}}\",\"last4\":\"{{#last}}4,{{ credit_card_number }}{{\/last}}\"}"
}
}
If the payment is successful, we will get a response like the following:
{
"token": "ZRh2xs8TaJUvLe0yTAVzSiXRki0",
"transaction_type": "DeliverPaymentMethod",
"state": "succeeded",
"created_at": "2018-09-29T04:08:39Z",
"updated_at": "2018-09-29T04:08:41Z",
"succeeded": true,
"message": "Succeeded!",
"url": "https:\/\/apisandbox.dev.clover.com\/v2\/merchant\/XXXXXXXXXXX\/pay",
"response": {
"status": 200,
"headers": "cache-control: no-cache, no-store, must-revalidate\r\npragma: no-cache\r\nexpires: Tue, 17 Sep 1991 10:00:00 PST\r\ncontent-length: 193\r\ncontent-type: application\/json; charset=utf-8\r\nX-Frame-Options: SAMEORIGIN\r\nX-Robots-Tag: none",
"body": "{\"paymentId\":\"6SD5W6AVQ0D5G\",\"result\":\"APPROVED\",\"authCode\":\"OK8629\",\"token\":\"HDAX90CY5WVF2\",\"vaultedCard\":{\"first6\":\"411111\",\"last4\":\"1111\",\"expirationDate\":\"1223\",\"token\":\"1894469479681111\"}}"
},
"receiver": {
"company_name": "TEST",
"receiver_type": "test",
"token": "XXXXXXXXXXXXXXXXXXX",
"hostnames": "https:\/\/apisandbox.dev.clover.com",
"state": "retained",
"created_at": "2018-09-29T04:05:51Z",
"updated_at": "2018-09-29T04:05:51Z",
"credentials": [
{
"name": "api_token",
"safe": "false"
}
]
},
"payment_method": {
"token": "XXXXXXXXXXXXXXXX",
"created_at": "2018-09-29T04:05:45Z",
"updated_at": "2018-09-29T04:08:41Z",
"email": null,
"data": null,
"storage_state": "retained",
"test": true,
"last_four_digits": "1111",
"first_six_digits": "411111",
"card_type": "visa",
"first_name": "Justin",
"last_name": "Cherniak",
"month": 12,
"year": 2023,
"address1": null,
"address2": null,
"city": null,
"state": null,
"zip": null,
"country": null,
"phone_number": null,
"company": null,
"full_name": "Justin Cherniak",
"eligible_for_card_updater": true,
"shipping_address1": null,
"shipping_address2": null,
"shipping_city": null,
"shipping_state": null,
"shipping_zip": null,
"shipping_country": null,
"shipping_phone_number": null,
"payment_method_type": "credit_card",
"errors": [],
"fingerprint": "e3cef43464fc832f6e04f187df25af497994",
"verification_value": "",
"number": "XXXX-XXXX-XXXX-1111"
}
}
You’ll notice that like in the request, the response from Clover is double-encoded. If we JSON-decode their response, it will look like the following:
{
"paymentId": "6SD5W6AVQ0D5G",
"result": "APPROVED",
"authCode": "OK8629",
"token": "HDAX90CY5WVF2",
"vaultedCard": {
"first6": "411111",
"last4": "1111",
"expirationDate": "1223",
"token": "1894469479681111"
}
}
Based on the source code from the remote-pay-cloud SDK (used for integrating with their physical terminals), we find the format of this return value to be:
/**
* @var array CloverPaymentResponse
* {
* @var string $authCode
* @var string $failureMessage
* @var string $token
* @var string $result (APPROVED|DECLINED)
* @var string $avsResult (SUCCESS|ZIP_CODE_MATCH|ZIP_CODE_MATCH_ADDRESS_NOT_CHECKED|ADDRESS_MATCH|ADDRESS_MATCH_ZIP_NOT_CHECKED|NEITHER_MATCH|SERVICE_FAILURE|SERVICE_UNAVAILABLE|NOT_CHECKED|ZIP_CODE_NOT_MATCHED_ADDRESS_NOT_CHECKED|ADDRESS_NOT_MATCHED_ZIP_CODE_NOT_CHECKED)
* @var string $paymentId
* @var string $cvvResult (SUCCESS|FAILURE|NOT_PROCESSED|NOT_PRESENT)
* }
*/
To process the response message from PMD, we need to check both Spreedly’s and Clover’s status codes. Here is some example code to do that:
$cloverResponse = $spreedlyResponse['response'];
if ($cloverResponse['status'] != 200)
{
throw new CloverPaymentException("Clover returned an error status of {$cloverResponse['status']}", $cloverResponse['status'], null, $cloverResponse);
}
// At this point, we have a successful request, but we haven't checked if the
// card was actually approved.
$cloverPaymentResponse = json_decode($cloverResponse['body'], true);
if ($cloverPaymentResponse['result'] != 'APPROVED')
{
throw new CloverPaymentException('There was an error processing your payment: ' . $cloverPaymentResponse['failureMessage'], $cloverResponse['status'], null, $cloverPaymentResponse);
}
At this point, you’ve completed a successful charge using Clover’s Developer Pay API!
Some additional notes and resources:
If you are building a Clover semi-integration, but want a better way to accept online payments, one option is to have your merchants get boarded to both Clover as well as CardConnect. Spreedly recently added a full gateway link with CardConnect, and their accounts can now be linked together. After writing all this code, we recently started doing this with our new merchants, so we’ll get full gateway support via Spreedly’s CardConnect gateway link, while using our Clover semi-integration for card-present sales.
If you are building a Clover semi-integration, do know that they have unpublished Void and Refund APIs. It’s unknown if they offer these for web apps, but for our semi-integration app, we were able to get documentation by emailing Clover support.
Documentation links:
- Clover Developer Pay API docs
- Spreedly Payment Method Distribution walkthrough
- Clover Rest API docs
- Setting up a Clover Developer Account
- Creating a Clover Web App
- Clover’s Support Forums