Integrate Paystack Payment Gateway using Spring Boot

Yahaya Yusuf
7 min readMar 22, 2023

--

Instead of having to create the wheel from scratch, which is a lot of work, using third-party APIs to collect payment makes accepting payment from consumers simple.

In this article, we will mostly be focusing on Paystack. I’ll walk you through the process of receiving payment using the Paystack payment gateway.

Prerequisites

  1. Basic Spring boot

2. Basic of Rest API

Instead of creating test cases or managing exceptions, we will concentrate mostly on using the Paystack API to collect payments.

Simply create an account on the Paystack website.

Setup a Spring Boot application with the following dependencies

  1. Spring Web

2. Spring Data JPA

3. MySQL Driver

4. HTTPClient

5. Hibernate Validation dependency

6. Lombok

Create the following packages in your project as seen below

Paystack project structure

The constant’s package creates a POJO APIConstants to add all static fields, as in below.


public class APIConstants {

public static final integer STATUS_CODE_OK = 200;
public static final integer STATUS_CODE_CREATED = 201;
public static final String PAYSTACK_INIT = "https://api.paystack.co/plan";
public static final String PAYSTACK_INITIALIZE_PAY = "https://api.paystack.co/transaction/initialize";
public static final String PAYSTACK_VERIFY = "https://api.paystack.co/transaction/verify/";
}

We need a POJO to map to create a table to store user information and payment information for reference because we are using Object Relational Mapping(ORM).

The model package creates a domain package, as seen in the code below. We have a POJO to map AppUser and PaymentPaystack.

AppUser


@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name= "appuser")
public class AppUser {

@Id
@Column(name = "id", nullable = false)
private Long id;

@Column(name = "username", nullable = false)
private String usernme;

@Column(name = "name", nullable = false)
private String name;

@Column(name = "address", nullable = false)
private String address;

@CreationTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_on", updatable = false, nullable = false)
private Date createdOn;
}

PaymentPaystack

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name= "paystack_payment")
public class PaymentPaystack {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "user_id")
private AppUser user;

@Column(name = "reference")
private String reference;

@Column(name = "amount")
private BigDecimal amount;

@Column(name = "gateway_response")
private String gatewayResponse;

@Column(name = "paid_at")
private String paidAt;

@Column(name = "created_at")
private String createdAt;

@Column(name = "channel")
private String channel;

@Column(name = "currency")
private String currency;

@Column(name = "ip_address")
private String ipAddress;

@Column(name = "pricing_plan_type", nullable = false)
@Enumerated(EnumType.STRING)
private pricing plan type PlanType = PricingPlanType BASIC;

@CreationTimestamp
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "created_on", updatable = false, nullable = false)
private Date createdOn;
}

Still within the model package create dto package for the data access layers POJOs we will be using.

CreatePlanDto

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CreatePlanDto {

@NotNull(message = "Plan name cannot be null")
@JsonProperty("name")
private String name;

@NotNull(message = "Interval cannot be null")
@JsonProperty("interval")
private String interval;

@NotNull(message = "Amount cannot be null")
@JsonProperty("amount")
@Digits(integer = 6, fraction = 2)
private Integer amount;
}

IntializePaymentDto

@Getter
@Setter
@AllArgsConstructor@NoArgsConstructor
@Builder
public class InitializePaymentDto {

@NotNull(message = "Amount cannot be null")
@JsonProperty("amount")
private String amount;

@NotNull(message = "Email cannot be null")
@JsonProperty("email")
private String email;

@NotNull(message = "Currency cannot be null")
@JsonProperty("currency")
private String currency;

@NotNull(message = "Plan cannot be null")
@JsonProperty("plan")
private String plan;

@NotNull(message = "Channels cannot be null")
@JsonProperty("channels")
private String[] channels;
}

PaymentVerificationDto

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class PaymentVerificationDto {

@JsonProperty("member_id")
private AppUser user;

@JsonProperty("reference")
private String reference;

@JsonProperty("amount")
private BigDecimal amount;

@JsonProperty("gateway_response")
private String gatewayResponse;

@JsonProperty("paid_at")
private String paidAt;

@JsonProperty("created_at")
private String createdAt;

@JsonProperty("channel")
private String channel;

@JsonProperty("currency")
private String currency;

@JsonProperty("ip_address")
private String ipAddress;

@JsonProperty("pricing_plan_type")
private String pricingPlanType;

@JsonProperty("created_on")
private Date createdOn = new Date();
}

An enums package for pricing plan type enum in the model package as in below

PricingPlanType

@Getter
public enum PricingPlanType {

BASIC("Basic"),
STANDARD("Standard"),
PREMIUM("Premium");

private final String value;
PricingPlanType(String value) {
this.value = value;
}

}

The response package sits inside the model package, and the POJOs under the response package are mapped to API responses.

