Skip to main content

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

  1. Request a Shareable API key from Dyneti.
  2. Provide the domains on which the integration will be hosted to Dyneti.
  3. 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.typeDescription
"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

collectorTypeDescription
scan-viewDisplays the camera feed as a resizable element
scan-modalShow 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.

propertyDescriptionType
submissionSuccessfulWhether form was successfully submitted to Dynetiboolean
formIdA UUID identifying the formstring
firstSixThe first six digits of the payment cardstring
lastFourThe last four digits of the payment cardstring
expMonthThe expiry month of the payment card as a two-character stringstring
expYearThe expiry year of the payment card as a four-character stringstring
litleTxnIdThe transaction ID from the Worldpay submissionstring
responseCodeThe response code from the Worldpay submissionstring

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
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()

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.

propertyDescriptionType
pathThe path component for your vault URL. Defaults to "/post".string
methodThe HTTP method for the request to your VGS vault. Defaults to "POST"string
headersCustom headers to add to the request to your VGS vault.Record<string, string>
dataCallback 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.

propertyDescriptionType
bodyThe body of the upstream response.string
statusThe 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
okBoolean indicating whether the response code is in the range 200-299.boolean
contentTypeThe 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

propertyDescriptionType
showBorderWhether to show the border of the input fieldboolean
fontSizeSets the font-size CSS propertystring
fontColorSets the color of the textstring
successColorSets the color of the text when the field input is validstring
lineHeightSets the line-height CSS propertystring
showCameraIconWhether to show a camera icon for card scanningboolean

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,
},
});