Payment API
Accept one-time payments from your customers using the Bhutan Payments REST API. This guide covers creating payment sessions, processing payments via hosted checkout, and handling payment results.
Base URLs
Production: https://api.bhutanpayments.com
Staging: https://api-staging.bhutanpayments.com
Authentication
All API requests require an API key passed in the X-API-Key header. You can generate API keys from the Bhutan Payments dashboard under Settings → API Keys.
Each API key has an associated secret key used for encrypting and decrypting request/response payloads. Store your secret key securely — it is shown only once at creation time.
X-API-Key: your_api_key
Content-Type: application/json
Encryption
All payment creation requests use AES encryption for data security. The request body contains a single encrypted-data field with your AES-encrypted payload. Responses that contain sensitive data are similarly encrypted.
Encryption flow:
- Use your
SECRET_KEYdirectly as the AES key - Derive the IV from the first 16 characters of your
SECRET_KEY - Encrypt the pipe-delimited payload string using AES-256-CBC
- Send the encrypted payload as the
encrypted-datafield
Endpoints
Create a Payment
Creates a payment session and returns a hosted checkout URL where your customer completes payment.
POST /createPayment
Alternate endpoint:
POST /api/payments
Both endpoints are functionally identical. The /api/payments path is the preferred convention.
Request Headers:
| Header | Value |
|---|---|
X-API-Key | Your API key |
Content-Type | application/json |
Request Body:
{
"encrypted-data": "<AES-encrypted-payload>"
}
Decrypted payload format (pipe-delimited):
The payload is a pipe-delimited string, not JSON. Fields must appear in this exact order:
| Position | Field | Type | Required | Description |
|---|---|---|---|---|
| 1 | amount | float | Yes | Payment amount (e.g., 100.00) |
| 2 | currency | string | Yes | Three-letter currency code (USD, BTN, INR) |
| 3 | responseUrl | string | Yes | URL to redirect the customer after successful payment |
| 4 | cancelUrl | string | Yes | URL to redirect the customer if they cancel |
| 5 | orderId | string | Yes | Your internal order identifier |
| 6 | metadata | string | No | JSON string of metadata (optional, omit if unused) |
Example decrypted payload:
100.00|USD|https://yoursite.com/payment/success|https://yoursite.com/payment/cancel|ORDER-2025-001
Success Response (200):
{
"code": "000",
"checkout_url": "https://pay.bhutanpayments.com/checkout/a1b2c3d4-...",
"encrypted-data": "<AES-encrypted-response>"
}
The encrypted-data in the response decodes to a pipe-delimited string:
statusCode|message|checkoutURL
Example:
000|success|https://pay.bhutanpayments.com/checkout/a1b2c3d4-...
Error Response (4xx/5xx):
{
"code": "100",
"message": "Invalid authentication credentials",
"error": [
{
"code": "UNAUTHORIZED",
"message": "Invalid authentication credentials"
}
]
}
Integration Flow
Step 1: Create Payment (Server-Side)
Your backend calls POST /api/payments with the encrypted payload and receives a checkout_url.
Your Server Bhutan Payments
| |
|-- POST /api/payments ----->|
| { "encrypted-data" } |
| |
|<--- 200 -------------------|
| { checkout_url, encrypted-data } |
Step 2: Redirect to Checkout
Redirect your customer's browser to the checkout_url. The hosted checkout page handles everything:
- Displays payment amount and merchant details
- Shows available payment methods (based on your merchant configuration)
- Tokenizes card details securely via Stripe.js or Authorize.Net Accept.js
- Handles 3D Secure authentication if required
- Calculates and displays fee breakdown
Step 3: Customer Completes Payment
The customer enters their payment details and submits. The hosted checkout processes the payment through the configured gateway.
Step 4: Redirect Back
After payment completes (or is cancelled), the customer is redirected to your URL with an encrypted result:
Success redirect:
https://yoursite.com/payment/success?data=<encrypted-response>
Cancel redirect:
https://yoursite.com/payment/cancel
Decrypted success response string (pipe-delimited):
| Position | Field | Description |
|---|---|---|
| 1 | status | 000 for success |
| 2 | amount | Payment amount |
| 3 | code | Payment code (e.g., CK-274232) |
| 4 | accessKey | API payment access key (UUID) |
| 5 | transactionId | Gateway transaction ID (e.g., Stripe pi_...) |
| 6 | orderId | Your original order identifier |
| 7 | metadata | JSON string of your original metadata (if provided) |
Supported Payment Types
Availability depends on your merchant configuration and active payment provider setup:
| Type | Description |
|---|---|
CREDIT_CARD | Visa, Mastercard credit cards |
DEBIT_CARD | Visa, Mastercard debit cards |
ACH | US bank transfer (ACH debit) |
US_BANK_ACCOUNT | US bank account payments |
Fee Breakdown
Every payment includes a fee breakdown showing how the total was calculated:
| Field | Description |
|---|---|
subtotal | Original payment amount |
merchantFeePercent | Merchant processing fee rate (%) |
merchantFeeAmount | Merchant fee in currency |
subtotalAfterMerchant | Amount after merchant fee is applied |
gatewayFeeType | Fee type (PERCENTAGE or FLAT) |
gatewayFeeRate | Gateway fee rate |
gatewayFeeAmount | Gateway fee in currency |
platformFeePercent | Platform fee rate (%) |
platformFeeAmount | Platform fee in currency |
totalAmount | Final amount charged to customer |
Error Codes
| Code | HTTP Status | Description |
|---|---|---|
BAD_REQUEST | 400 | Invalid request body or encrypted data format |
UNAUTHORIZED | 401 | Invalid or inactive API key |
INVALID_INPUT | 422 | Missing or invalid required fields in decrypted data |
MERCHANT_BANK_ACCOUNT_NOT_APPROVED | 403 | Bank account pending approval — payments cannot be created |
INTERNAL_SERVER_ERROR | 500 | Server-side error |
Payment Statuses
Payments move through the following statuses during their lifecycle:
| Status | Description |
|---|---|
PENDING | Payment created, awaiting processing |
PROCESSING | Payment is being processed by the gateway |
SUCCEEDED | Payment completed successfully |
FAILED | Payment failed (see webhook error_code / decline_code for details) |
CANCELED | Payment was cancelled by the customer |
REQUIRES_ACTION | Additional customer action required (e.g., 3D Secure verification) |
API Request Logging
All REST API requests are automatically logged with the following details for debugging and audit purposes:
- Request method, path, headers (auth headers redacted), and body (sensitive fields redacted)
- Response status code, headers, and body
- Latency in milliseconds
- Client IP address and user agent
- Request ID (returned in the
X-Request-IDresponse header)
You can view API logs in the Bhutan Payments dashboard.
Code Examples
PHP
<?php
$secretKey = getenv('SECRET_KEY');
$encryptionMethod = getenv('ENCRYPTION_METHOD'); // AES-256-CBC
$apiKey = getenv('API_KEY');
$iv = substr($secretKey, 0, 16);
function encryptPayload(string $plaintext, string $method, string $key, string $iv): string {
return base64_encode(openssl_encrypt($plaintext, $method, $key, OPENSSL_RAW_DATA, $iv));
}
function decryptPayload(string $encrypted, string $method, string $key, string $iv): string {
return openssl_decrypt(base64_decode($encrypted), $method, $key, OPENSSL_RAW_DATA, $iv);
}
// --- Step 1: Encrypt and send payment ---
$orderId = 'ORDER-' . date('Ymd-His');
$payload = implode('|', [
'100.00',
'USD',
'https://yoursite.com/payment/success',
'https://yoursite.com/payment/cancel',
$orderId,
]);
$encryptedData = encryptPayload($payload, $encryptionMethod, $secretKey, $iv);
$ch = curl_init('https://api.bhutanpayments.com/api/payments');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-API-Key: ' . $apiKey,
],
CURLOPT_POSTFIELDS => json_encode(['encrypted-data' => $encryptedData]),
]);
$response = json_decode(curl_exec($ch), true);
$checkoutUrl = $response['checkout_url'];
curl_close($ch);
// Redirect customer
header('Location: ' . $checkoutUrl);
exit;
// --- Step 2: Handle redirect back ---
// GET https://yoursite.com/payment/success?data=<encrypted>
$decrypted = decryptPayload($_GET['data'], $encryptionMethod, $secretKey, $iv);
[$status, $amount, $code, $accessKey, $transactionId, $orderId, $metadata] = explode('|', $decrypted);
if ($status === '000') {
echo "Payment successful! Transaction: $transactionId";
}
Go
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
)
func pkcs7Pad(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
pad := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, pad...)
}
func pkcs7Unpad(data []byte) []byte {
padding := int(data[len(data)-1])
return data[:len(data)-padding]
}
func encryptPayload(plaintext, secretKey, iv string) (string, error) {
block, err := aes.NewCipher([]byte(secretKey))
if err != nil {
return "", err
}
padded := pkcs7Pad([]byte(plaintext), aes.BlockSize)
encrypted := make([]byte, len(padded))
cipher.NewCBCEncrypter(block, []byte(iv)).CryptBlocks(encrypted, padded)
return base64.StdEncoding.EncodeToString(encrypted), nil
}
func decryptPayload(encryptedB64, secretKey, iv string) (string, error) {
data, err := base64.StdEncoding.DecodeString(encryptedB64)
if err != nil {
return "", err
}
block, err := aes.NewCipher([]byte(secretKey))
if err != nil {
return "", err
}
cipher.NewCBCDecrypter(block, []byte(iv)).CryptBlocks(data, data)
return string(pkcs7Unpad(data)), nil
}
func main() {
secretKey := os.Getenv("SECRET_KEY")
apiKey := os.Getenv("API_KEY")
iv := secretKey[:16]
// --- Step 1: Encrypt and send payment ---
payload := strings.Join([]string{
"100.00",
"USD",
"https://yoursite.com/payment/success",
"https://yoursite.com/payment/cancel",
"ORDER-2025-001",
}, "|")
encryptedData, err := encryptPayload(payload, secretKey, iv)
if err != nil {
panic(err)
}
body, _ := json.Marshal(map[string]string{"encrypted-data": encryptedData})
req, _ := http.NewRequest("POST", "https://api.bhutanpayments.com/api/payments", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", apiKey)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
checkoutURL := result["checkout_url"].(string)
fmt.Println("Redirect customer to:", checkoutURL)
// --- Step 2: Decrypt redirect response ---
// encryptedResponse comes from the ?data= query param after redirect
encryptedResponse := "..." // from query param
decrypted, _ := decryptPayload(encryptedResponse, secretKey, iv)
parts := strings.Split(decrypted, "|")
// parts: [status, amount, code, accessKey, transactionId, orderId, metadata]
if parts[0] == "000" {
fmt.Println("Payment successful! Transaction:", parts[4])
}
}
Testing
Use TEST environment API keys to create test payments without processing real charges. Test payments flow through the same checkout experience but use Stripe test mode.
Test card numbers:
| Card | Number |
|---|---|
| Visa (success) | 4242 4242 4242 4242 |
| Visa (decline) | 4000 0000 0000 0002 |
| 3DS required | 4000 0025 0000 3155 |
Use any future expiry date and any 3-digit CVC.