The code in this repo currently uses PHP but could very easily be ported into other languages. Eventually, there will be more code examples and samples in this repo that demonstrate use in other languages.
- Pre-Reqs
- Quick Start
- Complete Integration Guide
- Process Overview
/get-card-form-url.php
Options/get-ach-form-url.php
Options/update-payment-data.php
- Tunl Frontend SDK Methods
- Larger Example
- WebHooks
- Custom CSS Styling
- Vault on Insufficient Funds
- Dual Vaulting
- Troubleshooting
Before attempting to embed our hosted payment form in your web application, you will need an account on our Tunl merchant platform. https://tunl.com/contact/
Already have an account? Here are some quick links to create API Keys.
- https://merchant.tunl.com/merchant/settings (Production Accounts)
- https://test.tunl.com/merchant/settings (Test Accounts ONLY)
Once you have an account, you will need to log in to your Tunl dashboard (links provided above) and create an API Key and Secret.
To create your keys, navigate to your Settings page by clicking on the gear icon in the upper left menu bar. Scroll down and select Create API Key.
IMPORTANT: Copy and save your Secret. Your Secret will be inaccessible once you navigate away from this page. If this happens, simply create another set of keys, and delete the inaccessible keys.
This repo is setup with docker and docker-compose. You can quickly get started by cloning this repository to your local dev environment and running:
docker-compose up
Once running, you can update the src/secrets.php
file with your
Tunl API Key and Secret.
Then you should be able to navigate to either:
Steps involved:
- Craft the options to customize the embedded form
- Generate a unique URL (similar to Stripe's "Create Payment Intent")
- Use the generated url in an iframe
Condensed Example in PHP:
<?php
require_once("./tunl-embed-sdk.php");
$tunl_sdk = new TunlEmbedSDK;
$tunl_form_options = array(
"api_key" => "apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"secret" => "xxxxxxxxxxxxxxxxxxxxxxxxxx",
"iframe_referer" => "https://localhost:8082/",
"tunl_sandbox" => true, // set this if using a test tunl account api keys
"allow_client_side_sdk" => true
);
// get the embeddable form url and client secret (similiar to Stripe's create payment intent)
$tunl_client_secrets = $tunl_sdk->get_form_url($tunl_form_options);
// respond to the request appropriately using JSON
header('Content-Type: application/json; charset=utf-8');
echo json_encode($tunl_client_secrets);
?>
This could be called from a client side fetch to retreive the unique url and then dynamically render the iframe. This code could also be modified to accept a JSON body that would allow some custom options to be passed in. This is demonstrated in our Complete Example
Keep in mind, this is potentially a sensitive operation and you should review for secure implementation. For example, the
iframe_referer
should always be a statically set value that is a domain you own.
It should NOT be allowed to be set dynamically via JSON options passed in. This parameter helps to ensure that the form is ONLY allowed to be embedded on your site/application.
The code above is the bare minimum. This will get you a url to embed the form in an iframe, but there really isn't any context to this form. The form rendered for the URL generated in the code above will look like this:
This basic form will process a verify
only transaction for $0.01
and then immediately void it. This is obviously not very useful except for quick testing to make sure you can connect to your Tunl account. In the next section we will see how to customize our form and add more context. Things like, card holder name, amount, transaction type, etc.
Side Note: You can find this voided preauth under Settled Reports in your Tunl Account. Sort by timestamp descending and filter by VOID_PREAUTH.
Alternatively you could modify this code to be completely Server Side Rendered. Checkout src/index.php
for an example that uses this technique.
The tunl-embed-sdk.php
is nothing fancy at present. It just contains all the boilerplate to do CURL calls and a wrapper method to get the form url and client secret. To illustrate, here is a command line version of the CURL call being made by
$tunl_sdk->get_form_url($tunl_form_options)
curl -X POST https://test-payment.tunl.com/embed/get-card-form-url.php \
-H 'Content-Type: application/json' \
-d '{"api_key":"apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx","secret":"xxxxxxxxxxxxxxxxxxxxxxxxxx","iframe_referer":"https://localhost:8082/"}'
Below are all of the available options.
The only ones that are required are:
api_key
- Your Tunl API Keysecret
- Your Tunl API Secretiframe_referer
- Your Domain URLtunl_sandbox
- Not strictly require, but commonly needed during developmentallow_client_side_sdk
- Not strictly required, but almost always what you want
$payment_data = array(
'amount' => '123.45',
'cardholdername' => 'Card Holder',
'action' => 'verify', // could be sale, preauth, or verify
'ordernum' => 'My Custom Reference: ' . time(),
'comments' => 'My Custom Comments',
'street' => '2200 Oak St.',
'zip' => '49203',
);
$tunl_form_options = array(
"api_key" => "apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"secret" => "xxxxxxxxxxxxxxxxxxxxxxxxxx",
"iframe_referer" => "https://localhost:8082/",
"allow_client_side_sdk" => true,
"disable_captcha" => false,
"tunl_sandbox" => true,
"payment_data" => $payment_data,
"web_hook" => "https://localhost:8082/web_hook.php",
"custom_style_url" => "https://localhost:8082/custom-embed.css",
"debug_mode" => true,
"show_card_holder_field" => false,
"show_street_field" => false,
"show_zip_field" => false,
);
#!/bin/bash
# Production URL
# API_URL="https://payment.tunl.com/embed/get-card-form-url.php"
API_URL="https://test-payment.tunl.com/embed/get-card-form-url.php"
API_KEY="apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx"
SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST $API_URL \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"api_key": "$API_KEY",
"secret": "$SECRET",
"iframe_referer": "https://localhost:8082/",
"tunl_sandbox": true,
"allow_client_side_sdk": true
}
EOF
All other parameters are optional but allow much more control over the output.
The options for the ACH form are identical to the Card Form, the only difference is the endpoint that you call to get an ACH form.
The ACH Form provides several convenience features
- Provides all the legal copy that is required.
- Automatically Creates Contacts if they don't exist (based on email)
- Instant Account Verification via Plaid
Micro Deposit Flow via Plaid (NOT COMPLETE)- Automatically Adds a Funding Source to an existing contact if exists (base on email)
- Automatically initiates a transfer against the newly added funding source.
- Returns all of the above info for you to store in your own integration to process future transfers against the new funding source.
While the embedded credit card form allows for any customer to return and fill out card details and make payments at any time, this form is more of a "single-use" flow designed to onboard a customer and capture a funding source to be used in future transfers. Currently this form provides the Initial Customer Onboarding flow and transfer functionality ONLY.
Returning customers that have already added a funding source via this form is NOT SUPPORTED
If a customer that has already been onboarded using this form attempts to go through the process again an error will occur.
Currently as the integrator you will need to provide any "Returning Customer" checkout feature in your application.
You can use the tunl API to fetch a list of funding sources for any existing customers in your system, but how you associate customers in your system with contacts in your Tunl account is up to you.
However, if all that is needed is onboarding the customer/contact and getting a vault token/id that can be used to process future transfers via your integration/service then this form provides exactly everything you need.
If all you need to do is onboard and get a vault id back without initating a transfer you can leave out the payment_data
key or set it to null
(Shown below).
#!/bin/bash
# Production URL
# API_URL="https://payment.tunl.com/embed/get-ach-form-url.php"
API_URL="https://test-payment.tunl.com/embed/get-ach-form-url.php"
API_KEY="apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx"
SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST $API_URL \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"api_key": "$API_KEY",
"secret": "$SECRET",
"iframe_referer": "https://localhost:8082/",
"tunl_sandbox": true,
"allow_client_side_sdk": true,
"payment_data" => null,
}
EOF
All frontend work is identical to the credit card form. Here is an example for quick reference:
(async function () {
// create new TunlEmbed SDK instance
const tunl = new TunlEmbed();
// tell the Tunl SDK about Your Server Side endpoint url
await tunl.getFrameURL("create.php");
// mount the embedded form in the iframe
await tunl.mount("#tunl-frame");
// create a button click handler
document.querySelector("button").addEventListener("click", async () => {
// request a form submission and capture the results
const results = await tunl.submit().catch((err) => err);
// handle success or failure to your liking
if (results.status === "SUCCESS") console.log("SUCCESS", results);
if (results.status !== "SUCCESS") console.log("ERROR", results);
});
})();
Once the tunl.submit()
method is called it will validate the form and kick off the plaid flow:
After the customer has completed the plaid flow your results
will contain the following payload:
{
"status": "SUCCESS",
"msg": "Successfully added funding source and initiated transfer.",
"ttid": "a8063f30-d5eb-4362-8b02-126249c67eb4",
"amount": "65.00",
"authnum": "2",
"timestamp": "2023-09-06 16:56:54 +0000",
"ordernum": null,
"type": "SALE",
"phardcode": "PENDING",
"verbiage": "PENDING",
"code": "2",
"batchnum": null,
"ptrannum": "1546425d-d64c-ee11-8154-ee5b5eeb80f1",
"clerkid": "Custom Clerk",
"stationid": "Station ID",
"entrymode": null,
"tax": "1.00",
"examount": "2.00",
"custref": null,
"balance": null,
"unsettled": true,
"vaultId": 7999, // the tunl vault id, required when performing transfers
"accountName": "Plaid Checking", // Plaid Provided Account Nickname
"accountMask": "0000", // Plaid Provided Account Mask
"account_id": "z88N4KJ4wdCnkLJ54dmlhmjPlDowD8clnMbXy", // the Plaid Account ID
"verificationMethod": "PLAID", // this currently will always be "PLAID"
"micro_deposits": false, // whether or not the user chose micro deposits
"contactId": "bbe2d464-1679-4c7a-b35a-5e1748d3a120", // internal tunl contact id, required when performing transfers
"fundingSourceId": "bd17d2d1-ffb1-47f5-b6e0-5a0857acd704", // internal tunl account id, helpful to retreive later by this id
"externalAchId": "914e11c5-bb52-4780-a8e9-d84b4602f4d5", // external reference (not needed)
"bankAccountType": "checking"
}
The important details to store for performing future transfers are the contactId
and the vaultId
For more information on performing ACH Transfers see our ACH Guide
Param | Default | Description |
---|---|---|
api_key | null | Your Tunl API Key |
secret | null | Your Tunl API Secret |
iframe_referer | null | Your Domain URL. ie: https://your.domain.com This must be set to the domain you intend to host the embedded form on. |
allow_client_side_sdk | false | Allows the embedded form to be interacted with using the Tunl Frontend SDK. Complete Example Available Here |
disable_captcha | false | Disables the Captcha System for testing purposes, this switch ONLY works in our TEST environments. |
tunl_sandbox | false | Selects the tunl api environment. true = https://test-api.tunl.com false = https://api.tunl.com If you created your API keys using a test merchant account via https://test.tunl.com instead of https://merchant.tunl.com then make sure to set this parameter to true |
payment_data | [] | Type: PHP Associative Array. See example in code snippet above under $payment_data Additional Data to post to the tunl payments endpoint. See below for info on the available options. |
web_hook | null | A url of the endpoint that you own/control to be called upon successful Tunl Payments API submission. See src/web_hook.php for an example web hook. |
custom_style_url | null | A url to your own custom stylesheet that will be used in the embedded form. See src/custom-embed.css for an example stylesheet. |
debug_mode | false | If set to true, puts PHP in an extreme error reporting mode. Additional data will be displayed related to the embeded form as well. For example: instead of seeing a success page, you will see a prettified JSON object of all the transaction response data and any response returned by the web_hook |
return_server_token | false | If set to true, the info returned from this API call will include a server_secret property. This server secret can be used to perform updates to the embedded form payment data after it has already been loaded and presented to the user. !!!IMPORTANT!!! You need to be careful not to pass this secret to the client/browser!!! This is a temporary value that only lasts the life of the form. It should only be stored on your server in some kind of session variable. |
show_card_holder_field | false | Force our built in Card Holder Name input field to be displayed in our embedded form. This is helpful, but for full control and customizeability we recommend that you implement your own fields and styling. You can then set this data using setPaymentData in our client library. |
show_street_field | false | Force our built in Street input field to be displayed in our embedded form. This is helpful, but for full control and customizeability we recommend that you implement your own fields and styling. You can then set this data using setPaymentData in our client library |
show_zip_field | false | Force our built in Zip Code input field to be displayed in our embedded form. This is helpful, but for full control and customizeability we recommend that you implement your own fields and styling. You can then set this data using setPaymentData in our client library |
Param | Default | Description |
---|---|---|
amount | "0.01" | The amount to be charged (or pre-authorized). Keep in mind that a HOLD for whatever amount you specify here will be applied to the user's card. If you are just trying to 'verify' that a card is real and can be charged, it is best to leave this setting at the default 0.01. |
cardholdername | null | The name printed on the physical credit card. |
action | "verify" | The type of payment transaction to post. This can be preauth , sale , or verify If verify is set it will run a preauth transaction and immediately void it. This allows you to verify card holder data without committing to a preauth or sale type transaction. |
ordernum | null | An Order Number to add as a reference to this transaction. If left blank the Tunl API will create its own order number. |
comments | null | Any freeform comments you would like to add to this transaction. |
street | null | The street of the billing address of the card holder. |
zip | null | The zip code of the billing address of the card holder. |
This endpoint allows you to update payment data (including action
and amount
) for an embedded form after it has already been loaded and displayed to the end user. In order to use this endpoint, you need the server_secret
that can optionally be returned by the /get-card-form-url.php
call. For this value to be returned in that call you need to set the return_server_token
option to true
.
!!!IMPORTANT!!! You should NEVER pass this server_secret
to the client/browser!!! This is a temporary value that only lasts the life of the form, but knowledge of the server secret enables modifying payment data such as the amount
to be charged. It should only be stored on your server in some kind of session variable.
#!/bin/bash
# Production URL
# API_URL="https://payment.tunl.com/embed/update-payment-data.php"
API_URL="https://test-payment.tunl.com/embed/update-payment-data.php"
API_KEY="apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx"
SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST $API_URL \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"server_secret": "bf51fbeccef9df9c49b28c5967ac279b51f5ef9dafbba59a7f7210ad5252c34f03f2f187edb0053d",
"payment_data": {
"amount": "1000.10",
"action": "sale",
"comments": "set on update endpoint"
}
}
EOF
We provide an OPTIONAL frontend sdk library to allow for client side interaction. This allows you to gain even more control over the end user experience and provide a seamless payment form/fields that integrate perfectly with your own applications/solutions.
Our complete integration guide goes into step by step detail using the client library.
Just include the following script in your head tag:
<script src="https://payment.tunl.com/embed/assets/tunl-embed-sdk.js"></script>
For the bleeding edge development version use:
<script src="https://test-payment.tunl.com/embed/assets/tunl-embed-sdk.js"></script>
This method will call your server end point to retrieve the unique iframe url and client secret.
If you do not want to use this method, you can call the endpoint yourself and pass the data directly into the mount
function via the options
argument. See mount
function documentation below for more details.
URL: string - this can be a FQDN and path or a simple relative path
Options: [FetchOptions object](https://developer.mozilla.org/en-US/docs/Web/API/fetch)
// tell the Tunl SDK about Your Server Side endpoint url
// the following are all valid URL inputs
await tunl.getFrameURL("create.php");
await tunl.getFrameURL("/create.php");
await tunl.getFrameURL("/create.php?order_id=1000"); // can also pass in query params
await tunl.getFrameURL("relative/path/create.php");
await tunl.getFrameURL("/absolute/path/create.php");
await tunl.getFrameURL("https://your.domain.com/create.php");
// Example with options
await tunl.getFrameURL("create.php", {
method: "POST",
headers: myHeaders,
body: JSON.stringify(data)
});
This method doesn't actually return anything, but the server endpoint that it calls should return an object the looks like the one below. The frontend library automatically handles this information. There is no need to perform any intermediate manipulation of this information.
{
"url": "https://test-payment.tunl.com/embed/load-embedded-form.php?one-time-use-code=e862721da6a0547f39cda1a7ea7475f8268e1ceb8d23b90209dd9a60a78635842f1379275f51c5d8",
"shared_secret": "07d687b5fd040e61f4af3fa3b13457b8d7d8234f1422f437bd2006ffe56a28671521a814cf06f460",
"msg": "SUCCESS"
}
This will "mount" the embedded form in the iframe.
cssSelector: string - any valid css selector that can be passed into `document.querySelector`
the selected element is expected to be an <IFRAME> node/element
options?: MountOptions - {
url?: string (iFrame URL),
shared_secret?: string,
disableAutoResize?: boolean,
}
// mount the embedded form in the iframe
await tunl.mount("#tunl-frame"); // selects an iframe with the id of "tunl-frame"
await tunl.mount(".tunl-frame"); // selects an iframe with a class of "tunl-frame"
await tunl.mount("iframe"); // selects the first <iframe> on the page
// how to manually call the create.php endpoint and use the results
// await tunl.getFrameURL("create.php");
const fetchResp = await fetch("create.php");
const frameData = await fetchResp.json();
await tunl.mount("#tunl-frame", frameData);
// or build the options object yourself:
await tunl.mount("#tunl-frame", {
url: frameData.url,
shared_secret: frameData.shared_secret,
disableAutoResize: true, // prevent the iframe from controlling its own height
});
Our iframe disables overflow (scrollbars) to prevent strange CSS bugs from causing them. We control height internally by default because we have validation messages that drive the height of the iframe larger and smaller depending on current validation state. If you would like full control over this behavior you can disable it here and listen for resize
events. See the addEventListener
for more info.
NONE
While this method does not return anything, if you use the await
keyword it will wait to return until the iframe is ready. This can be helpful for rendering a loading div/image before calling the mount
method and then hiding the loader immediately after the mount
method returns.
This will set the focus on the first input inside the payment iframe.
NONE
// mount the embedded form in the iframe
await tunl.mount("iframe");
// then immediately set focus on the first input in the iframe
await tunl.setFocus();
NONE
While this method does not return anything, if you use the await
keyword it will wait to return until the iframe input focus is ready. This can be helpful for rendering a loading div/image before calling the mount
method and then hiding the loader immediately after the mount
method returns.
This method is used to set payment details such as card holder name, street, zip, and comments directly on the client. Technically the iframe does make a server side request to update its server side state with the new payment details, but that is all handled for you. As this is a sensitive operation, the details that are allowed to be updated are limited to the items shown in the example below.
All properties of the object are optional
interface paymentData {
cardholdername?: string;
ordernum?: string;
comments?: string;
street?: string;
zip?: string;
}
// helper function to get values from named <input> elements
const getVal = (name) => {
return document.querySelector(`[name="${name}"]`).value;
};
// set additional payment data
const results = await tunl.setPaymentData({
cardholdername: getVal("cardholdername"),
street: getVal("street"),
zip: getVal("zip"),
comments: getVal("comments"),
});
console.log(results)
This method returns a JSON Object. This isn't particularly useful for anything other than debugging or confirming expectations. However, it is a good idea to wrap this in your usual error handling strategy as it is a network call under the hood, so of course the usual failure modes are possible.
{
"status": "SUCCESS",
"msg": "Successfully updated payment data.",
"payment_data": {
"action": "verify",
"terminalId": 0,
"ordernum": 1680831905,
"comments": "comments",
"amount": "0.01",
"tax": 0,
"examount": 0,
"street": "street",
"zip": "zip",
"cv": "",
"expdate": "",
"account": "",
"cardholdername": "Zach",
"custref": null,
"clerkid": "iDep Embed Form",
"autovault": "Y",
"vaultAccount": true,
"accountId": null,
"contactId": null
}
}
This will check if the payment form inside the iframe is valid or not. This is not required as the submit function will automatically validate and report errors. This function is provided to allow for edge cases where your integration may require advanced knowledge of the form's validity before attempting to submit.
NONE
async function testCheck(){
const results = await tunl.checkValidity().catch((err) => err);
console.log(results);
}
Success Response:
{
"status": "SUCCESS",
"msg": "Form entry is valid."
}
Error Response:
{
"error": "FORM_NOT_VALID",
"msg": "Form entry is not valid, please correct errors",
"errors": [
{
"input": "account",
"error": "Field is required"
},
{
"input": "expdate",
"error": "Field is required"
},
{
"input": "cv",
"error": "Field is required"
}
],
"msgID": "db63eebc-ec02-4734-82fe-74801904dfed"
}
This will add an event listener to the tunl payment iframe. The events that are currently available are:
paymentFormBecameValid
- fires when the form is complete and validpaymentFormBecameInvalid
- fires if the form subsequently becomes INVALIDresize
- fires when the internal body height of the iframe changes.
type: A case-sensitive string representing the event type to listen for.
listener: The callback function to be fired in response to the event.
tunl.addEventListener("paymentFormBecameValid", (ev) => console.log(ev))
tunl.addEventListener("paymentFormBecameInvalid", (ev) => console.log(ev))
tunl.addEventListener("resize", (msgData) => {
document.querySelector('#tunl-frame').style.height = msgData.bodyHeight.toString() + "px";
});
event: an object containing basic info from the event
Example paymentFormBecameValid
Event Object:
{
"event": "paymentFormBecameValid",
"msg": "Form is complete and valid.",
"msgID": "event"
}
Example paymentFormBecameInvalid
Event Object
{
"event": "paymentFormBecameInvalid",
"msg": "Form is no longer valid!",
"msgID": "event"
}
This method will attempt to submit the tunl payment form. The embedded form has its own client side validation that must pass before it will actually submit. If validation fails this method will return an error. Other errors could occur as well, but this one will likely be the most common. On success it will return transaction information that you should store in your database. This transaction information is completely sanitized and safe to store.
NONE
// request a form submission and capture the results
const results = await tunl.submit().catch((err) => err);
// handle success or failure to your liking
if (results.status === "SUCCESS") {
document.querySelector("button").style.display = "none";
document.getElementById("tunl-frame").style.display = "none";
document.getElementById("success").style.display = "";
}
if (results.status !== "SUCCESS") {
document.getElementById("error").style.display = "";
document.getElementById("error").innerText =
results.msg || "Unknown Error";
}
Full Success Response:
{
"status": "SUCCESS",
"msg": "Card was successfully verified.",
"embedded_form_action": "verify",
"transaction_ttid": "309574334",
"transaction_amount": "0.01",
"transaction_authnum": "522169",
"transaction_timestamp": "2023-04-06 13:26:05 +0000",
"transaction_ordernum": "ClientSetOrderNum",
"transaction_type": "PREAUTH",
"transaction_phardcode": "SUCCESS",
"transaction_verbiage": "APPROVED",
"transaction_code": "1",
"vault_token": "088acc40-c28f-4084-a3d2-b801b9c4fccb",
"webhook_response": [],
"cardholdername": "Testing Client Set",
"street": "client set street",
"zip": "49203",
"comments": "client set comments",
"void_ttid": "309574334",
"void_phardcode": "SUCCESS",
"void_verbiage": "SUCCESS".
"void_code": "1"
}
Error response:
{
"error": "FORM_NOT_VALID",
"msg": "Form entry is not valid, please correct errors",
"msgID": "a414b0ab-0502-4c21-8efa-cd1dfb485305"
}
A full sample of this example is available in less than 100 lines of code in the src/client-side-example.php
, but we are going to break that down piece by piece here.
In this example, we create a front end client that has a few fields to gather some info from the customer. This code will not render a very pretty page, but it cuts right to the core of the intention.
<style>
input, button, modal {display: block; margin-bottom: 10px;}
iframe {height: 500px; border: none;}
</style>
Card Holder Name <input name="cardholdername" />
Order No <input name="ordernum" />
Comment <input name="comments" />
Street <input name="street" />
Zip <input name="zip" />
<!-- example only, do not use inline event handlers like onclick in production -->
<button onclick="start()">Make Payment</button>
<modal style="display: none;">
<iframe></iframe>
</modal>
<script src="client-side.js"></script>
This HTML will render a form that looks like so:
In the code above, the User will fill out their details and click the Make Payment
button. This button will call some javascript to generate our unique embeddable form url. We can then udpate the iframe in our mock modal and display it to the User to fill out their credit card details.
The javascript in our example client-side.js
looks like this:
async function start() {
const payment_data = {
cardholdername: document.querySelector('[name=cardholdername]').value,
ordernum: document.querySelector('[name=ordernum]').value,
comments: document.querySelector('[name=comments]').value,
street: document.querySelector('[name=street]').value,
zip: document.querySelector('[name=zip]').value,
}
const form = await get_form_url(payment_data);
document.querySelector("iframe").src = form.url;
document.querySelector("modal").style.display = ''
}
async function get_form_url(payment_data) {
const resp = await fetch("",
{
method: "POST",
body: JSON.stringify(payment_data)
}
)
return await resp.json();
}
The start
function collects the data from the html input fields and stores them in payment_data
const. It then passes this data into the get_form_url
function that we see just below.
This function just POST's this data back to the page we are already on (which is actually a php page as can be seen in the full example code: src/client-side-example.php
and then simply returns the parsed JSON directly to the caller.
The start
function uses these results to update the src
attribute on the iframe on our html and removes the display: none
style from our modal. The User can now see the credit card form as shown in the image below.
Not exactly a modal, but you can easily imagine that part!
require_once('./secrets.php');
require_once("./tunl-embed-sdk.php");
$tunl_sdk = new TunlEmbedSDK;
// get json payload
$json = file_get_contents('php://input');
$data = json_decode($json, true);
$amount = get_amount_from_order($data['ordernum']);
$payment_data = array(
'amount' => $amount,
'cardholdername' => $data['cardholdername'] ?? null,
'action' => 'verify',
'ordernum' => $data['ordernum'] ?? null,
'comments' => $data['comments'] ?? null,
'street' => $data['street'] ?? null,
'zip' => $data['zip'] ?? null,
);
$tunl_form_options = array(
"api_key" => $tunl_api_key, // from secrets.php
"secret" => $tunl_secret, // from secrets.php
"iframe_referer" => "https://localhost:8082/",
"tunl_sandbox" => true, // set this if using a test tunl account api keys
"allow_client_side_sdk" => true
"payment_data" => $payment_data,
// "web_hook" => "https://localhost:8082/web_hook.php",
"custom_style_url" => "https://localhost:8082/custom-embed.css",
// "debug_mode" => true,
);
$form = $tunl_sdk->get_form_url($tunl_form_options);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($form);
The above will look familiar as it is basically a copy and paste of "All Available Options". The changes that have been made are to be able to receive input via a JSON POST request that takes in the parameters from our HTML form above.
Notice that we are doing a lookup in our own database to set the amount
field. This is important to make sure the amount cannot be tampered with by the client performing the request. The specific implementation here will heavily depend on your own code structure, database, framework, etc; but, the stub function in src/client-side-example.php
looks like this:
function get_amount_from_order($ordernum){
// do something to get the payment amount from your database or backend
// this prevents abuse of this endpoint and protects against bad actors setting their own amount
// $amount = fetch_from_db($ordernum);
// return $amount;
return "123.45";
}
WebHooks allow you to handle more advanced transaction scenarios. WebHooks will be called either on a transaction failure (and include error info) OR on transaction success (and include transaction data). WebHook must respond with JSON data. Any response from your webhook is passed thru back to the client for your own use on the client side. Optionally, you can disable the standard response all together as shown below.
Your webhook can be enabled by setting the web_hook
tunl form option.
Example in your PHP backend:
$tunl_form_options = array(
...
"web_hook" => "https://yoursite.com/web_hook.php",
...
);
To disable the standard response make sure to set a property named only_return_webhook_response_to_client
to true
in your webhook json response. Here is an example in PHP:
$newData = array(
'only_return_webhook_response_to_client' => true,
'other_data' => $data
);
echo json_encode($newData);
The above let's you choose when and what messages should be sent to the client/browser directly from your webhook, any others sent back without this parameter set will include the standard responses from our embedded form server.
Or disable it entirely via the create URL call in the options. This will disable the standard response completely and ONLY respond with data directly from your web_hook.
$tunl_form_options = array(
...
"web_hook" => "https://yoursite.com/web_hook.php",
"only_return_webhook_response_to_client" => true,
...
);
{
"data": {
{
"status": "SUCCESS",
"msg": "Sale processed successfully.",
"embedded_form_action": "sale",
"transaction_ttid": "311489097",
"transaction_amount": "6545.00",
"transaction_authnum": "647828",
"transaction_timestamp": "2023-04-25 01:29:38 +0000",
"transaction_ordernum": "1682386177",
"transaction_type": "SALE",
"transaction_phardcode": "SUCCESS",
"transaction_verbiage": "APPROVED",
"transaction_code": "1",
"vault_token": "244cac1d-1893-440f-8ba0-16cf48be2524",
"webhook_response": [],
"cardholdername": "Zach",
"street": "",
"zip": "",
"comments": ""
... lot's more!
}
},
"status": 200,
"curl_error": "",
"curl_errno": 0
}
{
"data": {
"message": "BAD CID",
"code": "PaymentException"
},
"status": 400,
"curl_error": "",
"curl_errno": 0
}
<?php
// get json payload
$json = file_get_contents('php://input');
$data = json_decode($json, true);
// if this web hook is called with a transaction error
if ($data['status'] !== 200){
// perform your own custom error processing
// optionally respond with custom response
handleErr($data);
exit();
}
// do stuff with the data
// at minimum you will likely want to store the following items
// in your database to be able to perform future actions
$transaction_id = $data["transaction_ttid"];
$vault_id = $data["vault_token"];
$orderNum = $data["transaction_ordernum"];
$some_potential_error_inside_the_webhook = false;
// handle any errors in your own code
if ($some_potential_error_inside_the_webhook){
// respond with any code other than 200
// Tunl API will attempt to void the transaction.
http_response_code(500);
handleErr(array("test" => "test"));
exit();
}
// returned data is passed thru back to the client
echo json_encode(array(
"status" => "SUCCESS",
"msg" => "Your Success Message",
// you can disable the standard response if you want full control.
// (Or set this in the createUrl Options)
// 'only_return_webhook_response_to_client' => true,
"data" => [ /* YOUR WEBHOOK RESPONSE DATA GOES HERE */ ]
));
function handleErr($data){
echo json_encode(array(
"status" => "ERROR",
"msg" => "Your Error Message",
// Example only: be careful about passing unhandled error data back to the client.
// https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html
"data" => $data
));
}
?>
This form comes with some sensible default styling. You have already seen this in several of the images in this readme, but for the sake of completeness, here it is again:
This default styling is great for getting started, but likely at odds with your brand and site styles. In order to get started applying custom styles we will need to set the custom_style_url
option in our Tunl Form Options.
Click here to view the full default css rules
Let's start with a completely unstyled look to see what we are working with. Create an empty CSS file in your project or in a publicly available uri on your domain. Then set the custom_style_url
option to point directly to it.
$tunl_form_options = array(
...
"custom_style_url" => "https://localhost:8082/custom-embed2.css",
...
);
We should now see something like this:
Woof, not very pretty. Let's see how we can improve this.
Below is what the underlying HTML looks like. You can see the we have plenty of class selectors, id's, name attributes and wrapper divs to use for styling.
<body class="tunl-embedded-body">
<div class="tunl-embedded-form-wrapper">
<form class="tunl-embedded-form" id="tunl_form" method="post">
<div class="tunl-field-group ccname-group">
<label for="tunl_cc_name">Card Holder Name</label>
<input type="text" name="cardholdername" id="tunl_cc_name">
</div>
<div class="tunl-field-group ccno-group">
<label for="tunl_cc_no">Credit Card No</label>
<input type="text" name="account" id="tunl_cc_no">
</div>
<div class="tunl-field-group expire-group">
<label for="tunl_cc_expires">Expiration</label>
<input type="text" name="expdate" id="tunl_cc_expires">
</div>
<div class="tunl-field-group cvv-group">
<label for="tunl_cc_cvv">CVV</label>
<input type="text" name="cv" id="tunl_cc_cvv">
</div>
<div class="tunl-field-group combo-error-group">
<p class="error-message" style="padding: 5px; height: 55px;"></p>
<p class="error-message-height-gauge"></p>
</div>
<div class="tunl-field-group submit-group">
<button>Submit</button>
</div>
</form>
</div>
</body>
An incredible improvement in style can be had in very few lines of CSS. For Example:
* {
box-sizing: border-box;
}
body {
margin: 0px;
font-family: arial;
overflow: hidden;
}
input,
button {
display: block;
margin-bottom: 10px;
width: 100%;
border-radius: 5px;
border: 1px solid gray;
padding: 10px;
}
Will turn the above 1990's form into the results shown below:
Throw in some CSS Grid magic (or flexbox) and you can really do anything.
* {
box-sizing: border-box;
}
body {
margin: 0px;
font-family: arial;
overflow: hidden;
}
.tunl-embedded-form {
display: grid;
grid-template-columns: repeat(6, 1fr);
column-gap: 10px;
}
.ccname-group {
grid-column: span 6;
}
.ccno-group {
grid-column: span 3;
}
.expire-group {
grid-column: span 2;
}
.cvv-group {
grid-column: span 1;
}
.submit-group {
grid-column: span 6;
}
label {
display: block;
width: 100%;
}
input,
button {
display: block;
border: 1px solid grey;
border-radius: 5px;
padding: 5px;
margin-bottom: 15px;
box-shadow: 1px 1px 5px -1px grey;
width: 100%;
}
The css above adds some box-shadow and CSS grid to render the following result:
If you prefer to start with the current default styling and make small tweaks then starting with the default css is the best idea. Below is the full default css as of Apr 6, 2023. If this becomes out of sync you can always use your browser inspect to grab the most current CSS the default iframe is downloading.
* {
box-sizing: border-box;
}
body {
margin: 0px;
font-family: arial;
overflow: hidden;
}
.tunl-embedded-form {
display: grid;
grid-template-columns: repeat(120, 1fr);
/* column-gap: 10px; */
align-items: end;
}
.ccname-group {
grid-column: span 120;
}
.combo-error-group {
grid-column: span 120;
}
.ccno-group {
grid-column: span 85;
}
.expire-group {
grid-column: span 20;
}
.cvv-group {
grid-column: span 15;
}
.submit-group {
grid-column: span 120;
}
/* .expire-group label, .cvv-group label {text-align: center;} */
.ccno-group input,
.expire-group input,
.cvv-group input {
margin-left: 0px;
margin-right: 0px;
padding-left: 0px;
padding-right: 0px;
}
.ccno-group .error-message,
.expire-group .error-message,
.cvv-group .error-message {
margin: 0px;
}
.error-message-height-gauge {
position: absolute;
transform: translateX(-10000px);
width: 100%;
}
.error-message.show,
.error-message-height-gauge {
padding: 5px 5px;
}
.error-message {
height: 0px;
padding: 0px 5px;
transition: all 0.3s;
overflow: hidden;
}
.error-message,
.error-message-height-gauge {
margin: 0px 0px 11px;
color: red;
border-radius: 5px;
font-size: 10pt;
white-space: pre-line;
}
.ccno-group.default-card-icon:before {
background-image: url(https://test-payment.tunl.com/embed/assets/code.svg);
}
.ccno-group.visa-card-icon:before {
background-image: url(https://test-payment.tunl.com/embed/assets/visa.svg);
}
.ccno-group.mastercard-card-icon:before {
background-image: url(https://test-payment.tunl.com/embed/assets/mastercard.svg);
}
.ccno-group.amex-card-icon:before {
background-image: url(https://test-payment.tunl.com/embed/assets/amex.svg);
}
.ccno-group.discover-card-icon:before {
background-image: url(https://test-payment.tunl.com/embed/assets/discover.svg);
}
.ccno-group:before {
background-size: contain;
background-repeat: no-repeat;
position: absolute;
width: 30px;
display: block;
height: 26px;
content: "";
transform: translate(15px, 26px);
}
.ccno-group input {
border-bottom-right-radius: 0px;
border-top-right-radius: 0px;
border-right: 0px;
padding-left: 55px;
}
.expire-group input {
border-radius: 0px;
border-left: 0px;
border-right: 0px;
}
.cvv-group input {
border-bottom-left-radius: 0px;
border-top-left-radius: 0px;
border-left: 0px;
}
label {
display: block;
width: 100%;
line-height: 14pt;
font-size: 12pt;
}
input.invalid {
color: red;
}
input:focus {
outline: none;
}
input,
button {
display: block;
border: 1px solid grey;
border-radius: 5px;
padding: 10px;
width: 100%;
height: 36px;
}
.tunl-field-group {
position: relative;
}
There are use cases where a transaction may fail for insufficient funds, but you still want to vault the card data as the card data is likely valid.
You can provide the vault_on_nsf
option in the config as shown below, this will bypass the usual checks and allow a card to be vaulted. Keep in mind that the transaction is still considered failed.
NOTE: This will also allow transactions that would normally fail for "BLOCKED 1ST USE"
#!/bin/bash
# Production URL
# API_URL="https://payment.tunl.com/embed/get-card-form-url.php"
API_URL="https://test-payment.tunl.com/embed/get-card-form-url.php"
API_KEY="apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx"
SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST $API_URL \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"api_key": "$API_KEY",
"secret": "$SECRET",
"iframe_referer": "https://localhost:8082/",
"tunl_sandbox": true,
"allow_client_side_sdk": true,
+ "vault_on_nsf": true
}
EOF
There are use cases where you may to vault card information without verifying any card details.
You can provide the vault_only
option in the config as shown below, this will bypass the usual checks and allow a card to be vaulted regardless of validity.
#!/bin/bash
# Production URL
# API_URL="https://payment.tunl.com/embed/get-card-form-url.php"
API_URL="https://test-payment.tunl.com/embed/get-card-form-url.php"
API_KEY="apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx"
SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST $API_URL \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"api_key": "$API_KEY",
"secret": "$SECRET",
"iframe_referer": "https://localhost:8082/",
"tunl_sandbox": true,
"allow_client_side_sdk": true,
+ "vault_only": true
}
EOF
Dual Vaulting allows you to add additional supported providers to vault (tokenize) card data.
The example below shows the basic additional config parameters to setup additional providers.
#!/bin/bash
# Production URL
# API_URL="https://payment.tunl.com/embed/get-card-form-url.php"
API_URL="https://test-payment.tunl.com/embed/get-card-form-url.php"
API_KEY="apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx"
SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST $API_URL \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"api_key": "$API_KEY",
"secret": "$SECRET",
"iframe_referer": "https://localhost:8082/",
"tunl_sandbox": true,
"allow_client_side_sdk": true,
+ "additional_vault_providers": [
+ { ... another provider },
+ { ... another provider }
+ ]
}
EOF
We currently support RepayOnline, but additional providers are generally easy to add, please inquire if you would like to see a new provider added to the list!
The example below shows how to configure RepayOnline as an additional vault provider:
#!/bin/bash
# Production URL
# API_URL="https://payment.tunl.com/embed/get-card-form-url.php"
API_URL="https://test-payment.tunl.com/embed/get-card-form-url.php"
API_KEY="apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx"
SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST $API_URL \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"api_key": "$API_KEY",
"secret": "$SECRET",
"iframe_referer": "https://localhost:8082/",
"tunl_sandbox": true,
"allow_client_side_sdk": true,
+ "additional_vault_providers": [
+ {
+ "provider": "repayonline",
+ "api-version": "1",
+ "rg-api-secure-token": "xxxxxxx",
+ "rg-api-user": "User_Name",
+ "rg-merchant-id": "xxxxxxxx", // OPTIONAL, but may be required for users with multiple merchants
+ "customer_key": "xxxxxxxx", // OPTIONAL, but possibly required for tokenizing against a specific customer.
+ "zero-auth" => true, // this option will add an additional authorization of the card before attempting to tokenize with repay, if this fails the card will not be tokenized with repay.
+ "sandbox": true
+ }
+ ]
}
EOF
The rest of the process for integration is identical, the only new part will be the availability of the additional vault token(s) inside the response payload.
The current response includes a vault_token
property that contains the Tunl Vault Token.
The additional RepayOnline token info will be provided inside the additional_vault_tokens
property in the response payload as shown below:
{
"status": "SUCCESS",
"msg": "Sale processed successfully.",
...
"vault_token": "b26aad1c-5ec3-49a5-9702-671875cf2630",
+ "additional_vault_tokens": [
+ {
+ "provider": "repayonline",
+ "token": "1234567890",
+ "full_response": {
+ "card_token_key": 1082478257,
+ "exp_date": "0324",
+ "name_on_card": "Zach",
+ "street": "",
+ "zip": "",
+ "last4": "1111",
+ "card_type": "VISA",
+ "is_eligible_for_disbursement": false,
+ "customer_id": null,
+ "custom_fields": [],
+ "nickname": null,
+ "bin": "411111",
+ "external_payment_token": null,
+ "card_info": {
+ "brand": "VISA",
+ "type": "CREDIT"
+ }
+ }
+ }
+ ]
...
}
The example below shows how to configure BridgePay as an additional vault provider:
#!/bin/bash
# Production URL
# API_URL="https://payment.tunl.com/embed/get-card-form-url.php"
API_URL="https://test-payment.tunl.com/embed/get-card-form-url.php"
API_KEY="apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx"
SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST $API_URL \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"api_key": "$API_KEY",
"secret": "$SECRET",
"iframe_referer": "https://localhost:8082/",
"tunl_sandbox": true,
"allow_client_side_sdk": true,
+ "additional_vault_providers": [
+ {
+ "provider": "bridgepay",
+ "username": "asdf",
+ "password": "asdf",
+ "merchantAccountCode": "123123123",
+ "invoiceNumber": "InvoiceNumber123",
+ "transIndustryType": "DM",
+ "storedCredential": "InitialUnscheduled",
+ "networkReferenceNumber": "60319733",
+ "holderType": "P",
+ "accountType": "R",
+ "sandbox": true
+ # "whitelabel": "pathtivity" # use whitelabel to select a whitelabel url provider. pathtivity is currently the only extra option.
+ # "operation": "pennyAuth" # default is "pennyAuth" which will put a "0.01" temp auth hold on the card, "tokenOnly" (default) returns the card token only
+ }
+ ]
}
EOF
The rest of the process for integration is identical, the only new part will be the availability of the additional vault token(s) inside the response payload.
The current response includes a vault_token
property that contains the Tunl Vault Token.
The additional RepayOnline token info will be provided inside the additional_vault_tokens
property in the response payload as shown below:
{
"status": "SUCCESS",
"msg": "Sale processed successfully.",
...
"vault_token": "b26aad1c-5ec3-49a5-9702-671875cf2630",
+ "additional_vault_tokens": [
+ {
+ "provider": "bridgepay",
+ "token": "1000000010261111",
+ "expirationDate": "1234",
+ "full_response": {
+ "cardType": "Visa",
+ "token": "1000000010261111",
+ "authorizationCode": "118192",
+ "referenceNumber": "345694081",
+ "gatewayResult": "00000",
+ "authorizedAmount": 1234,
+ "originalAmount": 1234,
+ "expirationDate": "1234",
+ "cvResult": "N",
+ "cvMessage": "Not matches",
+ "isCommercialCard": "False",
+ "gatewayTransID": "4434790404",
+ "gatewayMessage": "A01 - Approved",
+ "internalMessage": "Approved: 118192 (approval code)",
+ "transactionDate": "20230803",
+ "remainingAmount": 0,
+ "isoCountryCode": "840",
+ "isoCurrencyCode": "USD",
+ "isoTransactionDate": "2023-08-03T21:58:54.523",
+ "isoRequestDate": "2023-08-03T21:58:54.523",
+ "networkReferenceNumber": "345694081",
+ "merchantCategoryCode": "5999",
+ "networkMerchantId": "123123123",
+ "networkTerminalId": "10001",
+ "maskedPAN": "************1111",
+ "responseTypeDescription": "auth",
+ "cardClass": "Credit",
+ "cardModifier": "None",
+ "cardHolderName": "Test",
+ "providerResponseMessage": "Approved",
+ "organizationId": "57182",
+ "merchantAccountCode": "14043001",
+ "requestType": "004",
+ "responseCode": "00000",
+ "responseDescription": "Successful Request"
+ }
+ }
+ ]
...
}
Errors returned from the additional vault providers will be returned as shown below:
{
"status": "SUCCESS",
"msg": "Sale processed successfully.",
...
"vault_token": "b26aad1c-5ec3-49a5-9702-671875cf2630",
"additional_vault_tokens": [
{
"provider": "repayonline",
- "token": "1234567890"
+ "error": true,
+ "error_code": 400,
+ "error_msg": "Something Bad Happened!",
+ "error_obj": { ... error object }
},
{ ... other token },
{ ... other token }
]
...
}
If you are configuring additional vault providers and would like to disable the tunl vault (to make the other providers primary) then you can set the autovault to 'N' in the payment_data
object. In the example below, only the repayonline vault provider will be executed:
#!/bin/bash
# Production URL
# API_URL="https://payment.tunl.com/embed/get-card-form-url.php"
API_URL="https://test-payment.tunl.com/embed/get-card-form-url.php"
API_KEY="apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx"
SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST $API_URL \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"api_key": "$API_KEY",
"secret": "$SECRET",
"iframe_referer": "https://localhost:8082/",
"tunl_sandbox": true,
"allow_client_side_sdk": true,
"additional_vault_providers": [
{
"provider": "repayonline",
"api-version": "1",
"rg-api-secure-token": "xxxxxxx",
"rg-api-user": "User_Name",
"sandbox": true
},
{ ... another provider },
{ ... another provider }
],
+ "payment_data": {
+ "autovault": "N"
+ }
}
EOF
Additional vault providers will be processed in the order that they appear in the additional_vault_providers
array.
Make sure the request to the get-card-form-url.php
contains all the following required properties:
- api_key
- secret
- iframe_referer
Error: call to URL https://test-payment.tunl.com/embed/get-card-form-url.php failed with status 401, response Bad API Key and Secret Combo, curl_error , curl_errno 0
Make sure that you have typed in your api key and secret correctly. Additionally, if you are using an API Key and Secret that was created using a Tunl Test Account (from https://test.tunl.com) then you will need to set the tunl_sandbox
option to true
$tunl_form_options = array(
"api_key" => "apikey_xxxxxxxxxxxxxxxxxxxxxxxxxxx",
"secret" => "xxxxxxxxxxxxxxxxxxxxxxxxxx",
"tunl_sandbox" => true, // set this if using a test tunl account api keys
"allow_client_side_sdk" => true
);
This typically happens when trying to use the generated URL incorrectly. If you generate the URL and use it immediately in an iframe, this should never happen. The generated URL employs the use of a one-time-use-code
that is unique and expires in 1 minute.
Scenarios that an Unauthorized
error would typically happen:
- Attempting to use the generated URL more than once
- Attempting to use a one-time-use-code that does not exist
- Not using the generated URL (or one-time-use-code) within 1 minute
Example generated URL for reference: https://test-payment.tunl.com/embed/load-embedded-form.php?one-time-use-code=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
This message occurs when trying to access the embedded form page directly or from a domain that has not been authorized by the iframe_referer
parameter.
Refused to frame 'https://payment.tunl.com/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors https://localhost:8082/".
This can occur when the iframe_referer
is not set properly. Make sure this option is set the the domain that will be hosting the iframe. This will be a domain that you own.
$tunl_form_options = array(
...
"iframe_referer" => "https://your.domain.com",
...
);
This message can occur for the same reasons as the previous item. The iframe_referer
is likely not set correctly.
These messages are usually triggered by the tunl gateway. There should be an additional message to help clue the user as the why the authentication failed. These are usually things like:
- DECLINED
- UNSUPPORTED CARD TYPE
- EXPIRATION DATE MUST BE IN FUTURE
- BAD CID
The list of possible messages here is the top 4, with DECLINED being the most common. You can view more information about these failures in the Tunl Application under Reports->Failed: https://test.tunl.com/payments/failed
If your webhook responds with anything else other than 200
this message will be displayed to the user. It is recommended to setup some error handling and logging in your web_hook so that you can review what might have happened in these situations.