The Complete Guide to Getting Started with the Braintree GraphQL API

Braintree is one of the world's leading payment platforms. On a basic level, Braintree allows you to charge customers from your web app. The service is owned by PayPal and supports a wide array of payment methods — card, PayPal, Venmo, Apple Pay, and Google Pay — so your customers can check out with ease no matter which payment method they prefer.

Braintree provides a powerful and modern GraphQL API to enable you to quickly integrate with the services' many features without having to add any dependencies to your project. This article will guide you through implementing a basic payment use case — charging a credit card — using the Braintree GraphQL API from Node.js and React, specifically Next.js. That said, it is straightforward to adapt the code shown in this guide to any web app tech stack. That's the beauty of integrating via an HTTP-based API rather than a language-specific SDK.

Security is of the utmost importance whenever financial data is involved, so it's critical that we handle our Braintree API key safely. That's why we will store the Braintree API key in the Zero secrets manager and fetch it at runtime using the official Zero TypeScript SDK. If using a language other than JavaScript or TypeScript, you can use one of Zero's other SDKs, or interface with Zero's GraphQL API directly.

🔗 The full code for this example is available in the zerosecrets/examples GitHub repository.

Signing Up for Braintree

To start using Braintree, you'll need to sign up for a Sandbox account. The Sandbox allows you to develop against the API with zero risk of making a real charge.

Once logged in, you will be presented with your public key, private key, and merchant ID. If not, click the gear icon in the upper right corner of the page and select "API". Your API key is the public key followed by the private key encoded in base 64. You can easily obtain your API key by running

1echo -n "v4ndq314c2s5c28r:93b78bc88be90d93ac282e50ae569fdd" | base64

Just remember to replace v4ndq314c2s5c28r with your public key and 93b78bc88be90d93ac282e50ae569fdd with your private key.

⚠️ Both your private key and API key provide access to your account, so they must be kept secure. Be careful not to commit either of these values to a git repository or access them on the client side.

Server-side calls to the GraphQL API use the API key (not the private key), so it's the API key which we need to copy into Zero:

Adding a Braintree secret in Zero

A High-Level Overview of Accepting a Payment

The simplest way to use Braintree is to accept a one-time payment. The excellent Braintree docs explain how this process works at a technical level:

  1. Your server calls the Braintree GraphQL API to create a client token which should be sent to the client (your web or mobile app).
  2. The client collects payment information from the user (e.g. credit card number) and passes it to Braintree along with the client token to obtain a payment method ID.
  3. The client sends the payment method ID to your server.
  4. The server creates a transaction in Braintree via the payment method ID.

This design is great because the customer's payment information never passes through your server. This sidesteps a whole range of serious security and privacy issues that can arise when handling sensitive financial information.

Requesting a Client Token

Let's bootstrap a new Next.js project so we can start integrating with Braintree:

1npx create-next-app nextjs-braintree --ts --use-npm

Our app will contain a single page with a "Submit fake payment" button that submits a hardcoded credit card and billing address to the server. When the server receives the request, it creates a transaction in Braintree and returns the transaction ID and status to the client.

The simple app we're building to test out Braintree

As described in the previous section, the first step is to request a client token. This requires your Braintree API key, so the request must be made on the server side. We can add server-side logic to a Next.js page by defining a getServerSideProps function in the same file as the page:

1interface Props {
2  clientToken: string
3}
4
5export const getServerSideProps: GetServerSideProps<Props> = async () => {
6  // TODO Get the client token from the GraphQL API
7
8  return {props: {clientToken}}
9}
10
11const Home: NextPage<Props> = ({clientToken}) => {
12  // TODO Render the UI
13}
14
15export default Home

With the general structure of the code in place, let's work on filling in getServerSideProps. First, we'll need to fetch the Braintree API key from Zero:

1const apiKey = await fetchBraintreeApiKey()

The API key will be used in multiple different places in the app, so it's best to create a fetchBraintreeApiKey helper function so that the code for calling Zero is not duplicated. The function uses the Zero TypeScript SDK to exchange your Zero token for the Braintree API key:

1import {zero} from '@zerosecrets/zero'
2
3let apiKey: string | undefined
4
5export async function fetchBraintreeApiKey(): Promise<string> {
6  // Don't call Zero if we already fetched the API key
7  if (apiKey) {
8    return apiKey
9  }
10
11  if (!process.env.ZERO_TOKEN) {
12    throw new Error('Did you forget to set the ZERO_TOKEN environment variable?')
13  }
14
15  const secrets = await zero({
16    token: process.env.ZERO_TOKEN,
17    pick: ['braintree'],
18  }).fetch()
19
20  if (!secrets.braintree) {
21    throw new Error('Did not receive an API key for Braintree.')
22  }
23
24  apiKey = secrets.braintree.API_KEY
25
26  return apiKey
27}

Next, we need to execute the GraphQL mutation

1mutation {
2  createClientToken {
3    clientToken
4  }
5}

Let's define this as a string constant in a new src/braintreeApi/graphql.ts file:

1export const CREATE_CLIENT_TOKEN_MUTATION = `
2mutation createClientToken($input: CreateClientTokenInput) {
3  createClientToken(input: $input) {
4    clientToken
5  }
6}`.trim()

Let's also define a TypeScript type for the response we expect the mutation to return:

1export interface CreateClientTokenResponse {
2  data: {createClientToken: {clientToken: string}}
3}

Now we have all the building blocks necessary to make the GraphQL call in getServerSideProps:

1const body = {
2  query: CREATE_CLIENT_TOKEN_MUTATION,
3  variables: {
4    input: {
5      clientToken: {
6        merchantAccountId: BRAINTREE_MERCHANT_ID,
7      },
8    },
9  },
10}
11
12// Node 18+ required for built-in fetch support
13const response = await fetch(BRAINTREE_GRAPHQL_ENDPOINT, {
14  method: 'POST',
15  body: JSON.stringify(body),
16  headers: {
17    'Content-Type': 'application/json',
18    Authorization: `Basic ${apiKey}`,
19    'Braintree-Version': BRAINTREE_VERSION,
20  },
21})
22const {data} = (await response.json()) as CreateClientTokenResponse
23const clientToken = data.createClientToken.clientToken

This code makes use of a few constants which can be defined in braintreeApi/constants.ts:

1export const BRAINTREE_GRAPHQL_ENDPOINT = 'https://payments.sandbox.braintree-api.com/graphql'
2
3// Set this to the date you started using the Braintree API
4export const BRAINTREE_VERSION = '2022-09-29'
5
6// TODO: Set NEXT_PUBLIC_BRAINTREE_MERCHANT_ID to your merchant ID in a .env file
7export const BRAINTREE_MERCHANT_ID = process.env.NEXT_PUBLIC_BRAINTREE_MERCHANT_ID

If you run the project with ZERO_TOKEN='your-zero-token' npm run dev, you should get a long string like eyJ2ZXJzaW9uIjoyLCJhdXRob3JpemF0... for the client token.

The client token is actually a base 64 encoded JSON string which contains an authorization fingerprint. The fingerprint can be extract from the client token like so

1const {authorizationFingerprint} = JSON.parse(atob(clientToken))

It is this fingerprint that must be passed in the Authorization header when making GraphQL requests on the client side.

Collecting Payment Information

Now that the frontend has an authorization fingerprint, the next step is to collect the user's payment information and exchange it for a token (payment method ID) using the mutation

1mutation tokenizeCreditCard($input: TokenizeCreditCardInput!) {
2  tokenizeCreditCard(input: $input) {
3    paymentMethod {
4      id
5    }
6  }
7}

The exact data required to form a TokenizeCreditCardInput can be determined from Braintree's GraphQL Explorer, specifically the "Docs" sidebar on the right.

TokenizeCreditCardInput in the Braintree GraphQL Explorer

TypeScript types should be added to braintreeApi/graphql.ts to represent TokenizeCreditCardInput and the input types it references, such as CreditCardInput and AddressInput.

In a real-world application, the UI would display a form where the user can enter their credit card information and billing address. To keep this demonstration simple, we'll simply hardcode a fake billing address and credit card:

1const billingAddress: AddressInput = {
2  addressLine1: '2435 Lynn Rd',
3  addressLine2: 'Suite 206',
4  locality: 'Raleigh',
5  region: 'NC',
6  postalCode: '27612',
7  countryCode: 'US',
8}
9
10const creditCard: CreditCardInput = {
11  number: '4242424242424242',
12  expirationYear: '25',
13  expirationMonth: '12',
14  cvv: '001',
15  cardholderName: 'Samuel Magura',
16  billingAddress: billingAddress,
17}

