Manual Transfer Confirmation with GUI
Learn how to create transfers that require user confirmation through the Paysera web interface - perfect for user-initiated payments where verification is needed.
Overview
Manual confirmation creates transfers that User must approve through the Paysera web interface. This is ideal when:
- User needs to verify payment details
- Additional security verification required
- Compliance requires user confirmation
- Two-factor authentication needed
Manual confirmation provides users with a familiar Paysera interface for reviewing and approving transactions.
Prerequisites
Before you begin:
- ✅ Standard API credentials
- ✅ Understanding of Transfer Statuses
- ✅ Callback URL configured (recommended)
Workflow
1. Create Transfer
↓
2. Redirect User to confirmation_url
↓
3. User Reviews & Approves Transfer
↓
4. User Redirected to redirect_url
↓
5. Transfer Status: done
Step-by-Step Guide
- Step 1: Create Transfer
- Step 2: Handle Response
- Step 3: Redirect User
- Step 4: User Confirms
- Step 5: Handle Return
Create a transfer without auto_confirm:
POST /transfer/rest/v1/transfers HTTP/1.1
Host: wallet.paysera.com
Content-Type: application/json
Authorization: MAC id="client_id", ts="...", nonce="...", mac="..."
{
"amount": {
"amount": "100.00",
"currency": "EUR"
},
"beneficiary": {
"type": "bank",
"name": "John Doe",
"bank_account": {
"iban": "LT123456789012345678"
}
},
"payer": {
"account_number": "EVP9876543210"
},
"purpose": {
"details": "Payment for invoice #12345"
},
"redirect_url": "https://your-site.com/payment/success"
}
Key Parameters:
redirect_url- Where to send user after confirmation (optional)- NO
auto_confirmparameter (or set tofalse)
Success Response:
{
"id": "12345",
"status": "new",
"amount": {
"amount": "100.00",
"currency": "EUR"
},
"beneficiary": {
"type": "bank",
"name": "John Doe",
"bank_account": {
"iban": "LT123456789012345678"
}
},
"payer": {
"account_number": "EVP9876543210"
},
"purpose": {
"details": "Payment for invoice #12345"
},
"confirmation_url": "https://bank.paysera.com/transfer/confirm/abc123xyz",
"redirect_url": "https://your-site.com/payment/success",
"created_at": 1697193600
}
Important Fields:
confirmation_url- URL to redirect user for confirmationstatus: "new"- Transfer awaits confirmation
Redirect the user to confirmation_url:
// PHP Example
header("Location: " . $transfer->getConfirmationUrl());
exit;
// JavaScript Example
window.location.href = transfer.confirmation_url;
User sees Paysera interface with:
- Transfer details
- Amount and recipient
- Account selection (if multiple)
- Confirmation button
User can:
- ✅ Approve → Transfer processes
- ❌ Reject → Transfer cancelled
After user action, they're redirected to your redirect_url:
On Success:
https://your-site.com/payment/success?transfer_id=12345&status=done
On Rejection:
https://your-site.com/payment/success?transfer_id=12345&status=rejected
Complete Code Examples
- PHP
- PHP Return Handler
- Node.js
- Python
<?php
use Paysera\Client\TransferClient;
// Initialize client
$client = TransferClient::create([
'auth' => [
'mac' => [
'mac_id' => getenv('PAYSERA_CLIENT_ID'),
'mac_secret' => getenv('PAYSERA_MAC_KEY'),
],
],
]);
try {
// Create transfer for manual confirmation
$transfer = $client->createTransfer([
'amount' => [
'amount' => '100.00',
'currency' => 'EUR',
],
'beneficiary' => [
'type' => 'bank',
'name' => 'John Doe',
'bank_account' => [
'iban' => 'LT123456789012345678',
],
],
'payer' => [
'account_number' => 'EVP9876543210',
],
'purpose' => [
'details' => 'Payment for invoice #12345',
],
'redirect_url' => 'https://your-site.com/payment/success',
]);
echo "Transfer created: " . $transfer->getId() . "\n";
echo "Status: " . $transfer->getStatus() . "\n";
echo "Confirmation URL: " . $transfer->getConfirmationUrl() . "\n";
// Redirect user to confirmation page
header("Location: " . $transfer->getConfirmationUrl());
exit;
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
// Show error to user
showErrorPage($e->getMessage());
}
// success.php - Handle user return
<?php
$transferId = $_GET['transfer_id'] ?? null;
$status = $_GET['status'] ?? null;
if (!$transferId || !$status) {
showError('Invalid payment response');
exit;
}
// Load transfer details
$transfer = $client->getTransfer($transferId);
switch ($transfer->getStatus()) {
case 'done':
// Payment successful
updateOrderStatus($transferId, 'paid');
showSuccessPage('Payment completed successfully!');
break;
case 'rejected':
// User rejected payment
updateOrderStatus($transferId, 'cancelled');
showErrorPage('Payment was cancelled');
break;
case 'failed':
// Payment failed
updateOrderStatus($transferId, 'failed');
showErrorPage('Payment failed. Please try again.');
break;
default:
// Still processing
showPendingPage('Payment is being processed...');
break;
}
const express = require('express');
const axios = require('axios');
const app = express();
const BASE_URL = 'https://wallet.paysera.com/transfer/rest/v1';
// Create transfer and redirect
app.post('/create-payment', async (req, res) => {
try {
const response = await axios.post(`${BASE_URL}/transfers`, {
amount: {
amount: '100.00',
currency: 'EUR'
},
beneficiary: {
type: 'bank',
name: 'John Doe',
bank_account: {
iban: 'LT123456789012345678'
}
},
payer: {
account_number: 'EVP9876543210'
},
purpose: {
details: 'Payment for invoice #12345'
},
redirect_url: 'https://your-site.com/payment/success'
}, {
headers: {
'Content-Type': 'application/json',
'Authorization': getMacAuthHeader()
}
});
const transfer = response.data;
// Store transfer ID in session
req.session.transferId = transfer.id;
// Redirect to confirmation
res.redirect(transfer.confirmation_url);
} catch (error) {
console.error('Payment error:', error);
res.status(500).send('Payment failed');
}
});
// Handle return from Paysera
app.get('/payment/success', async (req, res) => {
const { transfer_id, status } = req.query;
try {
// Get updated transfer details
const response = await axios.get(
`${BASE_URL}/transfers/${transfer_id}`,
{
headers: {
'Authorization': getMacAuthHeader()
}
}
);
const transfer = response.data;
if (transfer.status === 'done') {
// Payment successful
await updateOrder(transfer_id, 'paid');
res.render('success', { transfer });
} else if (transfer.status === 'rejected') {
// Payment rejected
res.render('cancelled', { transfer });
} else {
// Still processing
res.render('pending', { transfer });
}
} catch (error) {
console.error('Error fetching transfer:', error);
res.status(500).send('Error processing payment');
}
});
from flask import Flask, request, redirect, render_template, session
import requests
app = Flask(__name__)
app.secret_key = 'your-secret-key'
BASE_URL = "https://wallet.paysera.com/transfer/rest/v1"
@app.route('/create-payment', methods=['POST'])
def create_payment():
headers = {
"Content-Type": "application/json",
"Authorization": get_mac_auth_header()
}
data = {
"amount": {
"amount": "100.00",
"currency": "EUR"
},
"beneficiary": {
"type": "bank",
"name": "John Doe",
"bank_account": {
"iban": "LT123456789012345678"
}
},
"payer": {
"account_number": "EVP9876543210"
},
"purpose": {
"details": "Payment for invoice #12345"
},
"redirect_url": "https://your-site.com/payment/success"
}
response = requests.post(
f"{BASE_URL}/transfers",
json=data,
headers=headers
)
if response.status_code == 200:
transfer = response.json()
# Store transfer ID
session['transfer_id'] = transfer['id']
# Redirect to confirmation
return redirect(transfer['confirmation_url'])
else:
return "Payment failed", 500
@app.route('/payment/success')
def payment_success():
transfer_id = request.args.get('transfer_id')
status = request.args.get('status')
# Get transfer details
headers = {"Authorization": get_mac_auth_header()}
response = requests.get(
f"{BASE_URL}/transfers/{transfer_id}",
headers=headers
)
if response.status_code == 200:
transfer = response.json()
if transfer['status'] == 'done':
update_order(transfer_id, 'paid')
return render_template('success.html', transfer=transfer)
elif transfer['status'] == 'rejected':
return render_template('cancelled.html', transfer=transfer)
else:
return render_template('pending.html', transfer=transfer)
return "Error", 500
Use Cases
- E-commerce Checkout
- Invoice Payment
- Subscription Renewal
User pays for order with manual confirmation:
function processCheckout($orderId, $userId) {
$order = getOrder($orderId);
$user = getUser($userId);
// Create transfer
$transfer = $client->createTransfer([
'amount' => [
'amount' => $order->total,
'currency' => $order->currency,
],
'beneficiary' => [
'type' => 'paysera',
'name' => 'Your Shop Name',
'paysera_account' => [
'account_number' => config('shop.account'),
],
],
'payer' => [
'account_number' => $user->paysera_account,
],
'purpose' => [
'details' => "Order #" . $orderId,
],
'redirect_url' => url("/orders/{$orderId}/success"),
]);
// Store transfer ID
$order->transfer_id = $transfer->getId();
$order->save();
// Redirect to confirmation
return redirect($transfer->getConfirmationUrl());
}
Customer pays invoice with verification:
function payInvoice($invoiceId, $customerId) {
$invoice = getInvoice($invoiceId);
if ($invoice->isPaid()) {
throw new Exception('Invoice already paid');
}
$transfer = $client->createTransfer([
'amount' => [
'amount' => $invoice->amount_due,
'currency' => $invoice->currency,
],
'beneficiary' => [
'type' => 'paysera',
'name' => $invoice->merchant_name,
'paysera_account' => [
'account_number' => $invoice->merchant_account,
],
],
'payer' => [
'account_number' => auth()->user()->paysera_account,
],
'purpose' => [
'details' => "Invoice #{$invoice->number}",
],
'redirect_url' => url("/invoices/{$invoiceId}/paid"),
]);
$invoice->transfer_id = $transfer->getId();
$invoice->status = 'pending_payment';
$invoice->save();
return redirect($transfer->getConfirmationUrl());
}
User manually renews subscription:
function renewSubscription($subscriptionId) {
$subscription = getSubscription($subscriptionId);
$plan = $subscription->plan;
$transfer = $client->createTransfer([
'amount' => [
'amount' => $plan->price,
'currency' => $plan->currency,
],
'beneficiary' => [
'type' => 'paysera',
'name' => 'Your Service',
'paysera_account' => [
'account_number' => config('service.account'),
],
],
'payer' => [
'account_number' => $subscription->user->paysera_account,
],
'purpose' => [
'details' => "Subscription renewal - {$plan->name}",
],
'redirect_url' => url("/subscriptions/{$subscriptionId}/renewed"),
]);
$subscription->pending_renewal_transfer_id = $transfer->getId();
$subscription->save();
return redirect($transfer->getConfirmationUrl());
}
Handling Callbacks
Set up callback endpoint to receive status updates:
// callback.php
<?php
$transferId = $_POST['transfer_id'] ?? null;
$status = $_POST['status'] ?? null;
$date = $_POST['date'] ?? null;
if (!$transferId || !$status) {
http_response_code(400);
exit('Invalid callback data');
}
try {
// Get transfer from database
$order = Order::where('transfer_id', $transferId)->first();
if (!$order) {
logger()->warning("Callback for unknown transfer: {$transferId}");
http_response_code(200);
exit('OK');
}
// Update order based on status
switch ($status) {
case 'done':
$order->status = 'paid';
$order->paid_at = now();
sendOrderConfirmation($order);
break;
case 'rejected':
$order->status = 'payment_rejected';
sendPaymentRejectedEmail($order);
break;
case 'failed':
$order->status = 'payment_failed';
break;
}
$order->save();
// Return success
http_response_code(200);
echo 'OK';
} catch (Exception $e) {
logger()->error('Callback error', [
'transfer_id' => $transferId,
'error' => $e->getMessage(),
]);
http_response_code(500);
exit('Error');
}
Error Handling
- User Cancellation
- Timeout
Handle when user rejects payment:
// In your success/return handler
if ($transfer->getStatus() === 'rejected') {
// User cancelled payment
logger()->info("User cancelled transfer: {$transfer->getId()}");
// Show friendly message
return view('payment.cancelled', [
'message' => 'You cancelled the payment. You can try again anytime.',
'retry_url' => route('checkout'),
]);
}
Handle payment timeout (user doesn't confirm):
// Check for old pending transfers
$pendingTransfers = Transfer::where('status', 'new')
->where('created_at', '<', now()->subHours(24))
->get();
foreach ($pendingTransfers as $transfer) {
// Mark as expired
$transfer->status = 'expired';
$transfer->save();
// Notify user
sendPaymentExpiredEmail($transfer->user);
}
Security Considerations
- 1. Validate Redirect URL
- 2. CSRF Protection
- 3. Verify User Ownership
// Whitelist allowed redirect URLs
$allowedDomains = ['your-site.com', 'shop.your-site.com'];
$redirectUrl = $request->input('redirect_url');
$domain = parse_url($redirectUrl, PHP_URL_HOST);
if (!in_array($domain, $allowedDomains)) {
throw new SecurityException('Invalid redirect URL');
}
// Generate CSRF token before redirect
$token = generateCsrfToken();
session(['payment_csrf' => $token]);
// Validate on return
if (session('payment_csrf') !== $request->input('csrf_token')) {
throw new SecurityException('CSRF token mismatch');
}
// Ensure user can only access their own transfers
$transfer = Transfer::where('id', $transferId)
->where('user_id', auth()->id())
->firstOrFail();
Comparison Table
| Feature | Manual Confirmation | Automatic Confirmation |
|---|---|---|
| User Interaction | Required | None |
| Security | User verifies | System controlled |
| Speed | User dependent | Instant |
| Use Case | User payments | Background automation |
| Permissions | Standard | Special permission |
| User Experience | Familiar Paysera UI | Seamless |
Additional Resources
Support
Need help with complex integrations?
Contact: tech_support@paysera.com