This guide shows how to build a card form to take a payment using the Payment Intents API, Dots Elements and React. You can also clone the example project and run it locally.

1. Setup your Dots Account server-side

First, create a Dots account or sign in.

2. Create a Payment Customer server-side optional

You may skip this step if you do not intend to integrate payment method reusability for your customers.

When a payment is created, a payment method is generated and saved to the PaymentIntent object. This payment method is only capable of being used once (i.e. single-use) unless it is attached to a PaymentCustomer, in which case it becomes reusable.

Therefore, to setup future usage, create a PaymentCustomer object on your server before creating the payment and save the id to your database. This will used in the following step. An alternative is to create a Dots User which will automatically back populates a payment customer on Dots end and use the user.id instead of payment_customer.id.

cURL
https://pls.senddotssandbox.com/api/v2/payment-customers/ \
-X POST \
-u "CLIENT_ID:API_KEY" \
-H "Content-Type: application/json" \
-d "{
      "first_name": "John",
      "last_name": "Hardy",
      "email": "john_hardy@gmail.com",
      ...
    }"

> {
  "id": "0e835300-5555-4ba1-816a-0abaa8609a83",
  ...
}

3. Create a Payment Intent server-side

Dots uses a PaymentIntent object to represent your intent to collect payment from a customer, tracking charge attempts and payment state changes throughout the process.

Create a PaymentIntent on your server with an amount and currency. Always decide how much to charge on the server side, a trusted environment, as opposed to the client. This prevents malicious customers from being able to choose their own prices.

If you intend to save the payment method captured by this payment intent for future usage, you must pass in the customer_id or user_id below and set setup_future_usage to on_session.

https://pls.senddotssandbox.com/api/v2/payment-intents/ \
-X POST \
-u "CLIENT_ID:API_KEY" \
-H "Content-Type: application/json" \
-d "{
      "amount": 1000,
      "currency": "usd",
      "transfer_data": {
        "destination_user_id": "0a7d51ec-04df-4eb8-a2c8-d066f6d3bb81"
      },
      "application_fee_amount": 100 //fee to be paid to the platform
    }"

> {
  "client_secret": "...",
  "id": "0e835300-5555-4ba1-816a-0abaa8609a83",
  ...
}

The returned PaymentIntent includes a client secret. The client side uses the client secret to securely complete the payment process instead of passing the entire PaymentIntent object. There are different approaches that you can use to pass the client secret to the client side.

You can retrieve the client secret from an endpoint on your server using the browser’s fetch function on the client side. This approach is generally most suitable when your client side is a single-page application, particularly one built with a modern frontend framework such as React. The following example shows how to create the server endpoint that serves the client secret:

server.ts
app.post('/create-payment-intent', async (req, res) => {
  const body = req.body;

  const response = await axios.post(
    process.env.DOTS_API_URL + '/v2/payment-intents',
    {
      currency: 'usd',
      amount: body.amount,
    },
    {
      withCredentials: true,
      auth: {
        username: process.env.DOTS_CLIENT_ID,
        password: process.env.DOTS_CLIENT_SECRET,
      },
    }
  );
  res.json(response.data);
});

This example demonstrates how to fetch the client secret with TypeScript on the client side:

app.ts
const createPaymentIntent = async (amount: number) => {
  const res = await axios.post('/create-payment-intent', {
    currency: 'usd',
    amount,
  });

  return res.data.client_secret;
};

4. Collect card details client-side

You’re ready to collect card information on the client with Dots Elements. Elements is a set of prebuilt UI components for collecting and validating card number and expiration date.

Setup Dots Elements

Install React Dots.js and the Dots.js loader from the npm public registry.

Terminal
npm install --save @dots.dev/react-dots-js @dots.dev/dots-js

Add Dots.js and Elements to your page

To use Element components, wrap the root of your React app in an Elements provider. Call loadDots with your publishable key and pass the returned Promise to the Elements provider.

app.ts
import React from 'react';
import ReactDOM from 'react-dom';
import { loadDots } from '@dots.dev/dots-js';
import { Elements } from '@dots.dev/react-dots-js';

import CheckoutForm from './CheckoutForm';

// Make sure to call `loadDots` outside of a component’s render to avoid
// recreating the `Dots` object on every render.

const dotsPromise = loadDots(
  'pk_prod_pMpPyn6IRnwibuUqqHaFS938RiLMj',
  'sandbox'
);