CreatePlanResponse

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CreatePlanResponse {

private Boolean status;
private String message;
private Data data;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class Data {

@JsonProperty("name")
private String name;

@JsonProperty("amount")
private String amount;

@JsonProperty("interval")
private String interval;

@JsonProperty("integration")
private String integration;

@JsonProperty("plan_code")
private String planCode;

@JsonProperty("send_invoices")
private String sendInvoices;

@JsonProperty("send_sms")
private String sendSms;

@JsonProperty("currency")
private String currency;

@JsonProperty("id")
private String id;

@JsonProperty("createdAt")
private String createdAt;

@JsonProperty("updatedAt")
private String updatedAt;

}
}

InitializePaymentResponse

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class InitializePaymentResponse {

@JsonProperty("status")
private Boolean status;

@JsonProperty("message")
private String message;

@JsonProperty("data")
private Data data;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class Data{

@JsonProperty("authorization_url")
private String authorizationUrl;

@JsonProperty("access_code")
private String accessCode;

@JsonProperty("reference")
private String reference;
}
}

PaymentVerificationResponse

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public class PaymentVerificationResponse {

@JsonProperty("status")
private String status;

@JsonProperty("message")
private String message;

@JsonProperty("data")
private Data data;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Data{

@JsonProperty("status")
private String status;

@JsonProperty("reference")
private String reference;

@JsonProperty("amount")
private BigDecimal amount;

@JsonProperty("gateway_response")
private String gatewayResponse;

@JsonProperty("paid_at")
private String paidAt;

@JsonProperty("created_at")
private String createdAt;

@JsonProperty("channel")
private String channel;

@JsonProperty("currency")
private String currency;

@JsonProperty("ip_address")
private String ipAddress;

@JsonProperty("pricing_plan_type")
private String pricingPlanType;

@JsonProperty("created_on")
private Date createdOn = new Date();

@JsonProperty("updated_on")
private Date updatedOn = new Date();
}
}

The PaystackPaymentRepository/Impl package has two repositories.

AppUserRepositoryImpl

@Repository
public interface AppUserRepositoryImpl extends JpaRepository<AppUser, Long> {
}

PaystackPaymentRepositoryImpl

@Repository
public interface PaystackPaymentRepositoryImpl extends JpaRepository<PaymentPaystack, Long> {
}

Next, we have our service and its implementation.

PaystackService Interface

public interface PaystackService {
CreatePlanResponse createPlan(CreatePlanDto createPlanDto) throws Exception;
InitializePaymentResponse initializePayment(InitializePaymentDto initializePaymentDto);
PaymentVerificationResponse payment Verification(String reference, String plan, Long id) throws Exception;
}

PaystackServiceImpl

@Service
public class PaystackServiceImpl implements PaystackService {

private final PaystackPaymentRepositoryImpl paystackPaymentRepository;
private final AppUserRepositoryImpl appUserRepository;

@Value("${applyforme.paystack.secret.key}")
private String paystackSecretKey;

public PaystackServiceImpl(PaystackPaymentRepositoryImpl paystackPaymentRepository, AppUserRepositoryImpl appUserRepository) {
this.paystackPaymentRepository = paystackPaymentRepository;
this.appUserRepository = appUserRepository;
}

@Override
public CreatePlanResponse createPlan(CreatePlanDto createPlanDto) throws Exception {
CreatePlanResponse createPlanResponse = null;

try {
Gson gson = new Gson();
StringEntity postingString = new StringEntity(gson.toJson(createPlanDto));
HttpClient client = HttpClientBuilder.create().build();
HttpPost post = new HttpPost(PAYSTACK_INIT);
post.setEntity(postingString);
post.addHeader("Content-type", "application/json");
post.addHeader("Authorization", "Bearer " + paystackSecretKey);
StringBuilder result = new StringBuilder();
HttpResponse response = client.execute(post);

if (response.getStatusLine(). getStatusCode() == STATUS_CODE_CREATED) {

BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));

String line;
while ((line = rd.readLine()) != null) {
result.append(line);
}
} else {
throw new Exception ("Paystack is unable to process payment at the moment " +
"or something wrong with request");
}

ObjectMapper mapper = new ObjectMapper();
createPlanResponse = mapper.readValue(result.toString(), CreatePlanResponse.class);
} catch(Throwable ex) {
ex.printStackTrace();
}
return createPlanResponse;
}

@Override
public InitializePayment Response initializePayment(InitializePaymentDto initializePaymentDto) {
InitializePaymentResponse initializePaymentResponse = null;

try {
Gson gson = new Gson();
StringEntity posting String = new StringEntity(gson.toJson(initializePaymentDto));
HttpClient client = HttpClientBuilder.create().build();
HttpPost post = new HttpPost(PAYSTACK_INITIALIZE_PAY);
post.setEntity(postingString);
post.addHeader("Content-type", "application/json");
post.addHeader("Authorization", "Bearer " + paystackSecretKey);
StringBuilder result = new StringBuilder();
HttpResponse response = client.execute(post);

if (response.getStatusLine(). getStatusCode() == STATUS_CODE_OK) {

BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));

String line;
while ((line = rd.readLine()) != null) {
result.append(line);
}
} else {
throw new Exception("Paystack is unable to initialize payment at the moment");
}

