Account Verification Endpoint
Overview
Here's a sample cross-border payment workflow between Country A and Country B.
Account Verification Endpoint
PIPEChain will request an account verification API endpoint during the onboarding process for your payment gateway.
Endpoint URL: https://url_of_payment_gateway_verification_endpoint
Request Methods:
- GET: Use this method when you prefer to transmit data via URL query parameters.
- POST: Use this method when you prefer to send data in the request body.
Depending on your choice of data transmission, you can choose to interact with the endpoint using either a GET or POST request.
Request:
To use the endpoint for a GET request, construct the URL with the following query parameters:
https://url_of_payment_gateway_verification_endpoint?
account={account_number} & destination={gateway_id} & bankCode={bank_code} &direction={credit|debit} &amount={amount} ¤cy={currency}
To use the endpoint for a POST request, create a JSON request body containing the following parameters:
JSON Request Body:
{
"account": "account_number",
"direction": "credit|debit",
"amount": "amount",
"currency": "currency"
}Where:
- account: The account query parameter will be replaced with the account number involved in the PIPEChain payment call.
- destination: Optional. Example: ng.bank.gtb.ngn. This field is necessary only for aggregator gateways that can terminate to other gateways.
- bankCode: Optional. Example: GTB or 1100998. This field is necessary only for aggregator gateways that can terminate to other gateways. This represents the internal code required for certain aggregators.
- direction: Whether it's a debit or a credit involved for the specified account in the payment call.
- amount: A string parameter that specifies the amount involved without fees in the payment call.
- currency: A string parameter that specifies the currency involved in the payment call.
Response:
A successful account name verification for both GET and POST request will return an HTTP code 200 with the following JSON response:
{
"name": "Name of account holder"
}If there is an error, the account name verification service should return a non-200 HTTP code, with the following JSON:
{
"error": "summary error message"
}Security
For extra security, all account verification endpoints are exclusively accessible through HTTPS to mitigate potential Bearer token exposure risks and must also implement one of the following Authentication mechanisms.
It is important to note that the payment gateway will supply the authentication mechanism to use when PIPEChain calls the endpoint.
There are three methods, and each has its own way of passing credentials or tokens in the HTTP request headers.
Method 1
OAuth2 Authorization: PIPEChain will use the access token provided during onboarding and pass it in the Authorization header of the HTTP request to perform the account verification.
curl --request GET \
--url 'https://url_of_payment_gateway_verification_endpoint' \
--header 'Authorization: Bearer OAUTH2_ACCESS_TOKEN' \
--header 'Content-Type: application/json'Method 2
Basic Authentication: For Basic Authentication, PIPEChain will use the username and password configured for PIPEChain and provided during onboarding in the Authorization header. These credentials (username:password) will be internally encoded in Base64 by PIPEChain and included in the header.
curl --request GET \
--url 'https://url_of_payment_gateway_verification_endpoint' \
--header 'Authorization: Basic <Base64_encoded_credentials>' \
--header 'Content-Type: application/json'Method 3
Dynamic Token Authentication: This authentication mechanism requires a token endpoint. All request/response payloads are encrypted using AES-256-GCM encryption. PIPEChain will obtain authentication tokens from the token endpoint using the steps below.
Encryption Details
| Parameter | Value |
|---|---|
| Algorithm | AES-256-GCM |
| Key Size | 256 bits (32 bytes) |
| IV/Nonce Size | 96 bits (12 bytes) |
| Auth Tag Size | 128 bits (16 bytes) |
| Key Format | Base64-encoded 32-byte key |
| Payload Format | Base64(IV + Ciphertext + AuthTag) |
Endpoints
- Health Check
Check if the server is running.
GET https://url_of_payment_gateway_token_endpoint/health
Response:
200 OK:"OK"
- Authentication
Authenticate and receive a JWT token.
POST https://url_of_payment_gateway_token_endpoint
Content-Type: text/plain
Request Body (before encryption):
{
"username": "string",
"password": "string"
}Request Body (actual): AES-256-GCM encrypted, Base64 encoded
Response Body (decrypted):
{
"jwttoken": "string"
}Response Codes:
| Code | Description |
|---|---|
| 200 | Success - encrypted JWT token returned |
| 401 | Authentication failed - invalid credentials |
| 500 | Server error |
Upon a successful request to the token endpoint using the credentials provided during onboarding, PIPEChain will receive an access token that will be used to call the account verification endpoint.
To use this access token for authentication when making requests to your account verification endpoint, PIPEChain will include it in the Authorization header:
- Account Verification
Verify an account for transactions. Requires JWT token from authentication.
POST https://url_of_payment_gateway_verification_endpoint
Content-Type: text/plain
Authorization: Bearer {jwt_token}
ACCOUNT_VERIFICATION: {sha256_checksum}
Headers:
| Header | Required | Description |
|---|---|---|
| Authorization | Yes | Bearer {jwt_token} from authentication |
| ACCOUNT_VERIFICATION | Optional | SHA256 checksum: SHA256(account + direction + amount + currency) |
| Content-Type | Yes | text/plain |
Request Body (before encryption):
{
"account": "string",
"direction": "credit|debit",
"amount": "string",
"destination": "string",
"bankCode": "string",
"currency": "string"
}Response Body (decrypted):
{
"status": "success|failed",
"message": "string",
"accountName": "string",
"accountNumber": "string",
"bankCode": "string",
"currency": "string",
"verified": true|false,
"name": "string"
}Response Codes:
| Code | Description |
|---|---|
| 200 | Success - encrypted verification result returned |
| 401 | Unauthorized - missing or invalid JWT token |
| 403 | Forbidden - checksum validation failed |
| 500 | Server error |
Implementation Examples of Dynamic Token Authentication
Java Implementation
AES256GCMUtil.java
package com.example.auth.util;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
public class AES256GCMUtil {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 128;
/**
* Encrypts plaintext using AES-256-GCM.
* Output format: Base64(IV + Ciphertext + AuthTag)
*/
public static String encrypt(String plaintext, String keyBase64) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// Combine IV + ciphertext (includes auth tag)
byte[] combined = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(combined);
}
/**
* Decrypts AES-256-GCM encrypted text.
* Input format: Base64(IV + Ciphertext + AuthTag)
*/
public static String decrypt(String encryptedBase64, String keyBase64) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
byte[] combined = Base64.getDecoder().decode(encryptedBase64);
byte[] iv = new byte[GCM_IV_LENGTH];
byte[] ciphertext = new byte[combined.length - GCM_IV_LENGTH];
System.arraycopy(combined, 0, iv, 0, GCM_IV_LENGTH);
System.arraycopy(combined, GCM_IV_LENGTH, ciphertext, 0, ciphertext.length);
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
}
}AuthClient.java
package com.example.auth.client;
import com.example.auth.util.AES256GCMUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public class AuthClient {
private final String baseUrl;
private final String symmetricKey;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
public AuthClient(String baseUrl, String symmetricKey) {
this.baseUrl = baseUrl;
this.symmetricKey = symmetricKey;
this.httpClient = HttpClient.newHttpClient();
this.objectMapper = new ObjectMapper();
}
/**
* Authenticate and get JWT token.
*/
public String authenticate(String username, String password) throws Exception {
// Create request payload
String requestJson = String.format(
"{\"username\":\"%s\",\"password\":\"%s\"}", username, password);
// Encrypt payload
String encryptedRequest = AES256GCMUtil.encrypt(requestJson, symmetricKey);
// Send request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/ts/authenticate"))
.header("Content-Type", "text/plain")
.POST(HttpRequest.BodyPublishers.ofString(encryptedRequest))
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Authentication failed: " + response.body());
}
// Decrypt response
String decryptedResponse = AES256GCMUtil.decrypt(response.body(), symmetricKey);
// Extract JWT token
var responseMap = objectMapper.readValue(decryptedResponse, java.util.Map.class);
return (String) responseMap.get("jwttoken");
}
/**
* Verify account.
*/
public AccountVerificationResponse verifyAccount(
String jwtToken,
String account,
String direction,
String amount,
String destination,
String bankCode,
String currency) throws Exception {
// Create request payload
String requestJson = objectMapper.writeValueAsString(new AccountVerificationRequest(
account, direction, amount, destination, bankCode, currency));
// Compute checksum: SHA256(account + direction + amount + currency)
String checksumInput = account + direction + amount + currency;
String checksum = sha256Hex(checksumInput);
// Encrypt payload
String encryptedRequest = AES256GCMUtil.encrypt(requestJson, symmetricKey);
// Send request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/api/account/verify"))
.header("Content-Type", "text/plain")
.header("Authorization", "Bearer " + jwtToken)
.header("ACCOUNT_VERIFICATION", checksum)
.POST(HttpRequest.BodyPublishers.ofString(encryptedRequest))
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("Verification failed: " + response.body());
}
// Decrypt response
String decryptedResponse = AES256GCMUtil.decrypt(response.body(), symmetricKey);
return objectMapper.readValue(decryptedResponse, AccountVerificationResponse.class);
}
private String sha256Hex(String input) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
}
// Request/Response classes
public record AccountVerificationRequest(
String account, String direction, String amount,
String destination, String bankCode, String currency) {}
public record AccountVerificationResponse(
String status, String message, String accountName,
String accountNumber, String bankCode, String currency,
boolean verified, String name) {}
}Usage Example (Java)
public class Main {
public static void main(String[] args) throws Exception {
String baseUrl = "http://localhost:8080";
String symmetricKey = "NllmV3FUcEVURW8wSWFyd0hrTWdJTzJvdWoyN0RFcVc=";
AuthClient client = new AuthClient(baseUrl, symmetricKey);
// Step 1: Authenticate
String jwtToken = client.authenticate("authserver", "authserver_password");
System.out.println("JWT Token: " + jwtToken);
// Step 2: Verify Account
var result = client.verifyAccount(
jwtToken,
"1234567890", // account
"credit", // direction
"1000.00", // amount
"DEST123", // destination
"058", // bankCode
"NGN" // currency
);
System.out.println("Verification Status: " + result.status());
System.out.println("Account Name: " + result.accountName());
System.out.println("Verified: " + result.verified());
}
}.NET Implementation
AES256GCMUtil.cs
using System;
using System.Security.Cryptography;
using System.Text;
namespace AuthServer.Util
{
public static class AES256GCMUtil
{
private const int GCM_IV_LENGTH = 12;
private const int GCM_TAG_LENGTH = 16;
/// <summary>
/// Encrypts plaintext using AES-256-GCM.
/// Output format: Base64(IV + Ciphertext + AuthTag)
/// </summary>
public static string Encrypt(string plaintext, string keyBase64)
{
byte[] keyBytes = Convert.FromBase64String(keyBase64);
byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext);
byte[] iv = new byte[GCM_IV_LENGTH];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(iv);
byte[] ciphertext = new byte[plaintextBytes.Length];
byte[] tag = new byte[GCM_TAG_LENGTH];
using var aesGcm = new AesGcm(keyBytes, GCM_TAG_LENGTH);
aesGcm.Encrypt(iv, plaintextBytes, ciphertext, tag);
// Combine IV + Ciphertext + Tag
byte[] combined = new byte[iv.Length + ciphertext.Length + tag.Length];
Buffer.BlockCopy(iv, 0, combined, 0, iv.Length);
Buffer.BlockCopy(ciphertext, 0, combined, iv.Length, ciphertext.Length);
Buffer.BlockCopy(tag, 0, combined, iv.Length + ciphertext.Length, tag.Length);
return Convert.ToBase64String(combined);
}
/// <summary>
/// Decrypts AES-256-GCM encrypted text.
/// Input format: Base64(IV + Ciphertext + AuthTag)
/// </summary>
public static string Decrypt(string encryptedBase64, string keyBase64)
{
byte[] keyBytes = Convert.FromBase64String(keyBase64);
byte[] combined = Convert.FromBase64String(encryptedBase64);
// Extract IV, Ciphertext, and Tag
byte[] iv = new byte[GCM_IV_LENGTH];
byte[] tag = new byte[GCM_TAG_LENGTH];
byte[] ciphertext = new byte[combined.Length - GCM_IV_LENGTH - GCM_TAG_LENGTH];
Buffer.BlockCopy(combined, 0, iv, 0, GCM_IV_LENGTH);
Buffer.BlockCopy(combined, GCM_IV_LENGTH, ciphertext, 0, ciphertext.Length);
Buffer.BlockCopy(combined, combined.Length - GCM_TAG_LENGTH, tag, 0, GCM_TAG_LENGTH);
byte[] plaintext = new byte[ciphertext.Length];
using var aesGcm = new AesGcm(keyBytes, GCM_TAG_LENGTH);
aesGcm.Decrypt(iv, ciphertext, tag, plaintext);
return Encoding.UTF8.GetString(plaintext);
}
}
}AuthClient.cs
using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using AuthServer.Util;
namespace Auth.Client
{
public class AuthClient
{
private readonly string _baseUrl;
private readonly string _symmetricKey;
private readonly HttpClient _httpClient;
private readonly JsonSerializerOptions _jsonOptions;
public AuthClient(string baseUrl, string symmetricKey)
{
_baseUrl = baseUrl;
_symmetricKey = symmetricKey;
_httpClient = new HttpClient();
_jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true
};
}
/// <summary>
/// Authenticate and get JWT token.
/// </summary>
public async Task<string> AuthenticateAsync(string username, string password)
{
// Create request payload
var requestObj = new { username, password };
string requestJson = JsonSerializer.Serialize(requestObj, _jsonOptions);
// Encrypt payload
string encryptedRequest = AES256GCMUtil.Encrypt(requestJson, _symmetricKey);
// Send request
var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/ts/authenticate")
{
Content = new StringContent(encryptedRequest, Encoding.UTF8, "text/plain")
};
var response = await _httpClient.SendAsync(request);
string responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Authentication failed: {responseBody}");
}
// Decrypt response
string decryptedResponse = AES256GCMUtil.Decrypt(responseBody, _symmetricKey);
var responseObj = JsonSerializer.Deserialize<AuthResponse>(decryptedResponse, _jsonOptions);
return responseObj?.Jwttoken ?? throw new Exception("No token in response");
}
/// <summary>
/// Verify account.
/// </summary>
public async Task<AccountVerificationResponse> VerifyAccountAsync(
string jwtToken,
string account,
string direction,
string amount,
string destination,
string bankCode,
string currency)
{
// Create request payload
var requestObj = new AccountVerificationRequest
{
Account = account,
Direction = direction,
Amount = amount,
Destination = destination,
BankCode = bankCode,
Currency = currency
};
string requestJson = JsonSerializer.Serialize(requestObj, _jsonOptions);
// Compute checksum: SHA256(account + direction + amount + currency)
string checksumInput = account + direction + amount + currency;
string checksum = ComputeSha256Hex(checksumInput);
// Encrypt payload
string encryptedRequest = AES256GCMUtil.Encrypt(requestJson, _symmetricKey);
// Send request
var request = new HttpRequestMessage(HttpMethod.Post, $"{_baseUrl}/api/account/verify")
{
Content = new StringContent(encryptedRequest, Encoding.UTF8, "text/plain")
};
request.Headers.Add("Authorization", $"Bearer {jwtToken}");
request.Headers.Add("ACCOUNT_VERIFICATION", checksum);
var response = await _httpClient.SendAsync(request);
string responseBody = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new Exception($"Verification failed: {responseBody}");
}
// Decrypt response
string decryptedResponse = AES256GCMUtil.Decrypt(responseBody, _symmetricKey);
return JsonSerializer.Deserialize<AccountVerificationResponse>(decryptedResponse, _jsonOptions)
?? throw new Exception("Failed to parse response");
}
private static string ComputeSha256Hex(string input)
{
using var sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
}
// Request/Response Models
public class AuthResponse
{
[JsonPropertyName("jwttoken")]
public string? Jwttoken { get; set; }
}
public class AccountVerificationRequest
{
public string Account { get; set; } = string.Empty;
public string Direction { get; set; } = string.Empty;
public string Amount { get; set; } = string.Empty;
public string Destination { get; set; } = string.Empty;
public string BankCode { get; set; } = string.Empty;
public string Currency { get; set; } = string.Empty;
}
public class AccountVerificationResponse
{
public string Status { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public string AccountName { get; set; } = string.Empty;
public string AccountNumber { get; set; } = string.Empty;
public string BankCode { get; set; } = string.Empty;
public string Currency { get; set; } = string.Empty;
public bool Verified { get; set; }
public string Name { get; set; } = string.Empty;
}
}Usage Example (.NET)
using System;
using System.Threading.Tasks;
using Auth.Client;
class Program
{
static async Task Main(string[] args)
{
string baseUrl = "http://localhost:8080";
string symmetricKey = "NllmV3FUcEVURW8wSWFyd0hrTWdJTzJvdWoyN0RFcVc=";
var client = new AuthClient(baseUrl, symmetricKey);
try
{
// Step 1: Authenticate
string jwtToken = await client.AuthenticateAsync("ISW", "ISW_PASSWORD");
Console.WriteLine($"JWT Token: {jwtToken}");
// Step 2: Verify Account
var result = await client.VerifyAccountAsync(
jwtToken,
account: "1234567890",
direction: "credit",
amount: "1000.00",
destination: "DEST123",
bankCode: "058",
currency: "NGN"
);
Console.WriteLine($"Verification Status: {result.Status}");
Console.WriteLine($"Account Name: {result.AccountName}");
Console.WriteLine($"Verified: {result.Verified}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}.NET Project File (Auth.Client.csproj)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>Error Handling
| Error Response | HTTP Code | Description |
|---|---|---|
authentication-failed | 401 | Invalid username or password |
unauthorized | 401 | Missing or invalid Authorization header |
verification-failed | 403 | Checksum validation failed |
server-configuration-error | 500 | Missing encryption key configuration |
internal-server-error | 500 | Unexpected server error |
Security Considerations
- Key Management: Store the symmetric encryption key securely (environment variables, secrets manager)
- HTTPS: Always use HTTPS in production
- JWT Expiration: Tokens expiration configurable
- Checksum Validation: ACCOUNT_VERIFICATION header provides integrity verification
Testing sample implementation with cURL
Authentication
# First, encrypt your payload (use the Java/C# utility or an online tool)
# Payload: {"username":"ISW","password":"ISW_PASSWORD"}
curl -X POST http://localhost:8080/ts/authenticate \
-H "Content-Type: text/plain" \
-d 'YOUR_ENCRYPTED_PAYLOAD_HERE'Health Check
curl http://localhost:8080/ts/healthVerification Checks for Successful Cross-Border Payments
To ensure a 100% success rate on cross-border payments, conduct the following checks (but not limited to):
- Daily Withdrawal Limits
- Verify the user's daily withdrawal limit status.
- Ensure the withdrawal amount doesn’t exceed the daily limit.
- Politically Exposed Person (PEP) Status
- Confirm if the account holder is designated as a PEP.
- Account Balance Verification
- Validate account balance sufficiency for withdrawal.
- Check if the balance supports the requested withdrawal amount.
Updated about 1 month ago