Skip to main content

Recurring Billing Examples

Complete code examples for implementing recurring billing with the Paysera REST API.

Complete PHP Implementation​

Helper Class for MAC Authentication​

<?php

class PayseraRecurringBilling
{
private $businessId;
private $macId;
private $macKey;
private $host = 'checkout-eu-a.paysera.com';
private $port = 443;

public function __construct($businessId, $macId, $macKey)
{
$this->businessId = $businessId;
$this->macId = $macId;
$this->macKey = $macKey;
}

/**
* Generate MAC authorization header
*/
private function generateAuthHeader($method, $uri, $ext = '')
{
$timestamp = time();
$nonce = bin2hex(random_bytes(16));

// Build normalized request string (each element on new line)
$normalized = implode("\n", [
$timestamp,
$nonce,
strtoupper($method),
$uri,
strtolower($this->host),
$this->port,
$ext,
'' // Important: trailing newline
]);

// Calculate MAC using HMAC-SHA256
$mac = base64_encode(
hash_hmac('sha256', $normalized, $this->macKey, true)
);

// Build authorization header
$header = sprintf(
'MAC id="%s",ts="%d",nonce="%s",mac="%s"',
$this->macId,
$timestamp,
$nonce,
$mac
);

if ($ext) {
$header .= sprintf(',ext="%s"', urlencode($ext));
}

return $header;
}

/**
* Make API request
*/
private function request($method, $uri, $data = null)
{
$url = "https://{$this->host}{$uri}";
$authHeader = $this->generateAuthHeader($method, $uri);

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);

$headers = [
'Authorization: ' . $authHeader,
'Content-Type: application/json; charset=utf-8'
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

if ($data && in_array($method, ['POST', 'PUT'])) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$result = json_decode($response, true);

if ($httpCode >= 400) {
throw new Exception(
$result['error_description'] ?? 'API Error',
$httpCode
);
}

return $result;
}

/**
* Step 1: Create initial payment request
*/
public function createPaymentRequest($orderId, $amount, $currency, $email, $description)
{
$data = [
'business_id' => $this->businessId,
'order_id' => $orderId,
'price' => [
'amount' => $amount,
'currency' => $currency
],
'description' => $description,
'method_key' => 'card',
'payer' => [
'email' => $email
],
'locale' => 'en',
'accept_url' => 'https://yoursite.com/payment/success',
'cancel_url' => 'https://yoursite.com/payment/cancel',
'callback_url' => 'https://yoursite.com/payment/callback'
];

return $this->request('POST', '/checkout/rest/v1/payment-requests', $data);
}

/**
* Step 4: Get notification details
*/
public function getNotification($notificationId)
{
return $this->request('GET', "/notification/rest/v1/notifications/{$notificationId}");
}

/**
* Step 5: Mark notification as read
*/
public function markNotificationRead($notificationId)
{
return $this->request('PUT', "/notification/rest/v1/notifications/{$notificationId}/read");
}

/**
* Step 6a: Create recurring payment request
*/
public function createRecurringPayment($orderId, $amount, $currency, $email, $description)
{
return $this->createPaymentRequest($orderId, $amount, $currency, $email, $description);
}

/**
* Step 6b: Authorize payment with token
*/
public function authorizeWithToken($paymentRequestId, $token)
{
$data = ['token' => $token];
return $this->request('PUT', "/checkout/rest/v1/payment-requests/{$paymentRequestId}/authorize", $data);
}
}

Step 1: Create Initial Payment​

<?php
require_once 'PayseraRecurringBilling.php';

// Initialize API client
$paysera = new PayseraRecurringBilling(
'Opb2XVb-gEh4aGcR09Ko5Wb8V_6vueDM', // business_id
'wkVd93h2uS', // mac_id
'IrdTc8uQodU7PRpLzzLTW6wqZAO6tAMU' // mac_key
);