ObjectMapper mapper = new ObjectMapper();
initializePaymentResponse = mapper. readValue(result.toString(), InitializePaymentResponse.class);
} catch(Throwable ex) {
ex.printStackTrace();
}
return initializePaymentResponse;
}

@Override
@Transactional
public PaymentVerification Response paymentVerification(String reference, String plan, Long id) throws an exception. {
PaymentVerificationResponse paymentVerificationResponse = null;
PaymentPaystack payment Paystack = null;

try{
HttpClient client = HttpClientBuilder.create().build();
HttpGet request = new HttpGet(PAYSTACK_VERIFY + reference);
request.addHeader("Content-type", "application/json");
request.addHeader("Authorization", "Bearer " + paystackSecretKey);
StringBuilder result = new StringBuilder();
HttpResponse response = client.execute(request);

if (response.getStatusLine(). getStatusCode() == STATUS_CODE_OK) {
BufferedReader rd = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
String line;

while ((line = rd.readLine()) != null) {
result.append(line);
}
} else {
throw new Exception("Paystack is unable to verify payment at the moment");
}

ObjectMapper mapper = new ObjectMapper();
paymentVerificationResponse = mapper.readValue(result.toString(), PaymentVerificationResponse.class);

if paymentVerificationResponse == null || paymentVerificationResponse getStatus().equals("false")) {
throw new Exception("An error");
} else if (paymentVerificationResponse. getData().getStatus().equals("success")) {

AppUser appUser = appUserRepository.getById(id);
PricingPlanType pricing PlanType = PricingPlanType.valueOf(plan.toUpperCase());

paymentPaystack = PaymentPaystack.builder()
.user(appUser)
.reference(paymentVerificationResponse.getData().getReference())
.amount(paymentVerificationResponse.getData().getAmount())
.gatewayResponse(paymentVerificationResponse.getData().getGatewayResponse())
.paidAt(paymentVerificationResponse.getData().getPaidAt())
.createdAt(paymentVerificationResponse.getData().getCreatedAt())
.channel(paymentVerificationResponse.getData().getChannel())
.currency(paymentVerificationResponse.getData().getCurrency())
.ipAddress(paymentVerificationResponse.getData().getIpAddress())
.pricingPlanType(pricingPlanType)
.createdOn(new Date())
.build();
}
} catch (Exception ex) {
throw new Exception("Paystack");
}
paystackPaymentRepository.save(paymentPaystack);
return paymentVerificationResponse;
}
}

Last but not least we have our PaystackController in the controller package.

@RestController
@RequestMapping(
value = "/paystack",
produces = MediaType. APPLICATION_JSON_VALUE
)
public class PaystackController {

private final PaystackService paystackService;

public PaystackController(PaystackService paystackService) {
this.paystackService = paystackService;
}

@PostMapping("/createplan")
public CreatePlanResponse createPlan (@Validated @RequestBody CreatePlanDto createPlanDto) throws Exception {
return paystackService.createPlan(createPlanDto);
}

@PostMapping("/initializepayment")
public InitializePaymentResponse initializePayment(@Validated @RequestBody InitializePaymentDto initializePaymentDto) throws Throwable {
return paystackService.initializePayment(initializePaymentDto);
}

@GetMapping("/verifypayment/{reference}/{plan}/{id}")
public PaymentVerification Response paymentVerification(@PathVariable(value = "reference") String reference,
@PathVariable (value = "plan") String plan,
@PathVariable(value = "id") Long id) throws Exception {
if (reference.isEmpty() || plan.isEmpty()) {
throw new Exception("reference, plan and id must be provided in path");
}
return paystackService.paymentVerification(reference, plan, id);
}
}

As you can see, the PaystackServiceImpl uses the Paystack secret key from the application properties file. Prior to using a live key that will be provided once you have submitted relevant business-related documentation, Paystack has provided you with this test key in order to test your implementation.

Visit the Paystack documentation page to further utilize the Paystack API.

There you go, You have successfully integrated the Paystack payment gateway. You can find all the code in this GitHub repository.

I’ll stop writing now because this article is already quite long. I’ll be releasing a follow-up, though, that will show you how to test your APIs using Postman.

Please leave any questions you may have in the comment section.

Connect with me on LinkedIn and Twitter.

Credit

Paystack Documentation

PS: It’s important to note that an additional variable needs to be introduced at the point of creating a plan called invoice_limit, i.e creating a variable called invoice_limit inside the CreatePlanDto, and the value should be 1 if the customer is to be charged only once; otherwise, if this variable is not provided, the customer will be charged continually.

--

--

Yahaya Yusuf

Hi 👋!! I am a Software Developer and welcome to my profile. I write new things I learn here.