FlowPe API Documentation
This document explains how to integrate with FlowPe APIs to initiate payments and manage transaction status.
Version: 1.0 | Last Updated: Nov 2026
1. API Security
All APIs in this document share common security measures:
- All API requests must use HTTPS protocol
- All API headers must include "secret-key", "client-id", "X-Request-Id", and "X-Timestamp" X-Timestamp should be in UTC
- "X-Request-Id" must contain a unique request identifier, and "X-Timestamp" must contain the request timestamp
- Use the API base URL provided by the FlowPe team
- Request body format is JSON
- Encryption key ENCRYPTION_KEY_BASE64 will be provided by the team
2. Initiate Payment URL API
This API is used to initiate an intent transaction.
Endpoint: POST /tur-core-main/api/v1/external/v1/payments/initiate
Request Parameters
| Key |
Description |
Mandatory |
| amount |
Payment amount as a whole number (e.g., 300) |
Yes |
| orderId |
Merchant-provided unique identifier, maximum 32 characters |
Yes |
| returnUrl |
Callback URL to redirect the user after payment completion |
Yes |
| username |
Customer name |
Yes |
| phoneNumber |
Customer phone number, including country code |
Yes |
| email |
Customer email address; must be a valid email address |
Yes |
| currency |
Currency code ,Optional (e.g., INR) |
No |
Plain Request (before encryption)
{
"orderId": "yuio1234567890ertyuio",
"amount": 300,
"returnUrl": "https://merchant.example.com/return",
"username": "Sample Customer",
"phoneNumber": "+919876543210",
"email": "customer@example.com",
"currency": "INR"
}
Encrypted Request Body
Encrypt the plain request using the provided encryption mechanism. Send the encrypted content in the encryptedRequest property.
{
"encryptedRequest": "{{extEncryptedBody}}"
}
Encrypted Response Body
The API returns the encrypted response in the response property. Decrypt its value to obtain the response described below.
{
"response": "{{encryptedResponse}}"
}
Decrypted Response
The decrypted response format remains unchanged.
{
"statusCode": 200,
"message": "Order Created Successfully",
"status": "OK",
"success": true,
"timestamp": "2025-08-26T07:52:23.243989861Z",
"data": {
"txnId": "5NNGB2SVNBY0NOZ3G809SEM",
"orderId": "Test010ff051988",
"amount": 300,
"sdkUrl": "https://payment-sdk.example.com/utr/payment?data=..."
}
}
3. Check Transaction Status API
This API is used to check the current status of a transaction.
Endpoint: POST /tur-core-main/api/v1/external/v1/payments/status
Request Parameters
| Key |
Description |
Example |
| orderId |
Order ID of the transaction; include it in the request before encryption |
Test001 |
Plain Request (before encryption)
{
"orderId": "Test001"
}
Encrypted Request Body
Encrypt the plain request above and send the encrypted content in the encryptedRequest property.
{
"encryptedRequest": "{{extEncryptedBody}}"
}
Encrypted Response
{
"response": "{{encryptedResponse}}"
}
Decrypted Response (unchanged)
{
"statusCode": 200,
"message": "Txn details fetched successfully",
"status": "OK",
"success": true,
"timestamp": "2025-03-21T15:35:43.797919902Z",
"data": {
"utr": "",
"orderId": "TestFlowPeTxn001",
"txnStatus": "Initiated / Approved / Rejected / Pending",
"amount": 2000
}
}
4. Webhook/Callback Format
Merchants need to provide a webhook URL to receive transaction status updates.
Callback Request Format
{
"encryptedData": "oRQPDxCyTU85s2NLjXd76VOrAjgMpFVG1QAOer846wzvbIsLULbMRk5CJsnLkGuTSpAKDyOk/cNzEtbp8ABfBah+9afSdlvu3vhHisPtgt1aen3cRCdPnIPcgQelKfNZ"
}
Decrypted Response Format
{
"utr": "402393486736",
"amount": 100,
"orderId": "Test123456789",
"txnStatus": "Approved / Rejected"
}
Note: Once the transaction status is updated, the merchant will receive the status. We expect a 200 status code with the message "OK" in response.
Important: Use the provided valid key to decrypt the callback you received.
5. Encryption and Decryption Logic
Requests and responses use AES-256-CBC with a 32-byte key. The key is supplied as Base64 and must decode to exactly 32 bytes. Generate a random 16-byte initialization vector (IV) for each encryption, prepend it to the ciphertext, then Base64-encode the combined bytes. AES-CBC uses PKCS#7 padding (called PKCS5Padding by Java).
Encrypt the plain request JSON, then send it as {"encryptedRequest":"BASE64_VALUE"}. The API returns {"response":"BASE64_VALUE"}; Base64-decode that value, split off the first 16 bytes as the IV, and decrypt the remaining ciphertext to get the original JSON response.
Replace the key placeholder in the examples with your own Base64-encoded key supplied by the API provider. Do not publish or commit real keys or credentials.
Code Examples
The examples contain only encryption and decryption logic, without endpoint-specific request fields. Supply your request JSON separately. Each example uses the same IV-prefixed format.
Java
JavaScript
PHP
Node.js
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class EncryptionExample {
private static final String KEY_BASE64 = "YOUR_BASE64_ENCODED_32_BYTE_KEY";
private static final int IV_LENGTH = 16;
public static void main(String[] args) {
try {
if (args.length > 0) {
System.out.println(encrypt(args[0], KEY_BASE64));
}
if (args.length > 1) {
System.out.println(decrypt(args[1], KEY_BASE64));
}
} catch (Exception e) {
System.err.println("Encryption or decryption failed: " + e.getMessage());
}
}
public static String encrypt(String plainText, String base64Key) throws Exception {
byte[] key = decodeKey(base64Key);
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
byte[] ciphertext = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
byte[] packed = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, packed, 0, iv.length);
System.arraycopy(ciphertext, 0, packed, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(packed);
}
public static String decrypt(String encryptedBase64, String base64Key) throws Exception {
byte[] packed = Base64.getDecoder().decode(encryptedBase64);
if (packed.length <= IV_LENGTH || (packed.length - IV_LENGTH) % 16 != 0) {
throw new IllegalArgumentException("Invalid encrypted data length");
}
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decodeKey(base64Key), "AES"),
new IvParameterSpec(packed, 0, IV_LENGTH));
byte[] decrypted = cipher.doFinal(packed, IV_LENGTH, packed.length - IV_LENGTH);
return new String(decrypted, StandardCharsets.UTF_8);
}
private static byte[] decodeKey(String base64Key) {
byte[] key = Base64.getDecoder().decode(base64Key);
if (key.length != 32) {
throw new IllegalArgumentException("Key must decode to 32 bytes");
}
return key;
}
}
const KEY_BASE64 = 'YOUR_BASE64_ENCODED_32_BYTE_KEY';
const toBytes = (base64) => Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const toBase64 = (bytes) => btoa(Array.from(bytes, b => String.fromCharCode(b)).join(''));
async function encryptAES(plainText, base64Key) {
const keyBytes = toBytes(base64Key);
if (keyBytes.length !== 32) throw new Error('AES-256 key must decode to 32 bytes');
const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-CBC', false, ['encrypt']);
const iv = crypto.getRandomValues(new Uint8Array(16));
const ciphertext = new Uint8Array(await crypto.subtle.encrypt(
{ name: 'AES-CBC', iv }, key, new TextEncoder().encode(plainText)
));
const packed = new Uint8Array(iv.length + ciphertext.length);
packed.set(iv);
packed.set(ciphertext, iv.length);
return toBase64(packed);
}
async function decryptAES(encryptedBase64, base64Key) {
const packed = toBytes(encryptedBase64);
const keyBytes = toBytes(base64Key);
if (keyBytes.length !== 32) throw new Error('AES-256 key must decode to 32 bytes');
if (packed.length <= 16 || (packed.length - 16) % 16 !== 0) {
throw new Error('Invalid encrypted data length');
}
const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-CBC', false, ['decrypt']);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv: packed.slice(0, 16) }, key, packed.slice(16)
);
return new TextDecoder().decode(decrypted);
}
<?php
const KEY_BASE64 = 'YOUR_BASE64_ENCODED_32_BYTE_KEY';
function decodeKey(string $base64Key): string {
$key = base64_decode($base64Key, true);
if ($key === false || strlen($key) !== 32) {
throw new RuntimeException('AES-256 key must decode to 32 bytes');
}
return $key;
}
function encryptAES(string $plainText, string $base64Key): string {
$key = decodeKey($base64Key);
$iv = random_bytes(16);
$ciphertext = openssl_encrypt($plainText, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
if ($ciphertext === false) {
throw new RuntimeException('Encryption failed');
}
return base64_encode($iv . $ciphertext);
}
function decryptAES(string $encryptedBase64, string $base64Key): string {
$packed = base64_decode($encryptedBase64, true);
if ($packed === false || strlen($packed) <= 16 || (strlen($packed) - 16) % 16 !== 0) {
throw new RuntimeException('Invalid encrypted data');
}
$decrypted = openssl_decrypt(
substr($packed, 16),
'AES-256-CBC',
decodeKey($base64Key),
OPENSSL_RAW_DATA,
substr($packed, 0, 16)
);
if ($decrypted === false) {
throw new RuntimeException('Decryption failed');
}
return $decrypted;
}
?>
const crypto = require('crypto');
const KEY_BASE64 = 'YOUR_BASE64_ENCODED_32_BYTE_KEY';
const IV_LENGTH = 16;
function decodeKey(base64Key) {
const key = Buffer.from(base64Key, 'base64');
if (key.length !== 32) {
throw new Error('AES-256 key must decode to 32 bytes');
}
return key;
}
function encryptAES(plainText, base64Key) {
const key = decodeKey(base64Key);
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
const ciphertext = Buffer.concat([
cipher.update(plainText, 'utf8'),
cipher.final()
]);
return Buffer.concat([iv, ciphertext]).toString('base64');
}
function decryptAES(encryptedBase64, base64Key) {
const packed = Buffer.from(encryptedBase64, 'base64');
if (packed.length <= IV_LENGTH || (packed.length - IV_LENGTH) % 16 !== 0) {
throw new Error('Invalid encrypted data length');
}
const decipher = crypto.createDecipheriv(
'aes-256-cbc',
decodeKey(base64Key),
packed.subarray(0, IV_LENGTH)
);
return Buffer.concat([
decipher.update(packed.subarray(IV_LENGTH)),
decipher.final()
]).toString('utf8');
}