Recurring Payments API
Create and manage recurring payment subscriptions via the Bhutan Payments REST API. Customers set up a payment method through a hosted checkout page, and Bhutan Payments automatically charges them on a schedule you define.
This API also covers the related Customer and Payment Method endpoints used alongside recurring payments.
Base URLs
Production: https://api.bhutanpayments.com
Staging: https://api-staging.bhutanpayments.com
Authentication
All endpoints require an API key passed in the X-API-Key header. Encrypted endpoints use the same AES encryption scheme described in the Payment API docs.
X-API-Key: your_api_key
Content-Type: application/json
Recurring Payment Endpoints
Create a Recurring Payment (Encrypted)
Creates a recurring payment and returns a hosted checkout URL where the customer sets up their payment method.
POST /api/recurring-payments
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 | integer | Yes | Amount in cents (e.g., 40000 = $400.00) |
| 2 | currency | string | Yes | Three-letter currency code (USD, BTN, INR) |
| 3 | interval | string | Yes | Billing interval: DAILY, WEEKLY, MONTHLY, YEARLY |
| 4 | intervalCount | integer | Yes | Number of intervals between charges (e.g., 3 = every 3 months) |
| 5 | successUrl | string | Yes | Redirect URL after successful payment method setup |
| 6 | cancelUrl | string | Yes | Redirect URL if the customer cancels setup |
| 7 | customerEmail | string | Yes | Customer's email address |
| 8 | customerName | string | No | Customer's display name (empty string if omitted) |
| 9 | externalId | string | No | Your internal reference ID for this subscription (empty string if omitted) |
| 10 | chargeImmediately | boolean | No | Whether to charge the customer immediately on setup (true/false) |
| 11 | description | string | No | Description shown on the subscription |
| 12 | metadata | string | No | JSON string of metadata (empty string if omitted) |
Example decrypted payload:
40000|USD|MONTHLY|1|https://yoursite.com/subscription/success|https://yoursite.com/subscription/cancel|jane@example.com|Jane Doe|sub_cust_123|true|Pro Plan|{"plan":"pro"}
Success Response (200):
{
"checkout_url": "https://pay.bhutanpayments.com/checkout/session_abc123...",
"encrypted_data": "<AES-encrypted-response>",
"recurring_payment_id": "01HQ3...",
"errors": []
}
Error Response:
{
"error": [
{
"code": "BAD_REQUEST",
"message": "Invalid currency"
}
]
}
Create a Recurring Payment (REST)
Alternative endpoint using the same encrypted request format.
POST /api/recurring-payments/create
Request and response format identical to POST /api/recurring-payments.
Get a Recurring Payment
Retrieve a single recurring payment by ID.
GET /api/recurring-payments/:id
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | UUID | Recurring payment ID |
Success Response (200):
{
"id": "01HQ3...",
"code": "RP-XK2M9",
"status": "ACTIVE",
"setup_status": "COMPLETED",
"amount": 2999,
"currency": "USD",
"interval": "MONTHLY",
"interval_count": 1,
"next_charge_date": "2025-05-13T00:00:00Z",
"start_date": "2025-04-13T00:00:00Z",
"end_date": null,
"charges_count": 1,
"max_charges": null,
"consecutive_failures": 0,
"max_retries": 3,
"last_charge_date": "2025-04-13T10:30:00Z",
"last_charge_status": "SUCCEEDED",
"description": null,
"external_id": "sub_cust_123",
"merchant_processing_fee": 2.5,
"payment_options": ["CREDIT_CARD", "DEBIT_CARD"],
"metadata": { "plan": "pro" },
"checkout_session_id": "session_abc123...",
"created_at": "2025-04-13T10:00:00Z",
"updated_at": "2025-04-13T10:30:00Z"
}
List Recurring Payments
Retrieve all recurring payments for your organization.
GET /api/recurring-payments
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
status | string | Filter by status (ACTIVE, PAUSED, CANCELED, FAILED, COMPLETED) |
first | integer | Number of results to return |
after | string | Cursor for pagination |
Success Response (200):
{
"edges": [
{
"node": { },
"cursor": "..."
}
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "..."
},
"totalCount": 42
}
Update a Recurring Payment
Update modifiable fields on an active subscription.
PUT /api/recurring-payments/:id
Request Body:
| Field | Type | Description |
|---|---|---|
description | string | Subscription description |
amount | integer | New amount in cents |
interval_count | integer | New interval count |
max_charges | integer | Maximum number of charges |
max_retries | integer | Maximum retry attempts on failure |
end_date | string | ISO 8601 end date |
metadata | object | Updated metadata |
{
"amount": 3999,
"description": "Pro Plan - Updated",
"metadata": { "plan": "pro_v2" }
}
Success Response (200): Returns the updated recurring payment object.
Pause a Recurring Payment
Temporarily stops billing. No charges are attempted while paused.
POST /api/recurring-payments/:id/pause
Success Response (200):
{
"recurringpayment": {
"id": "01HQ3...",
"status": "PAUSED"
},
"errors": []
}
Resume a Recurring Payment
Reactivates a paused subscription. The next charge date is recalculated from the resume date.
POST /api/recurring-payments/:id/resume
Success Response (200):
{
"recurringpayment": {
"id": "01HQ3...",
"status": "ACTIVE",
"next_charge_date": "2025-05-20T00:00:00Z"
},
"errors": []
}
Cancel a Recurring Payment
Permanently cancels a subscription. No further charges will be attempted.
POST /api/recurring-payments/:id/cancel
Success Response (200):
{
"recurringpayment": {
"id": "01HQ3...",
"status": "CANCELED"
},
"errors": []
}
Customer Endpoints
Customers are automatically created when a recurring payment is set up. You can also manage them directly.
Create a Customer
POST /api/customers
Request Body:
{
"email": "jane@example.com",
"name": "Jane Doe",
"phone": "+1234567890",
"external_id": "your_customer_123",
"metadata": { "tier": "premium" }
}
Get a Customer
GET /api/customers/:id
List Customers
GET /api/customers
Update a Customer
PUT /api/customers/:id
Delete a Customer
DELETE /api/customers/:id
List Customer Payment Methods
Get all payment methods for a specific customer.
GET /api/customers/:customerId/payment-methods
List Customer Recurring Payments
Get all recurring payments for a specific customer.
GET /api/customers/:customerId/recurring-payments
Payment Method Endpoints
Get a Payment Method
GET /api/payment-methods/:id
Success Response (200):
{
"id": "pm-uuid",
"type": "CREDIT_CARD",
"card_brand": "visa",
"card_last4": "4242",
"card_expiry_month": 12,
"card_expiry_year": 2027,
"status": "ACTIVE",
"is_default": true,
"created_at": "2025-04-13T10:00:00Z"
}
Set Default Payment Method
Set a payment method as the default for a customer. This is the method used for future recurring charges.
POST /api/payment-methods/:id/set-default
Detach a Payment Method
Remove a payment method from a customer. The payment method is detached from the payment provider and can no longer be used for charges.
DELETE /api/payment-methods/:id
Checkout Flow
After creating a recurring payment, redirect your customer to the checkout_url. The hosted checkout page handles the full payment method setup:
Your Server Bhutan Payments Customer Browser
| | |
|-- POST /api/recurring- -->| |
| payments | |
|<-- { checkout_url } -------| |
| | |
|------- redirect ---------->|----------- serve --------->|
| | Hosted Checkout Page |
| | |
| |<-- card details ------------|
| |-- Stripe SetupIntent ------>|
| |<-- payment method saved ----|
| | |
| |-- activate subscription |
| |-- redirect to success_url ->|
|<----------- customer returns to your site --------------|
Completion Redirect
After setup, the customer is redirected to your successUrl with an encrypted query parameter:
https://yoursite.com/subscription/success?data=<encrypted-response>
Decrypted response string (pipe-delimited):
| Position | Field | Description |
|---|---|---|
| 1 | statusCode | 000 for success |
| 2 | message | Status message (e.g., success) |
| 3 | checkoutURL | The hosted checkout URL for this session |
| 4 | recurringPaymentId | The ULID of the created recurring payment |
Subscription Statuses
| Status | Description |
|---|---|
ACTIVE | Subscription is active and charging on schedule |
PAUSED | Temporarily paused — no charges will be attempted |
COMPLETED | All charges completed (reached max_charges or end_date) |
CANCELED | Permanently cancelled by merchant or customer |
FAILED | Suspended due to consecutive payment failures |
Setup Statuses
| Status | Description |
|---|---|
PENDING | Awaiting customer to complete payment method setup via checkout |
COMPLETED | Payment method saved, subscription ready to charge |
EXPIRED | Checkout session expired before customer completed setup |
Billing Intervals
| Interval | Description | Example |
|---|---|---|
DAILY | Charge every N days | intervalCount: 1 → daily |
WEEKLY | Charge every N weeks | intervalCount: 2 → biweekly |
MONTHLY | Charge every N months | intervalCount: 1 → monthly |
YEARLY | Charge every N years | intervalCount: 1 → annually |
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 recurring payment ---
$payload = implode('|', [
'2999', // amount in cents
'USD',
'MONTHLY',
'1', // intervalCount
'https://yoursite.com/subscription/success',
'https://yoursite.com/subscription/cancel',
'jane@example.com',
'Jane Doe',
'sub_cust_123', // externalId
'true', // chargeImmediately
'Pro Plan', // description
json_encode(['plan' => 'pro']), // metadata
]);
$encryptedData = encryptPayload($payload, $encryptionMethod, $secretKey, $iv);
$ch = curl_init('https://api.bhutanpayments.com/api/recurring-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 to checkout
header('Location: ' . $checkoutUrl);
exit;
// --- Step 2: Handle completion redirect ---
// GET https://yoursite.com/subscription/success?data=<encrypted>
$decrypted = decryptPayload($_GET['data'], $encryptionMethod, $secretKey, $iv);
[$statusCode, $message, $checkoutURL, $recurringPaymentId] = explode('|', $decrypted);
if ($statusCode === '000') {
echo "Subscription activated! ID: $recurringPaymentId";
}
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 recurring payment ---
payload := strings.Join([]string{
"2999",
"USD",
"MONTHLY",
"1",
"https://yoursite.com/subscription/success",
"https://yoursite.com/subscription/cancel",
"jane@example.com",
"Jane Doe",
"sub_cust_123",
"true",
"Pro Plan",
`{"plan":"pro"}`,
}, "|")
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/recurring-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 completion redirect ---
// encryptedResponse comes from the ?data= query param after redirect
encryptedResponse := "..." // from query param
decrypted, _ := decryptPayload(encryptedResponse, secretKey, iv)
parts := strings.Split(decrypted, "|")
// parts: [statusCode, message, checkoutURL, recurringPaymentId]
if parts[0] == "000" {
fmt.Println("Subscription activated! ID:", parts[3])
}
}
Retry & Failure Handling
When a scheduled charge fails, the system:
- Records the failure and increments the
consecutive_failurescounter - Sends a
recurringpayments.payment_failedwebhook event (if you have webhooks configured) - Sends a failure notification email to the customer with a link to update their payment method
- Retries based on the
max_retriessetting (default: 3) - If all retries are exhausted, the subscription status changes to
FAILEDand arecurringpayments.cancelledevent is emitted
Notifications
The system automatically sends email notifications for the following events:
To customers: Setup completed, payment success (receipt), payment failed (with link to update payment method), subscription paused, subscription cancelled, subscription suspended.
To merchants: Subscription payment processed notifications.