Quick start
Our quick start tutorial implemented in Next.js to start taking payments with Blink
Overview
In this guide we will show you how to get started with the REST API. The examples will be using typescript with Next.js for both the client and the server but you can use any language you want. We will assume a basic understanding of client and server side development, and working with REST APIs.
What we will build in this quick start guide
- Create an example product for the user to purchase
- Setup and authenticate to the Blink API
- Create a payment intent
- Present a payment form to the user
- Process the payment via credit or debit card
Out of the scope of this tutorial is styling, details on handling validation, error handling, or managing application state. For a working example see our Next.js storefront Blink demo
Blink Next.js Storefront Demo
An example eCommerce store demo for working with Blink API's in Next.js with typescript.
Storefront Github Repo
An example eCommerce store demo for working with Blink API's in Next.js with typescript.
Setting up Next.js
For the most part we will leave you rely on the Next.js documentation for setting up your local development enviroment. However, there are a few things to note and here is the basic outline of the steps you will need to take.
In our storefront demo we installed Next.js with Typescript enabled, no to App Directory (Pages Architechture), and Tailwind. But feel free to use whatever framework or configuration you want.
npx create-next-app@latest
Start the development server
npm run dev
Setup some basic UI for the example home page
We're going to add a title, and few product cards to the /pages/index.tsx you can remove the default content and add the following or use your own approach.
import { useState } from "react";
import { ProductCard, Product, ProductProps } from "../components/ProductCard";
import PaymentForm from "../components/PaymentForm";
import ProductDetails from "@/components/ProductDetails";
import { Plus_Jakarta_Sans } from "next/font/google";
import Logo from "@/components/Logo";
const products: Product[] = [
{
name: "Donut",
description: "A tasty treat to start your day",
price: 2.99,
currency: "GBP",
image: "https://loremflickr.com/250/250/coffee-beans?lock=2743448612372480",
},
{
name: "Latte",
description: "A delicious caffeinated cup of joy",
price: 2.5,
currency: "GBP",
image: "https://loremflickr.com/250/250/coffee-beans?lock=1518637902987264",
},
{
name: "Beans",
description: "Make our delicious coffee in the comfort of your own home",
price: 8.99,
currency: "GBP",
image: "https://loremflickr.com/250/250/coffee-beans?lock=5383783945601024",
},
];
const plusJakartaSans = Plus_Jakarta_Sans({
subsets: ["latin"],
variable: "--font-plus-jakarta-sans",
});
export default function Home() {
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
return (
<main
className={`${plusJakartaSans.variable} flex h-screen flex-col items-center gap-12 bg-orange-100 font-sans`}
>
{selectedProduct ? (
<>
<div className="flex h-full w-full flex-col bg-white lg:flex-row">
<ProductDetails
{...selectedProduct}
setSelectedProduct={setSelectedProduct}
/>
<div className="mx-auto flex h-full w-full max-w-4xl items-center justify-center py-10">
<PaymentForm
price={selectedProduct.price}
currency={selectedProduct.currency}
/>
</div>
</div>
</>
) : (
<div className="bg-gradient-winter flex h-fit w-full flex-col justify-center gap-20 px-10 py-20 lg:h-full">
<div className="mx-auto flex max-w-2xl flex-col items-center gap-8">
<Logo />
<h1 className=" mt-5 text-center text-5xl font-semibold">
Example storefront
</h1>
<h2 className="mb-12 text-center text-2xl font-normal">
An demo example of how to use Blink{"'"}s API{"'"}s to start
accepting payments.
</h2>
</div>
<div className="container mx-auto">
<div className="flex flex-col items-center gap-4">
<div className="grid h-full w-fit grid-cols-1 gap-12 lg:grid-cols-3">
{products.map((product) => (
<ProductCard
key={product.name}
name={product.name}
description={product.description}
price={product.price}
currency={product.currency}
setSelectedProduct={setSelectedProduct}
image={product.image}
/>
))}
</div>
</div>
</div>
</div>
)}
</main>
);
}
you should now have something that looks something like this.
Connect to Blink and setup your server
First let's add the enviroment variables to your .env file. You can access your sandbox API keys inside the Blink dashboard under your Blink pages. Each page has it's own API keys so make sure you are using the correct ones for the page you are working on. In this example I have also created a publishable key which you can set to anything you want, we will use this to add an extra layer of security to our API routes.
BLINK_API_KEY=your-api-key
BLINK_API_SECRET=your-api-secret
NEXT_PUBLIC_PUBLISHABLE_KEY=your-publishable-key
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Now let's setup an API route to handle connecting to the Blink API first. Create a new file called /pages/api/blink/initate-payment.ts and add the following code to connect to Blink and handle some basic security, we'll use axios to make the request to the Blink API but you can use any http client you want.
Next we will create the initiate payment logic inside of a try catch which will get the token from the from the function we just created and then create a payment intent for the customer. Add the following code after th getToken function.
You can find a full list of error codes and responses in our API reference.
import { NextApiRequest, NextApiResponse } from "next";
import axios from "axios";
import { z } from "zod";
type TokenResponse = {
access_token: string;
expired_on: string;
payment_types: string[];
currency: string;
payment_api_status: boolean;
send_blink_receipt: boolean;
enable_moto_payments: boolean;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { BLINK_API_KEY, BLINK_API_SECRET, NEXT_PUBLIC_PUBLISHABLE_KEY } =
process.env;
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL
? process.env.NEXT_PUBLIC_BASE_URL
: process.env.NEXT_PUBLIC_VERCEL_URL;
if (
!BLINK_API_KEY ||
!BLINK_API_SECRET ||
!NEXT_PUBLIC_PUBLISHABLE_KEY ||
!baseUrl
) {
return res.status(500).json({ error: "Invalid config" });
}
const schema = z.object({
amount: z.number(),
currency: z.string(),
paymentType: z.union([
z.literal("open-banking"),
z.literal("credit-card"),
z.literal("direct-debit"),
]),
publishableKey: z.string(),
});
const parsedBody = schema.safeParse(req.body);
if (!parsedBody.success) {
return res.status(400).json({ error: parsedBody.error });
}
if (req.body.publishableKey !== NEXT_PUBLIC_PUBLISHABLE_KEY) {
return res.status(401).json({ error: "Unauthorized" });
}
async function getToken(): Promise<TokenResponse> {
try {
const response = await axios.post(
"https://secure.blinkpayment.co.uk/api/pay/v1/tokens",
{
api_key: BLINK_API_KEY,
secret_key: BLINK_API_SECRET,
},
{
headers: {
"Content-Type": "application/json",
},
},
);
return response.data;
} catch (error: any) {
console.dir(error, { depth: null });
throw new Error(error);
}
}
try {
const { access_token } = await getToken();
const response = await axios.post(
"https://secure.blinkpayment.co.uk/api/pay/v1/intents",
{
amount: parsedBody.data.amount,
currency: parsedBody.data.currency,
transaction_type: "SALE",
payment_type: parsedBody.data.paymentType,
return_url: `${baseUrl}/payment-success`,
notification_url: `${baseUrl}/api/blink/payment-notification`,
card_layout: "single-line",
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${access_token}`,
},
},
);
console.dir(response.data, { depth: null });
return res.status(200).json({ form: response.data });
} catch (error: any) {
console.log(error);
return res.status(500).json(error);
}
}
Next we are going to implement the initate payment endpoint into our frontend. We're going to go over direct debit payments in this example as it is the simplest, however refer to the payment page doc for card payments or see the storefront demo github repo. Earlier we by added some state to the index.tsx page to keep track of which product has been selected and keep the state of the payment process.
Now we're going to skip the inner components like the product details and product card in this example as they contain no business logic related to Blink. Feel free to refer to our Github repo.
Let's add the payment form, this will fetch our payment intent from the server that we made earlier and give us the merchantId, currency, transaction unique, and price to pass to our forms.
import { useEffect, useState, useRef, useCallback } from "react";
import { PaymentFormSkeleton } from "../PaymentFormSkeleton";
import axios from "axios";
import { PaymentMethodButton } from "../PaymentMethodButton";
import Card from "../Icons/Card";
import DirectDebit from "../Icons/DirectDebit";
import OpenBanking from "../Icons/OpenBanking";
import CreditCardForm from "./CreditCardForm";
import OpenBankingForm from "./OpenBankingForm";
import DirectDebitForm from "./DirectDebitForm";
type PaymentFormProps = {
price: number;
currency: string;
};
type InitiatePaymentApiResponse = {
id: number;
payment_intent: string;
transaction_type: string;
expiry_date: string;
amount: number;
currency: string;
payment_type: string;
return_url: string;
notification_url: string;
card_layout: string;
element: {
ccElement: string;
obElement: string;
ddElement: string;
};
merchant_id: number;
transaction_unique: string;
};
export default function PaymentForm({ price, currency }: PaymentFormProps) {
const [paymentIntent, setPaymentIntent] =
useState<InitiatePaymentApiResponse | null>(null);
const [paymentType, setPaymentType] = useState<string>("credit-card");
async function getPaymentIntent(
price: number,
currency: string,
paymentType: string,
) {
try {
const response = await axios.post(
`/api/blink/initiate-payment`,
{
price,
currency,
publishableKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
amount: price,
paymentType,
},
{
headers: {
"Content-Type": "application/json",
},
},
);
return response.data.form;
} catch (error: any) {
console.log(error);
}
}
useEffect(() => {
async function fetchPaymentIntent() {
setPaymentIntent(null);
const response = await getPaymentIntent(price, currency, paymentType);
if (!response) return;
setPaymentIntent(response);
}
fetchPaymentIntent();
}, [paymentType, currency, price]);
return (
<div className="w-full max-w-2xl px-4 md:px-12">
{paymentIntent ? (
<>
<div className="mb-8 flex flex-col gap-2">
<h2 className="text-2xl font-semibold">Choose payment method</h2>
<p className="text-lg">
You can pay with card, open banking, or Direct Debit.
</p>
</div>
<div className="mb-12 flex flex-col flex-wrap gap-4 md:flex-row md:gap-0 md:space-x-8">
<PaymentMethodButton
method="credit-card"
paymentType={paymentType}
setPaymentType={setPaymentType}
icon={Card}
name="Card"
/>
<PaymentMethodButton
method="open-banking"
paymentType={paymentType}
setPaymentType={setPaymentType}
icon={OpenBanking}
name="Open Banking"
/>
<PaymentMethodButton
method="direct-debit"
paymentType={paymentType}
setPaymentType={setPaymentType}
icon={DirectDebit}
name="Direct Debit"
/>
</div>
<h3 className="mb-8 text-xl font-semibold">
Pay with{" "}
{paymentType === "credit-card"
? "card"
: paymentType === "open-banking"
? "open banking"
: "Direct Debit"}
</h3>
{paymentType && paymentType === "credit-card" && (
<CreditCardForm
merchantId={paymentIntent.merchant_id}
transactionUnique={paymentIntent.transaction_unique}
paymentIntent={paymentIntent.payment_intent}
currency={currency}
price={price}
/>
)}
{paymentType && paymentType === "open-banking" && (
<OpenBankingForm
paymentIntent={paymentIntent.payment_intent}
transactionUnique={paymentIntent.transaction_unique}
merchantId={paymentIntent.merchant_id}
currency={currency}
price={price}
/>
)}
{paymentType && paymentType === "direct-debit" && (
<DirectDebitForm
paymentIntent={paymentIntent.payment_intent}
transactionUnique={paymentIntent.transaction_unique}
merchantId={paymentIntent.merchant_id}
currency={currency}
price={price}
/>
)}
</>
) : (
<div className="h-fit w-full">
<PaymentFormSkeleton />
</div>
)}
</div>
);
}
In this example we are going to focus specifically on the DirectDebit form only for simplicity. We have added some basic form validation with Zod and created a useEffect to handle our process payment logic but the error handling has been removed from this tutorial to shorten the code. Error handling is available in our example repo.
In this example we have constructed the form manually but you could also insert this through dangerouslySetInnerHTML and using the element object from the payment intent response
import Button from "../Button";
import { useEffect, useState } from "react";
import axios from "axios";
import { useRouter } from "next/router";
import { z } from "zod";
type CreditCardFormProps = {
merchantId: number;
transactionUnique: string;
paymentIntent: string;
currency: string;
price: number;
};
export default function DirectDebitForm({
merchantId,
transactionUnique,
paymentIntent,
currency,
price,
}: CreditCardFormProps) {
const [companyOrIndividual, setCompanyOrIndividual] = useState("individual");
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
async function processPayment(data: any) {
try {
const response = await axios.post(
`/api/blink/process-direct-debit`,
data,
{
headers: {
"Content-Type": "application/json",
},
},
);
return response.data.url;
} catch (error: any) {
alert("Error processing payment, check your account details");
router.reload();
}
}
const directDebitgCompanyFormSchema = z.object({
transaction_unique: z.string(),
payment_intent: z.string(),
company_name: z.string().min(1, "Enter your company name"),
email: z
.string()
.min(1, "Enter your email address")
.email("Enter a valid email address"),
account_holder_name: z.string().min(1, "Enter the account holder name"),
branch_code: z.string().min(1, "Enter the branch code"),
account_number: z.string().min(1, "Enter the account number"),
});
const directDebitIndividualFormSchema = z.object({
transaction_unique: z.string(),
payment_intent: z.string(),
given_name: z.string().min(1, "Enter your first name"),
family_name: z.string().min(1, "Enter your last name"),
email: z
.string()
.min(1, "Enter your email address")
.email("Enter a valid email address"),
account_holder_name: z.string().min(1, "Enter the account holder name"),
branch_code: z.string().min(1, "Enter the branch code"),
account_number: z.string().min(1, "Enter the account number"),
});
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
setIsLoading(true);
const formData = new FormData(event.currentTarget);
const object: { [key: string]: string } = {};
formData.forEach((value, key) => (object[key] = value as string));
let resultData;
if (companyOrIndividual === "individual") {
const result = directDebitIndividualFormSchema.safeParse(object);
if (result.success) {
resultData = result.data;
}
}
if (companyOrIndividual === "company") {
const result = directDebitgCompanyFormSchema.safeParse(object);
if (result.success) {
resultData = result.data;
}
}
const redirectUrl = await processPayment(resultData);
setIsLoading(false);
if (!redirectUrl) return;
router.push(redirectUrl);
}
return (
<form id="payment" onSubmit={handleSubmit} className="h-fit w-full">
<div className="pb-8 font-medium">
<label className="mb-4 block">
Are you an individual or a company?
</label>
<button
type="button"
onClick={() => setCompanyOrIndividual("individual")}
className={`${
companyOrIndividual === "individual"
? "border-2 border-solid border-black text-gray-900"
: "border-2 border-solid border-gray-200 text-gray-900"
} mr-4 rounded-md px-4 py-2`}>
Individual
</button>
<button
type="button"
onClick={() => setCompanyOrIndividual("company")}
className={`${
companyOrIndividual === "company"
? "border-2 border-solid border-black text-gray-900"
: "border-2 border-solid border-gray-200 text-gray-900"
} rounded-md px-4 py-2`}
>
Company
</button>
</div>
<input type="hidden" name="merchantID" defaultValue={merchantId} />
<input
type="hidden"
name="transaction_unique"
defaultValue={transactionUnique}
/>
<input type="hidden" name="payment_intent" defaultValue={paymentIntent} />
<input
type="hidden"
name="transaction_unique"
defaultValue={transactionUnique}
/>
<input type="hidden" name="resource" defaultValue="directdebits" />
{companyOrIndividual === "individual" && (
<>
<label className="mb-4 block">Your info</label>
<div className="fieldgroup-container mb-4">
<input
type="text"
name="given_name"
placeholder="Your first name"
/>
</div>
{errors.given_name.error && (
<p className="mb-4 text-red-500">{errors.given_name.message}</p>
)}
<div className="fieldgroup-container mb-4">
<input
type="text"
placeholder="Your last name"
name="family_name"
/>
</div>
{errors.family_name.error && (
<p className="mb-4 text-red-500">{errors.family_name.message}</p>
)}
</>
)}
{companyOrIndividual === "company" && (
<>
<div className="fieldgroup-container mb-4">
<label className="mb-2 block">Your company info</label>
<input
type="text"
placeholder="Your company name"
name="company_name"
/>
</div>
</>
)}
<div className="fieldgroup-container mb-8">
<input type="text" placeholder="Your email" name="email" />
</div>
<div className="fieldgroup-container mb-4">
<label className="mb-4 block">Account details</label>
<input
type="text"
placeholder="Account holder name"
name="account_holder_name"
/>
</div>
<div className="fieldgroup-container mb-4">
<input type="text" placeholder="Branch code" name="branch_code" />
</div>
<div className="fieldgroup-container mb-4">
<input
type="text"
placeholder="Bank account number"
name="account_number"
/>
</div>
<Button type="submit" variant="fullWidth">
{!isLoading ? (
<>
Pay {currency === "USD" && "$"} {currency === "EUR" && "€"}{" "}
{currency === "GBP" && "£"}
{price?.toFixed(2)}{" "}
</>
) : (
<>
<svg
className="-ml-1 mr-3 h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Processing...
</>
)}
</Button>
</form>
);
}
Finally we are going to add an endpoint to process the direct debit form.
import { NextApiRequest, NextApiResponse } from "next";
import axios from "axios";
type TokenResponse = {
access_token: string;
expired_on: string;
payment_types: string[];
currency: string;
payment_api_status: boolean;
send_blink_receipt: boolean;
enable_moto_payments: boolean;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { BLINK_API_KEY, BLINK_API_SECRET, NEXT_PUBLIC_PUBLISHABLE_KEY } =
process.env;
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL
? process.env.NEXT_PUBLIC_BASE_URL
: process.env.NEXT_PUBLIC_VERCEL_URL;
if (
!BLINK_API_KEY ||
!BLINK_API_SECRET ||
!NEXT_PUBLIC_PUBLISHABLE_KEY ||
!baseUrl
) {
return res.status(500).json({ error: "Invalid config" });
}
async function getToken(): Promise<TokenResponse> {
try {
const response = await axios.post(
"https://secure.blinkpayment.co.uk/api/pay/v1/tokens",
{
api_key: BLINK_API_KEY,
secret_key: BLINK_API_SECRET,
},
{
headers: {
"Content-Type": "application/json",
},
},
);
return response.data;
} catch (error: any) {
console.dir(error, { depth: null });
throw new Error(error);
}
}
try {
const { access_token } = await getToken();
const response = await axios.post(
"https://secure.blinkpayment.co.uk/api/pay/v1/directdebits",
{
...req.body,
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${access_token}`,
},
},
);
return res.status(200).json({ url: response.data.url });
} catch (error: any) {
console.log(error);
return res.status(500).json(error);
}
}