Tilled.js
Tilled.js allows you to seamlessly integrate a payment form into your application and securely store payment information on Tilled's remote servers, minimizing your PCI compliance burden.
Implement Tilled.js
Include Tilled.js in your webpage
To start, embed the Tilled.js script directly in the <head>
tag of your HTML to ensure it loads before your page content. You may use the defer
attribute to avoid blocking the parser; however, use of the async
attribute is discouraged. For PCI compliance, Tilled.js must be loaded directly from the URL provided and not included in any bundles.
<script src="https://js.tilled.com/v2" defer></script>
Initialize Tilled.js
Instantiate Tilled.js using your publishable API key, the account_id
, and the initialization options.
const {{ACCOUNT_HEADER}} = new {{COMPANY_NAME}}('pk_…', 'acct_…', { sandbox: true });
PARAMETER | TYPE | DESCRIPTION | DEFAULT |
---|---|---|---|
publishableKey | string | Your publishable API key. | None |
accountId | string | Your account ID. | None |
options | object | Additional settings for the Tilled instance. | None |
options.sandbox | boolean | Indicates whether to use the sandbox environment. | false |
Create the Form
Initialize your main Form instance where you'll add all the form fields. This form acts as the container for all subsequent fields.
const form = await {{ACCOUNT_HEADER}}.form({ payment_method_type: 'card' });
PARAMETER | TYPE | DESCRIPTION | DEFAULT |
---|---|---|---|
payment_method_type | string | Specifies the payment_method_type , either card or ach_debit . | card |
Create and inject Form Fields
Create each Form Field individually using form.createField()
and then inject them into your HTML using either a CSS selector or a DOM element.
const cardNumberField = form.createField('cardNumber');
cardNumberField.inject('#card-number-container');
PARAMETER | TYPE | REQUIRED | DESCRIPTION |
---|---|---|---|
selector | string or DOM Element | Yes | CSS selector or DOM element where the field should be injected. |
Configure Field Styles (optional)
Customize the appearance of your form fields using CSS properties. This step is optional but recommended to maintain a consistent design across your software.
const fieldOptions = {
styles: {
base: {
fontFamily: 'Helvetica Neue, Arial, sans-serif',
color: '#30416e',
fontWeight: '400',
}
}
};
const styledCardNumberField = form.createField('cardNumber', fieldOptions);
styledCardNumberField.inject('#card-number-container');
PARAMETER | TYPE | DESCRIPTION | DEFAULT |
---|---|---|---|
formFieldType | string | Type of the form field (e.g., cardNumber , cardExpiry ). | None |
options | object | Additional settings for the field's customization. | None |
selector | string/object | CSS selector or DOM element for embedding the field. | None |
styles | object | CSS styles to apply to the field. | None |
placeholder | string | Text hint that appears in the field. | Depends on field type |
Form Field styles
CSS PROPERTY | TYPE | DESCRIPTION |
---|---|---|
color | string | Sets the text color. |
opacity | string | Sets the opacity level. |
letterSpacing | string | Controls spacing between characters. |
textAlign | string | Aligns text horizontally. |
textIndent | string | Indents the first line of text. |
textDecoration | string | Applies text decoration. |
textShadow | string | Adds shadow to text. |
font | string | Sets the font style. |
fontFamily | string | Specifies the font family. |
fontSize | string | Sets the size of the font. |
fontStyle | string | Defines the style of the font. |
fontWeight | string | Sets the weight of the font. |
lineHeight | string | Specifies the line height. |
transition | string | Defines transition effects. |
Add event listeners to handle user interactions
Attach event listeners to your form fields to handle actions like input validation or dynamic updates.
cardNumberField.on('change', (event) => {
if (event.valid) {
console.log('Card number is valid');
} else {
console.log('Card number is invalid:', event.error);
}
});
EVENT | TRIGGER CONDITION | DESCRIPTION |
---|---|---|
blur | Fired when a field loses focus. | Useful for validation and UI updates. |
focus | Fired when a field gains focus. | Useful for highlighting and UI enhancements. |
change | Fired when the value of a field changes. | Useful for real-time validation and feedback. |
ready | Fired when a field is fully rendered and ready for interaction. | Indicates readiness for user input. |
Build the Form
Once all fields are created and configured, build the Form
.
await form.build();
Methods
METHOD | DESCRIPTION | PARAMETERS | RETURNS |
---|---|---|---|
tilled.form() | Initializes a new form instance. | payment_method_type : Specifies the type of payment method | Form object |
form.createField(fieldType, options) | Creates a new form field of the specified type. | fieldType : Type of fieldoptions : { selector , styles , placeholder } | FormField object |
field.inject(selector) | Injects the form field into the specified DOM element. | selector : CSS selector or DOM element | FormField instance for chaining |
form.build() | Prepares all fields within the form for user interaction. | None | Promise (resolves when complete) |
form.teardown() | Removes all fields from the DOM and cleans up resources. | handler : Optional callback to execute after teardown is complete. | Promise (resolves to a boolean) |
Create a payment intent
To Create a Payment IntentAPI, specify the necessary details such as currency
, payment_method_type
, and amount
.
curl -L '{{PRODUCTION_API_URL}}/v1/payment-intents' \
-H '{{ACCOUNT_HEADER}}-account: {{MERCHANT_ACCOUNT_ID}}' \
-H 'Content-Type: application/json' \
-H '{{ACCOUNT_HEADER}}-api-key: {{SECRET_KEY}}' \
-d '{
"amount": 1000,
"currency": "usd",
"payment_method_types": [
"card"
],
"confirm": true,
"payment_method_id": "pm_wAv1DzFajFHS50zIoT2N1"
}'
This server-side step generates a client_secret
which will be used to complete the transaction on the client side with Tilled.js.
Collect payment method details using Tilled.js (optional)
Use Tilled.js to securely collect payment details on the client side. If the payment method will be reused in the future, you can store it using the Attach the Payment Method to a CustomerAPI endpoint.
tilled
.createPaymentMethod({
type: 'card',
billing_details: {
name: 'John Doe',
address: {
zip: '80021',
country: 'US',
},
},
})
.then(
(paymentMethod) => {
// attach to customer before confirming the payment
// confirm payment
tilled.confirmPayment(paymentIntentClientSecret, paymentMethod);
},
(error) => {
// An error with the request (>400 status code)
}
);
Confirm the payment intent
In order to confirm a payment intent, the payment method details or a payment_method_id
must be specified. You can confirm at creation by setting confirm=true
in the request body.
If the payment intent status is requires_payment_method
, you'll need to attach a payment method using the Confirm a Payment IntentAPI endpoint.
tilled.confirmPayment(paymentIntentClientSecret, {
payment_method: { // Can also be a PaymentMethod ID (paymentMethod.id)
type: 'card',
billing_details: {
name: 'John Doe',
address: {
zip: '80021',
country: 'US',
},
},
}
})
.then(
(paymentIntent) => {
// Be sure to check the 'status' and/or 'last_error_message'
// properties to know if the charge was successful
},
(error) => {
// Typically an error with the request (>400 status code)
}
);
Teardown when done
Remove your Form
from the DOM and clean up resources when the Form
is no longer needed.
await form.teardown();
PARAMETER | TYPE | REQUIRED | DESCRIPTION |
---|---|---|---|
handler | function | No | Optional callback to execute after teardown is complete. |
Examples
Credit card form
/* Example assumptions:
* The card fields have divs defined in the DOM
* <div id="card-number-element"></div>
* <div id="card-expiration-element"></div>
* <div id="card-cvv-element"></div>
* A submit button is defined
* <button id='submit-btn'></button>
*/
const tilled = new Tilled('pk_...', 'acct_...');
const form = await tilled.form({
payment_method_type: 'card',
});
const fieldOptions = {
styles: {
base: {
fontFamily: 'Helvetica Neue, Arial, sans-serif',
color: '#304166',
fontWeight: '400',
fontSize: '16px',
},
invalid: {
':hover': {
textDecoration: 'underline dotted red',
},
},
valid: {
color: '#00BDA5',
},
},
};
form.createField('cardNumber', fieldOptions).inject('#card-number-element');
form.createField('cardExpiry', {...fieldOptions, selector: '#card-expiration-element'});
form.createField('cardCvv', fieldOptions).inject('#card-cvv-element');
await form.build();
const submitButton = document.getElementById('submit-btn');
submitButton.onclick = () => {
tilled.confirmPayment(paymentIntentClientSecret, {
payment_method: {
billing_details: {
name: 'John Doe',
address: {
zip: '80021',
country: 'US',
},
},
},
})
.then((paymentIntent) => {
if (paymentIntent.status === 'succeeded') {
alert('Payment successful');
} else {
const errMsg = paymentIntent.last_payment_error?.message;
alert('Payment failed: ' + errMsg);
}
})
.catch((error) => {
alert('An error occurred: ' + error.message);
});
ACH bank account form
/**
* Example assumptions:
* The ach_debit fields have divs defined in the DOM
* <div id="bank-account-number-element"></div>
* <div id="bank-routing-number-element"></div>
*
* A submit button is defined
* <button id='submit-btn'></button>
*/
const tilled = new Tilled('pk_…', 'acct_…');
const form = await tilled.form({
payment_method_type: 'ach_debit',
});
const fieldOptions = {
styles: {
base: {
fontFamily: 'Helvetica Neue, Arial, sans-serif',
color: '#304166',
fontWeight: '400',
fontSize: '16px',
},
invalid: {
':hover': {
textDecoration: 'underline dotted red',
},
},
valid: {
color: '#00BDA5',
},
},
};
form
.createField('bankAccountNumber', fieldOptions)
.inject('#bank-account-number-element');
form
.createField('bankRoutingNumber', fieldOptions)
.inject('#bank-routing-number-element');
await form.build();
const submitButton = document.getElementById('submit-btn');
submitButton.on('click', () => {
// A payment intent will be created on your backend server and the
// payment_intent.client_secret will be passed to your frontend to
// be used below.
tilled
.confirmPayment(paymentIntentClientSecret, {
payment_method: {
billing_details: {
name: 'John Doe',
address: {
street: '370 Interlocken Blvd',
city: 'Broomfield',
state: 'CO',
zip: '80021',
country: 'US',
},
},
ach_debit: {
account_type: 'checking',
account_holder_name: 'John Doe',
},
},
})
.then(
(paymentIntent) => {
// Be sure to check the `status` and/or `last_payment_error`
// properties to know if the charge was successful
if (
paymentIntent.status === 'succeeded' ||
paymentIntent.status === 'processing'
) {
alert('Payment successful');
} else {
const errMsg = paymentIntent.last_payment_error?.message;
alert('Payment failed: ' + errMsg);
}
},
(err) => {
// Typically an error with the request (>400 status code)
}
);
});
Apple Pay
/**
* Example assumptions:
* The paymentRequestButton field has a div defined in the DOM
* <div id="native-payment-element"></div>
*
*/
const form = tilled.form({
payment_method_type: 'card',
});
const paymentRequest = tilled.paymentRequest({
total: {
label: 'Tilled tee',
amount: secretData.amount,
},
});
const prButton = form.createField('paymentRequestButton', {
paymentRequest: paymentRequest,
});
paymentRequest.canMakePayment().then((result) => {
if (result) {
prButton.inject('#native-payment-element');
} else {
document.getElementById('native-payment-element').style.display = 'none';
}
});
paymentRequest.on('paymentmethod', (ev) => {
let paymentMethod = ev.paymentMethod;
tilled
.confirmPayment(paymentIntentClientSecret, {
payment_method: paymentMethod.id,
})
.then(
(paymentIntent) => {
// The payment intent confirmation occurred, but the
// actual charge may still have failed. Check
if (
paymentIntent.status === 'succeeded' ||
paymentIntent.status === 'processing'
) {
ev.complete('success');
alert('Successul payment');
} else {
ev.complete('fail');
const errMsg = paymentIntent.last_payment_error?.message;
alert('Payment failed: ' + errMsg);
}
},
(err) => {
ev.complete('fail');
}
);
});