When the user clicks the "Submit fake payment" button in the UI, we'll call the tokenizeCreditCard mutation with the hardcoded credit card. This returns a payment method ID which we pass to our backend, which will use the payment method ID to charge the credit card.

1const submit: React.FormEventHandler = async (e) => {
2  e.preventDefault()
3
4  const {authorizationFingerprint} = JSON.parse(atob(clientToken))
5
6  const input: TokenizeCreditCardInput = {creditCard}
7
8  const body = {
9    query: TOKENIZE_CREDIT_CARD_MUTATION,
10    variables: {input},
11  }
12
13  const tokenizeCreditCardResponse = await fetch(BRAINTREE_GRAPHQL_ENDPOINT, {
14    method: 'POST',
15    body: JSON.stringify(body),
16    headers: {
17      'Content-Type': 'application/json',
18      Authorization: `Bearer ${authorizationFingerprint}`,
19      'Braintree-Version': BRAINTREE_VERSION,
20    },
21  })
22  const {data} = (await tokenizeCreditCardResponse.json()) as TokenizeCreditCardResponse
23  const paymentMethodId = data.tokenizeCreditCard.paymentMethod.id
24
25  const createTransactionResponse = await fetch('/api/createTransaction', {
26    method: 'POST',
27    body: JSON.stringify({paymentMethodId}),
28    headers: {
29      'Content-Type': 'application/json',
30    },
31  })
32
33  setServerResponse((await createTransactionResponse.json()) as string)
34}

Creating a Transaction

The only remaining step is to implement the /api/createTransaction method on the backend. In Next.js, you can create a backend API method by placing files in the src/pages/api directory, so let's create the file src/pages/api/createTransaction.ts.

The createTransaction API route should accept a payment method ID in the request body and use it to call the Braintree API with the following mutation:

1mutation chargePaymentMethod($input: ChargePaymentMethodInput!) {
2  chargePaymentMethod(input: $input) {
3    transaction {
4      id
5      status
6    }
7  }
8}

TypeScript types can be created for the input and response objects by using the GraphQL explorer as we did previously. The final code for the API route looks like this:

1try {
2  if (req.method !== 'POST') {
3    throw new Error('Method must be POST.')
4  }
5
6  const paymentMethodId = req.body.paymentMethodId
7
8  if (typeof paymentMethodId !== 'string' || paymentMethodId.length === 0) {
9    throw new Error('paymentMethodId was not provided.')
10  }
11
12  const apiKey = await fetchBraintreeApiKey()
13
14  const transaction: TransactionInput = {
15    amount: 10,
16  }
17
18  const chargePaymentMethodInput: ChargePaymentMethodInput = {
19    paymentMethodId,
20    transaction: transaction,
21  }
22
23  const body = {
24    query: CHARGE_PAYMENT_METHOD_MUTATION,
25    variables: {
26      input: chargePaymentMethodInput,
27    },
28  }
29
30  const response = await fetch(BRAINTREE_GRAPHQL_ENDPOINT, {
31    method: 'POST',
32    body: JSON.stringify(body),
33    headers: {
34      'Content-Type': 'application/json',
35      Authorization: `Basic ${apiKey}`,
36      'Braintree-Version': BRAINTREE_VERSION,
37    },
38  })
39
40  const {data} = (await response.json()) as ChargePaymentMethodResponse
41  const transactionResponse = data.chargePaymentMethod.transaction
42
43  res
44    .status(200)
45    .json(`Transaction ${transactionResponse.id} created successfully. ` + `Status = ${transactionResponse.status}`)
46} catch (e) {
47  res.status(500).json(e instanceof Error ? `${e.message}\n\n${e.stack}` : 'Unknown error.')
48}

The string returned by this API method should be displayed in the UI so we know whether the integration with the Braintree Sandbox is working. If it worked, you'll see a string like

1Transaction dHJhbnNhY3Rpb25fY2t2bWsycjI created successfully. Status = SUBMITTED_FOR_SETTLEMENT

Congratulations, you just charged your first customer!

In Conclusion

Once you wrap your head around the multi-step process that is required to submit a transaction to Braintree, the rest is straightforward thanks to the documentation provided in Braintree's GraphQL explorer. While Braintree does provide a few ready-made SDKs, calling the GraphQL API directly is likely to be the most flexible approach. It also keeps your node_modules directory lean as it does not require any additional dependencies.

Author avatar

Sam Magura