Dyneti Checkout Form Builder
Dyneti Checkout is the only hosted payment page that keeps your application out of PCI scope while delivering a built-in credit card scanner that increases conversion by up to 26%. Dyneti Checkout is compatible with any payment processor.
Prerequisites
- Request a Shareable API key from Dyneti.
- Provide the domains on which the integration will be hosted to Dyneti.
- Add the DyScan Form Builder client script to your page
<script src="https://dyscanweb.dyneti.com/static/front_end/dist/dyneti-checkout-client.js"></script>
If you're integrating with Worldpay, you will need to provide Dyneti with the Worldpay user/password pair you would like to use to tokenize the card number. Dyneti will provide you a Payment Token API key. Alternatively, if you're integrating with VGS, you will need to set up a vault and have a Vault ID.
1. Create the Form Client
const DyScanForms = window.DyScanForms;
// configured to send the results to Worldpay.
const client = new DyScanForms.Form({
apiKey: "YOUR_SHAREABLE_API_KEY",
stateCallback: (state) => console.log(`Got state: ${JSON.stringify(state)}`),
scanConfig: {
uiVersion: 2,
showExplanation: false,
showResult: false
showCancelButton: true,
},
worldpayConfig: {
customerId: "<customerId>",
id: "<id>",
reportGroup: "<reportGroup>",
},
// optional: Custom scan behavior callbacks
onScanAction: (startScan) => {
// custom logic before scan starts
console.log('Scan starting...');
// you can show custom UI here
startScan(); // call this to actually start the scan
},
onScanComplete: () => {
// custom logic after scan completes
console.log('Scan completed');
// hide custom UI, detach the collector
},
});
See here for all the options that can be passed to the scanConfig
object. The above example shows integration with Worldpay. To integrate instead with VGS, see here.
2. Create the Form Layout
First create an HTML element that you want to use as a form field. For example, you can create a <div>
element to hold the card number or expiration date:
<style>
.main-container {
display: flex;
background: #f0f1f3;
width: 442px;
margin: 0 auto;
flex-direction: column;
padding: 24px;
gap: 16px;
border-radius: 10px;
align-items: center;
}
.form-field {
width: 100%;
height: 40px;
position: relative;
background: white;
border-radius: 4px;
padding: 0 10px;
}
/*
The form fields will be inside secure iframes, so we recommend setting this CSS.
*/
iframe {
width: 100%;
height: 100%;
}
</style>
<div id="collect-form" class="main-container">
<div id="cc-number" class="form-field"></div>
<div id="cc-exp" class="form-field"></div>
<div id="cc-cvv" class="form-field"></div>
</div>
To create a field that will be collected and sent to Dyneti, use the attach()
method of the client created by new DyScanForms.Form()
. For example,
// Assuming you've created a `client` object as in Step 1.
client.attach({
id: "cc-number", // the id of the element to attach to
config: {
type: "cc",
placeholder: "Card number",
},
style: {
showBorder: false,
lineHeight: "20px",
fontSize: "16px",
showCameraIcon: true,
},
});
client.attach({
id: "cc-exp",
config: {
type: "exp",
placeholder: "MM/YY",
},
style: {
showBorder: false, // the fields can be styled independently
},
});
client.attach({
id: "cc-cvv",
config: {
type: "cvv",
placeholder: "CVV",
},
style: {
showBorder: false,
},
});
The following types of fields are supported as text inputs. They are supplied to the attach
as part of config
property.
config.type | Description |
---|---|
"cc" | Credit card number |
"exp" | Expiration date |
"cvv" | Card security code |
"zip" | Postal code |
"name" | Cardholder name |
Alternatively, the expiration date can be supplied via dropdown select menus. To use these, use "exp-month-select"
and "exp-year-select"
as the types.
3. Card Scanning
Any of the text input fields can be configured to have a pressable camera button for card scanning. To enable this, set showCameraIcon
to true
in the style
object of the attach
method. For example
client.attach({
id: "cc-number",
config: {
type: "cc",
},
style: {
showCameraIcon: true,
}
});
We recommend only passing showCameraIcon
on one field.
Alternatively, the client can manually begin the card scan by calling startScan
. The results will be populated into the form.
client.startScan();
Custom Scan UI with Collectors
For more advanced scan integrations, you can bind a scan collector to a specific HTML element and customize the scan experience:
Setting up an element to bind to
<div id="scan-holder" style="display: none;">
<div id="my-scan-view"></div>
</div>
Binding a Scan Collector
// Bind a scan collector to display the camera feed
client.bindCollector({
id: "my-scan-view",
collectorType: "scan-view"
});
Full Custom Scan Integration Example
const client = new DyScanForms.Form({
apiKey: "YOUR_API_KEY",
stateCallback: (state) => console.log('Form state:', state),
onScanAction: (startScan) => {
// show your custom scan UI
document.getElementById('scan-holder').style.display = 'block';
startScan(); // start the actual scan
},
onScanComplete: () => {
// your custom ui code here
document.getElementById('scan-holder').style.display = 'none';
// unbind the collector to stop the camera
client.detachCollector({id: 'scan-holder'})
}
});
// initial collector binding
client.bindCollector({
id: "my-scan-view",
collectorType: "scan-view"
});
Collector Types
collectorType | Description |
---|---|
scan-view | Displays the camera feed as a resizable element |
scan-modal | Show a pop-over modal, intended for full-screen use |
React Cleanup
When your hook which creates the client is done, be sure to call client.destroy()
for the client you created.
useEffect(() => {
const formClient = new DyScanForms.Form({
// ... configuration
})
return ()=> {
// ... other clean up
formClient.destroy()
}
}, []);
Angular Cleanup
In your ngOnDestroy
method, be sure to call client.destroy()
for the client you created.
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html'
})
export class MyComponent implements OnInit, OnDestroy {
private formClient: any;
ngOnInit() {
this.formClient = new DyScanForms.Form({
// ... configuration
});
}
ngOnDestroy() {
// ... other cleanup
if (this.formClient) {
this.formClient.destroy();
}
}
}
4. Form Validation and State Callback
On any change to the form, the client receives a callback with the updated state. Note that to ensure PCI compliance, the state does not contain any form values. The callback is registered as part of the constructor to DyScanForms.Form
:
const client = new DyScanForms.Form({
apiKey: "YOUR_SHAREABLE_API_KEY",
stateCallback: (state) => console.log(`Got state: ${JSON.stringify(state)}`),
});
Here is an example of the state object:
{
valid: false,
focused: null,
issuingNetwork: "VISA"
fields: {
cc: { valid: false, touched: true, dirty: true },
name: { valid: true, touched: true, dirty: true },
exp: { valid: true, touched: true, dirty: true },
cvv: { valid: false, touched: false, dirty: false },
}
}
The state
object always contains a top-level valid
property that is a boolean
. It indicates whether the entire form is ready for submission. Note that form submission will fail unless the top-level valid
is true
.
Each of the objects under the fields
property corresponds to one of the fields of the form. The valid
property indicates whether the field is individually valid. The touched
property indicates whether the user has focused the field at least once. This is also set to true
if the field is automatically filled for example by browser autofill or card scanning. The dirty
property indicates that the value is not empty. Unlike focused
, it can be reset to true
if the user deletes all input in the field.
The top-level focused
property indicates which field currently has the user's focus. For example if they are focused on the credit card field, the state object will have
{
...
focused: "cc",
...
}
The top-level issuingNetwork
property denotes which issuing network issued the card. For example, "VISA", "MASTERCARD", "AMEX".
issuingNetwork
values
type Issuer =
| "AMEX"
| "TUNION"
| "UNIONPAY"
| "DINERSCLUBINTERNATIONAL"
| "DISCOVER"
| "RUPAY"
| "INTERPAYMENT"
| "JCB"
| "MAESTRO"
| "DANKORT"
| "MIR"
| "BORICA"
| "MASTERCARD"
| "TROY"
| "VISA"
| "UATP"
| "VERVE"
| "LANKAPAY"
| "UZCARD"
| "HUMO"
| "GPN"
| "NAPAS"
| "UNKNOWN";
Finally, the state object can have a top-level event
property that is present when the user focuses a field, removes their focus from the field, or presses a button. If they add their focus, the event will look like
{
...
event: {field: "cc", event: "focus"},
...
}
When a field loses focus, the event will be "blur"
. For example when the credit card field loses focus:
{
...
event: {field: "cc", event: "blur"},
...
}
When a button has been pressed the event will be "click"
and the element that was clicked will be specified. For instance, if the camera button that triggers DyScan is pressed:
{
...
event: {field:"cc",event:"click",element:"cameraIcon"},
...
}
5. Form Submission
To submit your form, use the submit
method. It returns a Promise
that must be awaited to access the results.
const results = await client.submit();
The results object always has a submissionSuccessful
property with boolean
value and a formId
property that is a UUID as a string
:
{
submissionSuccessful: false,
formId: "92eb501d-3111-427d-a64c-b5ccd030bb22",
}
In the case of a successful form submission, the results object will also include the last four digits of the card number, the expiration month, and the expiration year as lastFour
, expMonth
, and expYear
respectively:
{
submissionSuccessful: true,
lastFour: "4142",
expMonth: "09",
expYear: "28",
formId: "92eb501d-3111-427d-a64c-b5ccd030bb22",
}
Note that lastFour
, expMonth
, and expYear
are all strings, and the expiration month and year are given with two characters. See below for submission results when integrating with Worldpay.
6. Worldpay Integration
Configuring the Scan
To configure the scan to use Worldpay for payment processing, first provide Dyneti with the Worldpay user/password pair you would like to use to tokenize the card number. Then, simply pass a worldpayConfig
object with your desired customerId
, id
, and reportGroup
:
const DyScanForms = window.DyScanForms;
const client = new DyScanForms.Form({
apiKey: "YOUR_SHAREABLE_API_KEY",
stateCallback: (state) => console.log(`Got state: ${JSON.stringify(state)}`),
scanConfig: {
uiVersion: 2,
showExplanation: false,
showResult: false
},
worldpayConfig: {
customerId: "<customerId>",
id: "<id>",
reportGroup: "<reportGroup>",
},
});
Receiving Worldpay metadata in the front end
For scans configured to use Worldpay, the results of await client.submit()
have additional information. Upon successful scan submission, the client will return the following information from Worldpay, if available.
property | Description | Type |
---|---|---|
submissionSuccessful | Whether form was successfully submitted to Dyneti | boolean |
formId | A UUID identifying the form | string |
firstSix | The first six digits of the payment card | string |
lastFour | The last four digits of the payment card | string |
expMonth | The expiry month of the payment card as a two-character string | string |
expYear | The expiry year of the payment card as a four-character string | string |
litleTxnId | The transaction ID from the Worldpay submission | string |
responseCode | The response code from the Worldpay submission | string |
Note that Worldpay returns expiration years as four-character strings. Here is an example of the results as JSON,
{
"submissionSuccessful": true,
"formId": "a6b1441a-d7af-4ec7-8799-697a5447083c",
"firstSix": "403446",
"lastFour": "6679",
"expMonth": "01",
"expYear": "2029",
"cardType": "VI",
"litleTxnId": "839354183362067",
"responseCode": "802"
}
Exchanging the Form ID for a Worldpay Token
After successfully submitting a scan, the returned formId
can be used to obtain a Worldpay token for payment processing. Make a request to Dyneti's server from your backend server to exchange the Dyneti Form ID for a Worldpay token.
import requests
PAYMENT_TOKEN_API_KEY = 'example-key-abcd-1234'
def exchange_token(dyneti_token):
response = requests.get(f'https://dyscanweb.dyneti.com/api/v1/worldpay/form/{dyneti_token}', headers={
'X-API-KEY': PAYMENT_TOKEN_API_KEY
})
worldpay_token = response.json().get('token')
return worldpay_token
- Python
- Node
- curl
import requests
form_id = '3d581785-2be8-44da-b6bf-7757e7553537'
endpoint = 'https://dyscanweb.dyneti.com/api/v1/worldpay/form/%s' % form_id
payment_token_api_key = 'example_0123456789u0ALMrmV2RXa7YZMdZjAOoTxjM3EbVnRz6SQ5TFzY1'
r=requests.get(
endpoint,
headers={
"X-API-KEY": payment_token_api_key
}
)
r.json()
const axios = require('axios');
const formId = '3d581785-2be8-44da-b6bf-7757e7553537';
const endpoint = 'https://dyscanweb.dyneti.com/api/v1/worldpay/form/' + formId;
const paymentTokenApiKey = 'example_0123456789u0ALMrmV2RXa7YZMdZjAOoTxjM3EbVnRz6SQ5TFzY1';
axios.get(endpoint, {
headers: {
'X-API-KEY': paymentTokenApiKey
}
}).then(response => {
console.log(response.data);
}).catch(error => {
console.error(error);
});
Response
{
"token": "exampleTokenResponseValue"
}
curl -X GET \
-H "X-API-KEY: example_0123456789u0ALMrmV2RXa7YZMdZjAOoTxjM3EbVnRz6SQ5TFzY1" \
"https://dyscanweb.dyneti.com/api/v1/worldpay/form/3d581785-2be8-44da-b6bf-7757e7553537"
No OpenAPI specification URL provided
7. VGS Integration
To integrate with VGS, pass your vault id and whether you're in sandbox or production mode to the client constructor:
const client = new DyScanForms.Form({
apiKey: "YOUR_DYNETI_API_KEY",
vgsConfig: { vaultId: "YOUR_VGS_VAULT_ID", mode: "sandbox" }, // or "production"
/* other configuration */
});
Then attach fields. The example below assumes there is an HTML element with id cc-number
as in the example above:
client.attach({
id: "cc-number",
config: {
type: "cc",
placeholder: "Card number",
},
style: {
showBorder: false,
lineHeight: "20px",
fontSize: "16px",
},
vgsOptions: {
serialization: "card_number",
},
});
The serialization key can be specified as part of the vgsOptions
object in the attach
method. This controls the key for the field when it is sent to VGS as JSON. In this example, specifying serialization
as "card_number"
will cause the ultimate request to your VGS vault to look like:
{
"card_number": "<card_number_to_be_tokenized>"
}
Additional Submit Options
When integrating with VGS, you may pass an optional object to client.submit()
. These allow customization of the path, headers, and HTTP method used when submitting to your VGS vault. Additionally, extra data can be included in the payload sent to VGS by specifying a data
callback. For example,
const dataMergeCallback = (formData: Record<string, string>) => {
return { extraField: "extra data", checkoutData: {...formData} }
};
const results = await client.submit({
path: "/custom-path",
method: "PUT",
headers: { "X-CUSTOM": "custom-data" },
data: dataMergeCallback,
});
The option object passed to submit supports the following fields.
property | Description | Type |
---|---|---|
path | The path component for your vault URL. Defaults to "/post" . | string |
method | The HTTP method for the request to your VGS vault. Defaults to "POST" | string |
headers | Custom headers to add to the request to your VGS vault. | Record<string, string> |
data | Callback that received an object whose keys are the attached fields. The object returned will be merged with the form data when submitted to VGS. | (Record<string, string>) => object |
The object that the data
callback receives will have keys equal to the serialization keys of the attached fields. For example if a credit card and name field have been attached with
client.attach({
id: "cc-number",
config: {
type: "cc",
placeholder: "Card number",
},
vgsOptions: {
serialization: "card_number",
},
});
client.attach({
id: "cc-cvv",
config: {
type: "cvv",
placeholder: "CVV",
},
vgsOptions: {
serialization: "cvv",
},
});
then, the object will have keys "card_number"
and "cvv"
. The values are not specified, but they will never contain sensitive data. An example of such an object is
{
"card_number":"__dynetiPlaceholder/card_number",
"cvv":"__dynetiPlaceholder/cvv",
}
Note that this object only reflects the fields attached with the attach
and detach
methods. Use the state callback to check for valid data.
VGS Submission Response
When submitting to VGS, the response from the upstream server will be returned to the client from client.submit()
. The data from the generic form submission such as formId
will be present alongside the upstreamResponse
which has the following properties.
property | Description | Type |
---|---|---|
body | The body of the upstream response. | string |
status | The HTTP status code for the upstream response. If there is an error before submitting to upstream (e.g., 409 for a previously submitted form or 403 for bad API key), that status code will be returned instead. | number |
ok | Boolean indicating whether the response code is in the range 200-299 . | boolean |
contentType | The content-type header from the upstream response. | string |
For example if the response from your backend server through VGS's proxy has a JSON body, you can access it with
const results = await client.submit({
path: "/custom-path",
method: "PUT",
headers: { "X-CUSTOM": "custom-data" },
data: dataMergeCallback,
});
const responseBody = JSON.parse(results?.upstreamResponse?.body);
8. Style Options
The style
object used in the attach
method of the client supports the following options
property | Description | Type |
---|---|---|
showBorder | Whether to show the border of the input field | boolean |
fontSize | Sets the font-size CSS property | string |
fontColor | Sets the color of the text | string |
successColor | Sets the color of the text when the field input is valid | string |
lineHeight | Sets the line-height CSS property | string |
showCameraIcon | Whether to show a camera icon for card scanning | boolean |
9. Postal Code Validation
The zip
field can be validated by setting the the validations
property on the config
object. For example
// showing VGS configuration but works with other backends
client.attach({
id: "cc-zip",
config: {
type: "zip",
placeholder: "Zip",
validations: ["postal_code/us"],
},
style: {
showBorder: false,
},
vgsOptions: {
serialization: "postal_code",
},
});
The validations
property is an array of strings. Currently supported are "postal_code/us"
for the United States and "postal_code/ca"
for Canada.
10. Removing fields
To remove an element from a form, use the detach
method of the client. To specify the field, use the HTML ID that the field was originally attached to. For example,
// create a credit card field. It will be unaffected by the detach call.
client.attach({
id: "cc-number",
config: {
type: "cc",
placeholder: "Card number",
},
style: {
showBorder: false,
showCameraIcon: true,
},
vgsOptions: {
serialization: "cc",
},
});
})
// attaching to an HTML element with ID "cc-zip"
// after this, the form will have two fields.
client.attach({
id: "cc-zip",
config: {
type: "zip",
placeholder: "Zip",
validations: ["postal_code/us"],
},
style: {
showBorder: false,
},
});
// now detach it. After this call, the form will only have a credit card field.
client.detach({
id: "cc-zip",
})
This can be used to change the validations for postal codes. For example,
client.attach({
id: "cc-number",
config: {
type: "cc",
placeholder: "Card number",
},
style: {
showBorder: false,
showCameraIcon: true,
},
vgsOptions: {
serialization: "cc",
},
});
})
// attach a US zip code field.
// after this, the form will have a credit card field and a US zip code field.
client.attach({
id: "cc-zip",
config: {
type: "zip",
placeholder: "Zip",
validations: ["postal_code/us"],
},
style: {
showBorder: false,
},
});
// detach the US zip field. After the detach call, the form will only have a credit card field.
client.detach({
id: "cc-zip",
})
// now attach a Canadian postal code to the same HTML element
// after this, the form will have a credit card field and a Canadian postal code field.
client.attach({
id: "cc-zip",
config: {
type: "zip",
placeholder: "Zip",
validations: ["postal_code/ca"], // note different validation for Canadian postal codes.
},
style: {
showBorder: false,
},
});