Quickstart

Learn how to build crypto payments directly into your product using Pluto.

Overview

You can build a custom payment flow using the Pluto React library, which allows you to accept multiple cryptocurrencies using a single integration. In this example, we'll set up a custom Ethereum payment flow using Next.js and the Pluto Node and React libraries. You can view the full source code here.

Set up Pluto

Use the official pluto-node library to interact with the Pluto API from your application:

# Using NPM
npm install @plutohq/pluto-node --save

# Using Yarn
yarn add @plutohq/pluto-node

Create a payment

Using cryptocurrencies like Ethereum, Solana, and Bitcoin for payments is fundamentally different from processing credit card payments. With credit cards, merchants can charge customers automatically without client-side confirmation — the user submits their credit card information, and the site can charge the user's credit card at any time. Crypto, on the other hand, cannot be automatically withdrawn from a user's wallet; the customer needs to manually approve the transaction. Because all crypto payments begin as an intent to pay, they are referred to as a PaymentIntent, as opposed to a Charge, Payment, or Transaction.

When a user submits the checkout form, we'll submit their information to the /checkout route, which will create a Customer and a PaymentIntent. Once the PaymentIntent is successfully created, we send the data back to the browser for confirmation.

import { Pluto } from '@plutohq/pluto-node';

// Initialize the Pluto Node.js library using your test secret key.
// See your keys here: https://pluto.co/developers
const pluto = new Pluto(process.env.PLUTO_SECRET_KEY);

async function handler(req, res) {
  try {
    if (req.method === 'POST') {
      if (!req.body.name || !req.body.email) {
        return res.status(400).json({ error: 'Missing name or email' });
      }

      const customer = await pluto.customers.create({
        name: req.body.name,
        email: req.body.email,
      });

      const paymentIntent = await pluto.paymentIntents.create({
        chain: 'eth',
        currency: 'eth',
        amount: 0.01,
        customer: customer.id,
      });

      return res.send(paymentIntent);
    }

    return res.status(400).json({ message: 'Invalid request' });
  } catch (err) {
    console.log(err);
    return res.status(500).json({ error: err.message });
  }
}

export default handler;

Collect payment details

We recommend using our React library, which includes components that allow you to collect a user's wallet information and hooks for confirming and polling PaymentIntents. You can also use our client-side JavaScript library if you want to implement this logic yourself.

# Using NPM
npm install @plutohq/pluto-react --save

# Using Yarn
yarn add @plutohq/pluto-react

To initialize the client library, wrap the checkout form in the Pluto provider context and pass your publishable API key.

import { loadPluto } from '@plutohq/pluto-js';
import { PlutoConfig } from '@plutohq/pluto-react';
import { NextSeo } from 'next-seo';
import React from 'react';
import CheckoutForm from '../components/CheckoutForm';

function Index() {
  const pluto = loadPluto(process.env.NEXT_PUBLIC_PLUTO_PUBLISHABLE_TEST_KEY);

  return (
    <PlutoConfig pluto={pluto}>
      <NextSeo title="Pluto Crypto Checkout Quickstart" noindex />
      <CheckoutForm />
    </PlutoConfig>
  );
}

export default Index;

The client library offers a headless wallet connection component, which can be used for displaying supported wallets as well as connecting or disconnecting the user's wallet.

import { ConnectEthWallet as ConnectWallet } from '@plutohq/pluto-react';
import React from 'react';

export default function ConnectEthWallet() {
  return (
    <ConnectWallet>
      {({ address, connectors, connect, disconnect }) => (
        <div>
          <div>
            <label htmlFor="address">
              {address ? 'Wallet address' : 'Connect a wallet'}
            </label>
            {address && (
              <button
                type="button"
                onClick={() => disconnect()}
              >
                Disconnect
              </button>
            )}
          </div>
          {address ? (
            <input
              readOnly
              type="text"
              name="address"
              value={address}
            />
          ) : (
            <div>
              {connectors.map((connector) => (
                <button
                  key={connector.id}
                  type="button"
                  onClick={() => connect({ connector })}
                >
                  {connector.name}
                </button>
              ))}
            </div>
          )}
        </div>
      )}
    </ConnectWallet>
  );
}

Once the user has connected their wallet, we call the usePluto() hook to return an instance of @plutohq/pluto-js. This gives us access to the confirmPayment() and waitForPayment() methods, which will send the payment and poll the payment intent until it has either succeeded or failed.

import { usePluto } from '@plutohq/pluto-react';
import React from 'react';
import ConnectEthWallet from './ConnectEthWallet';
import PaymentModal from './PaymentModal';

export default function CheckoutForm() {
  const pluto = usePluto();
  const [loading, setLoading] = React.useState(false);
  const [transaction, setTransaction] = React.useState(null);
  const [showPaymentModal, setShowPaymentModal] = React.useState(false);

  const handleSubmit = React.useCallback(
    async (event) => {
      event.preventDefault();
      
      setLoading(true);

      const paymentIntent = await fetch('/api/checkout', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: event.target.name.value,
          email: event.target.email.value,
        }),
      })
        .then((res) => res.json())
        .then((result) => {
          if (result.error) {
            setLoading(false);
            console.error(result.error);
            return null;
          }

          return result;
        });

      if (paymentIntent) {
        await pluto.confirmPayment(paymentIntent.id)
          .then(async (data) => {
            setTransaction(data.hash);
            const completedPayment = await pluto.waitForPayment(paymentIntent.id);
            
            if (completedPayment.status === 'succeeded') {
              console.log('Payment intent succeeded!');
              setLoading(false);
            }
          })
          .catch(console.error)
          .finally(() => setLoading(false));
      }
    },
    [pluto],
  );

  return (
    <form id="checkout-form" onSubmit={handleSubmit}>
      <ConnectEthWallet />
      <input type="text" name="name" />
      <input type="text" name="email" />
      <button type="submit">{loading ? 'Processing...' : 'Pay now'}</button>
      {transaction && (
        <div>
          Your transaction hash:
          {transaction}
        </div>
      )}
    </form>
  );
}

After the payment

If you want to execute any logic after the payment succeeds, you can use a webhook to listen for any updates to the payment. You can set up a webhook via the dashboard or the API. If you use the API, the signing secret is returned in the response, which is used for verifying the webhook in the next step.

const webhook = await pluto.webhooks.create({
  endpoint_url: 'https://api.example.com/webhook',
  event_types: ['*'],
});

Once you've added an endpoint to your server to receive webhooks, you're all set. You can view the full list of events here.

const { buffer } = require('micro');
const { Webhook } = require('svix');

const webhook = new Webhook(process.env.PLUTO_WEBHOOK_SECRET);

export default async function handler(req, res) {
  try {
    if (req.method === 'POST') {
      const body = (await buffer(req))?.toString();
      const event = webhook.verify(body, req.headers);

      if (event.type === 'payment_intent.succeeded') {
        const payment = event.data;
        console.log(`Payment ${payment.id} for ${payment.amount} ${payment.currency.toUpperCase()} succeeded`);
      }

      if (event.type === 'payment_intent.failed') {
        const payment = event.data;
        console.log(`Payment ${payment.id} for ${payment.amount} ${payment.currency.toUpperCase()} failed`);
      }
    }

    return res.send(202);
  } catch (err) {
    return res.status(500).json({ error: err.message });
  }
}

export const config = {
  api: {
    bodyParser: false,
  },
};

Test the integration

We recommend using a service such as Svix or ngrok to forward requests to your local API. You can test checkout flows using test mode.