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} &currency={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

ParameterValue
AlgorithmAES-256-GCM
Key Size256 bits (32 bytes)
IV/Nonce Size96 bits (12 bytes)
Auth Tag Size128 bits (16 bytes)
Key FormatBase64-encoded 32-byte key
Payload FormatBase64(IV + Ciphertext + AuthTag)

Endpoints

  1. Health Check

Check if the server is running.

GET https://url_of_payment_gateway_token_endpoint/health

Response:

  • 200 OK: "OK"

  1. 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:

CodeDescription
200Success - encrypted JWT token returned
401Authentication failed - invalid credentials
500Server 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:

  1. 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:

HeaderRequiredDescription
AuthorizationYesBearer {jwt_token} from authentication
ACCOUNT_VERIFICATIONOptionalSHA256 checksum: SHA256(account + direction + amount + currency)
Content-TypeYestext/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:

CodeDescription
200Success - encrypted verification result returned
401Unauthorized - missing or invalid JWT token
403Forbidden - checksum validation failed
500Server 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 ResponseHTTP CodeDescription
authentication-failed401Invalid username or password
unauthorized401Missing or invalid Authorization header
verification-failed403Checksum validation failed
server-configuration-error500Missing encryption key configuration
internal-server-error500Unexpected server error

Security Considerations

  1. Key Management: Store the symmetric encryption key securely (environment variables, secrets manager)
  2. HTTPS: Always use HTTPS in production
  3. JWT Expiration: Tokens expiration configurable
  4. 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/health

Verification Checks for Successful Cross-Border Payments

To ensure a 100% success rate on cross-border payments, conduct the following checks (but not limited to):

  1. Daily Withdrawal Limits
  • Verify the user's daily withdrawal limit status.
  • Ensure the withdrawal amount doesn’t exceed the daily limit.
  1. Politically Exposed Person (PEP) Status
  • Confirm if the account holder is designated as a PEP.
  1. Account Balance Verification
  • Validate account balance sufficiency for withdrawal.
  • Check if the balance supports the requested withdrawal amount.