Skip to main content

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:

  1. Use your SECRET_KEY directly as the AES key
  2. Derive the IV from the first 16 characters of your SECRET_KEY
  3. Encrypt the pipe-delimited payload string using AES-256-CBC
  4. Send the encrypted payload as the encrypted-data field

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:

HeaderValue
X-API-KeyYour API key
Content-Typeapplication/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:

PositionFieldTypeRequiredDescription
1amountfloatYesPayment amount (e.g., 100.00)
2currencystringYesThree-letter currency code (USD, BTN, INR)
3responseUrlstringYesURL to redirect the customer after successful payment
4cancelUrlstringYesURL to redirect the customer if they cancel
5orderIdstringYesYour internal order identifier
6metadatastringNoJSON 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):

PositionFieldDescription
1status000 for success
2amountPayment amount
3codePayment code (e.g., CK-274232)
4accessKeyAPI payment access key (UUID)
5transactionIdGateway transaction ID (e.g., Stripe pi_...)
6orderIdYour original order identifier
7metadataJSON string of your original metadata (if provided)

Supported Payment Types​

Availability depends on your merchant configuration and active payment provider setup:

TypeDescription
CREDIT_CARDVisa, Mastercard credit cards
DEBIT_CARDVisa, Mastercard debit cards
ACHUS bank transfer (ACH debit)
US_BANK_ACCOUNTUS bank account payments

Fee Breakdown​

Every payment includes a fee breakdown showing how the total was calculated:

FieldDescription
subtotalOriginal payment amount
merchantFeePercentMerchant processing fee rate (%)
merchantFeeAmountMerchant fee in currency
subtotalAfterMerchantAmount after merchant fee is applied
gatewayFeeTypeFee type (PERCENTAGE or FLAT)
gatewayFeeRateGateway fee rate
gatewayFeeAmountGateway fee in currency
platformFeePercentPlatform fee rate (%)
platformFeeAmountPlatform fee in currency
totalAmountFinal amount charged to customer

Error Codes​

CodeHTTP StatusDescription
BAD_REQUEST400Invalid request body or encrypted data format
UNAUTHORIZED401Invalid or inactive API key
INVALID_INPUT422Missing or invalid required fields in decrypted data
MERCHANT_BANK_ACCOUNT_NOT_APPROVED403Bank account pending approval — payments cannot be created
INTERNAL_SERVER_ERROR500Server-side error

Payment Statuses​

Payments move through the following statuses during their lifecycle:

StatusDescription
PENDINGPayment created, awaiting processing
PROCESSINGPayment is being processed by the gateway
SUCCEEDEDPayment completed successfully
FAILEDPayment failed (see webhook error_code / decline_code for details)
CANCELEDPayment was cancelled by the customer
REQUIRES_ACTIONAdditional 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-ID response 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:

CardNumber
Visa (success)4242 4242 4242 4242
Visa (decline)4000 0000 0000 0002
3DS required4000 0025 0000 3155

Use any future expiry date and any 3-digit CVC.