// Create initial payment
try {
$payment = $paysera->createPaymentRequest(
'SUB_' . time(), // Unique order ID
999, // Amount in cents (9.99 EUR)
'EUR', // Currency
'customer@example.com', // Customer email
'Monthly subscription' // Description
);

// Redirect user to authorization URL
header('Location: ' . $payment['authorization_url']);
exit;

} catch (Exception $e) {
echo "Error: " . $e->getMessage();
}

Step 3-5: Handle Callback​

<?php
// callback.php
require_once 'PayseraRecurringBilling.php';

$paysera = new PayseraRecurringBilling(
getenv('PAYSERA_BUSINESS_ID'),
getenv('PAYSERA_MAC_ID'),
getenv('PAYSERA_MAC_KEY')
);

// Get notification ID from callback
$notificationId = $_POST['notification_id'] ?? null;

if (!$notificationId) {
http_response_code(400);
die('Missing notification_id');
}

try {
// Retrieve notification details
$notification = $paysera->getNotification($notificationId);

// Check if payment was captured
if ($notification['event'] === 'payment_request.captured') {
$paymentData = $notification['data']['payment_request'];

// Store the token for future recurring charges
$token = $paymentData['issued_token'];
$orderId = $paymentData['order_id'];
$amount = $paymentData['price_paid']['amount'];
$email = $paymentData['payer']['email'];

// Save to database
$pdo = new PDO('mysql:host=localhost;dbname=yourdb', 'user', 'pass');
$stmt = $pdo->prepare("
INSERT INTO subscriptions (order_id, token, amount, currency, email, status, created_at)
VALUES (?, ?, ?, ?, ?, 'active', NOW())
");
$stmt->execute([
$orderId,
$token,
$amount,
$paymentData['price_paid']['currency'],
$email
]);

// Send confirmation email
mail($email, 'Subscription Confirmed', 'Your subscription has been activated!');
}

// Mark notification as read
$paysera->markNotificationRead($notificationId);

// Respond with success
http_response_code(200);
echo 'OK';

} catch (Exception $e) {
error_log("Callback error: " . $e->getMessage());
http_response_code(500);
}

Step 6: Create Recurring Charge​

<?php
// charge_subscriptions.php - Run this monthly via cron
require_once 'PayseraRecurringBilling.php';

$paysera = new PayseraRecurringBilling(
getenv('PAYSERA_BUSINESS_ID'),
getenv('PAYSERA_MAC_ID'),
getenv('PAYSERA_MAC_KEY')
);

// Get all active subscriptions
$pdo = new PDO('mysql:host=localhost;dbname=yourdb', 'user', 'pass');
$stmt = $pdo->query("SELECT * FROM subscriptions WHERE status = 'active'");

foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $subscription) {
try {
// Create new payment request
$payment = $paysera->createRecurringPayment(
$subscription['order_id'] . '_' . date('Ym'), // Unique order ID
$subscription['amount'],
$subscription['currency'],
$subscription['email'],
'Monthly subscription - ' . date('F Y')
);

// Authorize with stored token
$result = $paysera->authorizeWithToken(
$payment['id'],
$subscription['token']
);

if ($result['status'] === 'captured') {
echo "✓ Charged subscription {$subscription['id']}\n";

// Update last charged date
$updateStmt = $pdo->prepare("
UPDATE subscriptions
SET last_charged_at = NOW()
WHERE id = ?
");
$updateStmt->execute([$subscription['id']]);

} else {
echo "✗ Failed to charge subscription {$subscription['id']}\n";
}

} catch (Exception $e) {
echo "✗ Error charging subscription {$subscription['id']}: {$e->getMessage()}\n";

// Handle failed charge
if (strpos($e->getMessage(), 'token') !== false) {
// Token expired or invalid
$pdo->prepare("UPDATE subscriptions SET status = 'token_expired' WHERE id = ?")
->execute([$subscription['id']]);

// Notify customer
mail($subscription['email'], 'Payment Method Update Required',
'Please update your payment method.');
}
}

// Rate limiting - don't hammer the API
sleep(1);
}

JavaScript/Node.js Example​

API Client​

const crypto = require('crypto');
const axios = require('axios');

class PayseraRecurringBilling {
constructor(businessId, macId, macKey) {
this.businessId = businessId;
this.macId = macId;
this.macKey = macKey;
this.host = 'checkout-eu-a.paysera.com';
this.port = 443;
}

generateAuthHeader(method, uri, ext = '') {
const timestamp = Math.floor(Date.now() / 1000);
const nonce = crypto.randomBytes(16).toString('hex');

// Build normalized request string
const normalized = [
timestamp,
nonce,
method.toUpperCase(),
uri,
this.host.toLowerCase(),
this.port,
ext,
'' // Trailing newline
].join('\n');

// Calculate MAC using HMAC-SHA256
const mac = crypto
.createHmac('sha256', this.macKey)
.update(normalized)
.digest('base64');

// Build authorization header
let header = `MAC id="${this.macId}",ts="${timestamp}",nonce="${nonce}",mac="${mac}"`;

if (ext) {
header += `,ext="${encodeURIComponent(ext)}"`;
}

return header;
}

async request(method, uri, data = null) {
const url = `https://${this.host}${uri}`;
const authHeader = this.generateAuthHeader(method, uri);

const config = {
method,
url,
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json; charset=utf-8'
}
};

if (data) {
config.data = data;
}

try {
const response = await axios(config);
return response.data;
} catch (error) {
throw new Error(
error.response?.data?.error_description || 'API Error'
);
}
}

async createPaymentRequest(orderId, amount, currency, email, description) {
const data = {
business_id: this.businessId,
order_id: orderId,
price: {
amount: amount,
currency: currency
},
description: description,
method_key: 'card',
payer: {
email: email
},
locale: 'en',
accept_url: 'https://yoursite.com/payment/success',
cancel_url: 'https://yoursite.com/payment/cancel',
callback_url: 'https://yoursite.com/payment/callback'
};

return this.request('POST', '/checkout/rest/v1/payment-requests', data);
}

async getNotification(notificationId) {
return this.request('GET', `/notification/rest/v1/notifications/${notificationId}`);
}

async markNotificationRead(notificationId) {
return this.request('PUT', `/notification/rest/v1/notifications/${notificationId}/read`);
}

async authorizeWithToken(paymentRequestId, token) {
return this.request('PUT', `/checkout/rest/v1/payment-requests/${paymentRequestId}/authorize`, { token });
}
}

