Skip to main content

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:

PositionFieldTypeRequiredDescription
1amountintegerYesAmount in cents (e.g., 40000 = $400.00)
2currencystringYesThree-letter currency code (USD, BTN, INR)
3intervalstringYesBilling interval: DAILY, WEEKLY, MONTHLY, YEARLY
4intervalCountintegerYesNumber of intervals between charges (e.g., 3 = every 3 months)
5successUrlstringYesRedirect URL after successful payment method setup
6cancelUrlstringYesRedirect URL if the customer cancels setup
7customerEmailstringYesCustomer's email address
8customerNamestringNoCustomer's display name (empty string if omitted)
9externalIdstringNoYour internal reference ID for this subscription (empty string if omitted)
10chargeImmediatelybooleanNoWhether to charge the customer immediately on setup (true/false)
11descriptionstringNoDescription shown on the subscription
12metadatastringNoJSON 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:

ParameterTypeDescription
idUUIDRecurring 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:

ParameterTypeDescription
statusstringFilter by status (ACTIVE, PAUSED, CANCELED, FAILED, COMPLETED)
firstintegerNumber of results to return
afterstringCursor 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:

FieldTypeDescription
descriptionstringSubscription description
amountintegerNew amount in cents
interval_countintegerNew interval count
max_chargesintegerMaximum number of charges
max_retriesintegerMaximum retry attempts on failure
end_datestringISO 8601 end date
metadataobjectUpdated 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):

PositionFieldDescription
1statusCode000 for success
2messageStatus message (e.g., success)
3checkoutURLThe hosted checkout URL for this session
4recurringPaymentIdThe ULID of the created recurring payment

Subscription Statuses​

StatusDescription
ACTIVESubscription is active and charging on schedule
PAUSEDTemporarily paused — no charges will be attempted
COMPLETEDAll charges completed (reached max_charges or end_date)
CANCELEDPermanently cancelled by merchant or customer
FAILEDSuspended due to consecutive payment failures

Setup Statuses​

StatusDescription
PENDINGAwaiting customer to complete payment method setup via checkout
COMPLETEDPayment method saved, subscription ready to charge
EXPIREDCheckout session expired before customer completed setup

Billing Intervals​

IntervalDescriptionExample
DAILYCharge every N daysintervalCount: 1 → daily
WEEKLYCharge every N weeksintervalCount: 2 → biweekly
MONTHLYCharge every N monthsintervalCount: 1 → monthly
YEARLYCharge every N yearsintervalCount: 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:

  1. Records the failure and increments the consecutive_failures counter
  2. Sends a recurringpayments.payment_failed webhook event (if you have webhooks configured)
  3. Sends a failure notification email to the customer with a link to update their payment method
  4. Retries based on the max_retries setting (default: 3)
  5. If all retries are exhausted, the subscription status changes to FAILED and a recurringpayments.cancelled event 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.