Docs
SDKs & Applications
Tilled.js

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 });
PARAMETERTYPEDESCRIPTIONDEFAULT
publishableKeystringYour publishable API key.None
accountIdstringYour account ID.None
optionsobjectAdditional settings for the Tilled instance.None
options.sandboxbooleanIndicates 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' });
PARAMETERTYPEDESCRIPTIONDEFAULT
payment_method_typestringSpecifies 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');
    
PARAMETERTYPEREQUIREDDESCRIPTION
selectorstring or DOM ElementYesCSS 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');
PARAMETERTYPEDESCRIPTIONDEFAULT
formFieldTypestringType of the form field (e.g., cardNumber, cardExpiry).None
optionsobjectAdditional settings for the field's customization.None
selectorstring/objectCSS selector or DOM element for embedding the field.None
stylesobjectCSS styles to apply to the field.None
placeholderstringText hint that appears in the field.Depends on field type

Form Field styles

CSS PROPERTYTYPEDESCRIPTION
colorstringSets the text color.
opacitystringSets the opacity level.
letterSpacingstringControls spacing between characters.
textAlignstringAligns text horizontally.
textIndentstringIndents the first line of text.
textDecorationstringApplies text decoration.
textShadowstringAdds shadow to text.
fontstringSets the font style.
fontFamilystringSpecifies the font family.
fontSizestringSets the size of the font.
fontStylestringDefines the style of the font.
fontWeightstringSets the weight of the font.
lineHeightstringSpecifies the line height.
transitionstringDefines 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);
  }
});
EVENTTRIGGER CONDITIONDESCRIPTION
blurFired when a field loses focus.Useful for validation and UI updates.
focusFired when a field gains focus.Useful for highlighting and UI enhancements.
changeFired when the value of a field changes.Useful for real-time validation and feedback.
readyFired 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

METHODDESCRIPTIONPARAMETERSRETURNS
tilled.form()Initializes a new form instance.payment_method_type: Specifies the type of payment methodForm object
form.createField(fieldType, options)Creates a new form field of the specified type.fieldType: Type of field
options: { selector, styles, placeholder }
FormField object
field.inject(selector)Injects the form field into the specified DOM element.selector: CSS selector or DOM elementFormField instance for chaining
form.build()Prepares all fields within the form for user interaction.NonePromise (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();
PARAMETERTYPEREQUIREDDESCRIPTION
handlerfunctionNoOptional 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');
			}
		);
});