function App() {
  return (
    <Elements dots={dotsPromise}>
      <CheckoutForm />
    </Elements>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

Add and configure a PaymentElement component

import { PaymentElement, useDots, useElements } from '@dots.dev/react-dots-js';
import countryList from 'react-select-country-list';
import React, { useEffect, useMemo, useState } from 'react';

export default function CheckoutForm() {
  ...
  const {
    register,
    handleSubmit,
    watch,
    reset,
    formState: { errors },
  } = useForm();

  const countryOptions = useMemo(() => countryList().getData(), []);

  const renderForm = () => {
    const fieldOptions = {
      styles: {
        base: {
          fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
          fontSmoothing: 'antialiased',
          color: '#32325d',
          fontWeight: '400',
          fontSize: '16px',
        },
        invalid: {
          ':hover': {
            textDecoration: 'underline dotted red',
          },
          color: '#fa755a',
        },
        valid: {
          color: '#32CD32',
        },
      },
    };

    return (
      <form onSubmit={handleSubmit(onSubmit)}>
        <h1>
          USD $
          {(amount / 100).toLocaleString(navigator.language, {
            minimumFractionDigits: 2,
          })}{' '}
        </h1>
        <br />
        <PaymentElement options={fieldOptions} />
        <div className="sr-combo-inputs">
          <div className="sr-combo-inputs-row">
            <input
              type="text"
              placeholder="Full Name"
              autoComplete="name"
              className="sr-input"
              {...register('name', { required: true })}
            />
            <input
              type="text"
              placeholder="12345"
              autoComplete="postal-code"
              className="sr-input"
              {...register('zip', { required: true })}
            />

            <select
              defaultValue="US"
              className="sr-select"
              {...register('country', { required: true })}
            >
              {countryOptions.map((country: any, index: number) => (
                <option key={index.toString()} value={country.value}>
                  {country.label}
                </option>
              ))}
            </select>
          </div>
        </div>

        {error && <div className="message sr-field-error">{error}</div>}

        <button className="btn" disabled={processing}>
          {processing ? 'Processing…' : 'Pay'}
        </button>
      </form>
    );
  };
  ...
}

Elements are completely customizable. You can style Elements to match the look and feel of your site, providing a seamless checkout experience for your customers. It’s also possible to style various input states, for example when the Element has focus.

Always collect a postal code to increase card acceptance rates and reduce fraud.

5. Submit payments to Dots client-side

Rather than sending the entire PaymentIntent object to the client, use its client secret from step 3. After you create the PaymentIntent, the API returns a unique client secret for it (the client secret is different from the secret key you use to make Dots API requests).

The client secret can complete a charge, so handle it with discretion. Don’t log it, embed it in URLs, or expose it to anyone but the customer.

To complete the payment when the user clicks, retrieve the client secret from the PaymentIntent you created in step two and call dots.confirmCardPayment with the client secret and the Element. Pass additional billing details, such as the cardholder name and address, to the billing_details hash.

To call dots.confirmCardPayment from your payment form component, use the useDots and useElements hooks.

CheckoutForm.ts
import React, { useEffect, useMemo, useState } from 'react';
import { PaymentElement, useDots, useElements } from '@dots.dev/react-dots-js';
import './CheckoutForm.css';

export default function CheckoutForm() {
  const [error, setError] = useState(null);
  const [metadata, setMetadata] = useState<any | null>();
  const [succeeded, setSucceeded] = useState(false);
  const [processing, setProcessing] = useState(false);
  const dots = useDots();
  const elements = useElements();
  ...

  const onSubmit = async (data: any) => {
    if (!dots || !elements) return;
    setProcessing(true);

    try {
      const payload = await dots.confirmCardPayment('{CLIENT_SECRET}', {
        payment_method: {
          element: elements?.getElement('payment')!,
          billing_details: {
            name: data.name, // not required
            address: {
              country: data.country,
              zip: data.zip,
            },
          },
        },
      });
      setSucceeded(true);
      setMetadata(payload);
    } catch (error: any) {
      setError(error.toString());
    } finally {
      setProcessing(false);
    }
  };

  const renderForm = () => {...};

  return (
    <div className="checkout-form">
      <div className="sr-payment-form">
        <div className="sr-form-row" />
        {renderForm()}
      </div>
    </div>
  );
}

When the payment completes successfully, the value of the returned PaymentIntent’s status property is succeeded. Check the status of a PaymentIntent in the Dashboard or by inspecting the status property on the object. If the payment isn’t successful, inspect the returned error to determine the cause.\

6. Create future payments with reusable payment method. optional

You can create an off_session payment by creating and confirming the same payment intent in single API call.

Save the reusable payment method

When a customer completed the first payment, a payment method will be saved to that payment intent. You can listen to the webhook event payment_intent.updated, and retrieve the payment intent object to find the payment_method_id. Save that id to your user on your database.

Create off session payments

Follow step 3 to create a payment intent. Instead of passing in customer_id or user_id, pass in the payment_method_id and set confirm to True. This creates and confirm the payments immediately.

Off session payment
https://pls.senddotssandbox.com/api/v2/payment-intents/ \
-X POST \
-u "CLIENT_ID:API_KEY" \
-H "Content-Type: application/json" \
-d "{
      "amount": 1000,
      "currency": "usd",
      "confirm": True
      "payment_method_id": "u5835300-5555-4ba1-816a-0abaa8609a42",
      "transfer_data": {
        "destination_user_id": "0a7d51ec-04df-4eb8-a2c8-d066f6d3bb81"
      },
      "application_fee_amount": 100 //fee to be paid to the platform
    }"

> {
  "id": "0e835300-5555-4ba1-816a-0abaa8609a83",
  "status": "succeeded",
  ...
}

7. Test the integration

This information only applies to the sandbox environment
NumberBrandCVCDATE
4111 1111 1111 1111Visa (Credit)Any 3 digitsAny future dates
4900 7700 0000 0001Visa (Debit)Any 3 digitsAny future dates
5100 4000 0000 0000MastercardAny 3 digitsAny future dates
6011 2345 6789 0123DiscoverAny 3 digitsAny future dates
3701 234567 89017American ExpressAny 3 digitsAny future dates