module.exports = PayseraRecurringBilling;

Usage Example​

const PayseraRecurringBilling = require('./PayseraRecurringBilling');
const express = require('express');

const app = express();
app.use(express.urlencoded({ extended: true }));

const paysera = new PayseraRecurringBilling(
process.env.PAYSERA_BUSINESS_ID,
process.env.PAYSERA_MAC_ID,
process.env.PAYSERA_MAC_KEY
);

// Create initial payment
app.post('/create-subscription', async (req, res) => {
try {
const payment = await paysera.createPaymentRequest(
`SUB_${Date.now()}`,
999, // 9.99 EUR in cents
'EUR',
req.body.email,
'Monthly subscription'
);

res.json({ paymentUrl: payment.authorization_url });
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// Handle callback
app.post('/payment/callback', async (req, res) => {
const notificationId = req.body.notification_id;

if (!notificationId) {
return res.status(400).send('Missing notification_id');
}

try {
// Get notification details
const notification = await paysera.getNotification(notificationId);

if (notification.event === 'payment_request.captured') {
const paymentData = notification.data.payment_request;

// Store token in database
// await db.subscriptions.create({ ... })

console.log('Token received:', paymentData.issued_token);
}

// Mark as read
await paysera.markNotificationRead(notificationId);

res.status(200).send('OK');
} catch (error) {
console.error('Callback error:', error);
res.status(500).send('Error');
}
});

app.listen(3000);

Python Example​

import hashlib
import hmac
import base64
import time
import secrets
import requests
from urllib.parse import urlencode

class PayseraRecurringBilling:
def __init__(self, business_id, mac_id, mac_key):
self.business_id = business_id
self.mac_id = mac_id
self.mac_key = mac_key
self.host = 'checkout-eu-a.paysera.com'
self.port = 443
self.base_url = f'https://{self.host}'

def generate_auth_header(self, method, uri, ext=''):
timestamp = str(int(time.time()))
nonce = secrets.token_hex(16)

# Build normalized request string
normalized = '\n'.join([
timestamp,
nonce,
method.upper(),
uri,
self.host.lower(),
str(self.port),
ext,
'' # Trailing newline
])

# Calculate MAC using HMAC-SHA256
mac = base64.b64encode(
hmac.new(
self.mac_key.encode(),
normalized.encode(),
hashlib.sha256
).digest()
).decode()

# Build authorization header
header = f'MAC id="{self.mac_id}",ts="{timestamp}",nonce="{nonce}",mac="{mac}"'

if ext:
header += f',ext="{urlencode({"ext": ext})}"'

return header

def request(self, method, uri, data=None):
url = f'{self.base_url}{uri}'
auth_header = self.generate_auth_header(method, uri)

headers = {
'Authorization': auth_header,
'Content-Type': 'application/json; charset=utf-8'
}

response = requests.request(method, url, json=data, headers=headers)

if response.status_code >= 400:
error_data = response.json()
raise Exception(error_data.get('error_description', 'API Error'))

return response.json()

def create_payment_request(self, order_id, amount, currency, email, description):
data = {
'business_id': self.business_id,
'order_id': order_id,
'price': {
'amount': amount,
'currency': currency
},
'description': description,
'method_key': 'card',
'payer': {
'email': email
},
'locale': 'en',
'accept_url': 'https://yoursite.com/payment/success',
'cancel_url': 'https://yoursite.com/payment/cancel',
'callback_url': 'https://yoursite.com/payment/callback'
}

return self.request('POST', '/checkout/rest/v1/payment-requests', data)

def get_notification(self, notification_id):
return self.request('GET', f'/notification/rest/v1/notifications/{notification_id}')

def mark_notification_read(self, notification_id):
return self.request('PUT', f'/notification/rest/v1/notifications/{notification_id}/read')

def authorize_with_token(self, payment_request_id, token):
return self.request('PUT', f'/checkout/rest/v1/payment-requests/{payment_request_id}/authorize', {'token': token})

# Usage
paysera = PayseraRecurringBilling(
'Opb2XVb-gEh4aGcR09Ko5Wb8V_6vueDM',
'wkVd93h2uS',
'IrdTc8uQodU7PRpLzzLTW6wqZAO6tAMU'
)

# Create payment
payment = paysera.create_payment_request(
f'SUB_{int(time.time())}',
999, # 9.99 EUR in cents
'EUR',
'customer@example.com',
'Monthly subscription'
)

print(f"Redirect to: {payment['authorization_url']}")

Database Schema​

CREATE TABLE subscriptions (
id INT AUTO_INCREMENT PRIMARY KEY,
order_id VARCHAR(255) UNIQUE NOT NULL,
token VARCHAR(255) NOT NULL,
amount INT NOT NULL,
currency VARCHAR(3) NOT NULL,
email VARCHAR(255) NOT NULL,
status ENUM('active', 'cancelled', 'expired', 'failed') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_charged_at TIMESTAMP NULL,
cancelled_at TIMESTAMP NULL,
INDEX idx_status (status),
INDEX idx_email (email)
);

CREATE TABLE charges (
id INT AUTO_INCREMENT PRIMARY KEY,
subscription_id INT NOT NULL,
order_id VARCHAR(255) UNIQUE NOT NULL,
amount INT NOT NULL,
currency VARCHAR(3) NOT NULL,
status ENUM('success', 'failed', 'pending') DEFAULT 'pending',
error_message TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id)
);
Next Steps