Payment Gateway - Partner Integration Guide
Complete documentation for integrating multi-gateway payment solution (Cards + Meal Vouchers)
Quick Start
- Download Postman collection and YAML spec
- Configure your notification webhook URL
- Integrate the Wrapper API (4 endpoints only)
PayGlobe.js - JavaScript Library
1. Include the Library
<!-- Include PayGlobe.js in your HTML --> <script src="https://api.payglobe.it/paymentgw/js/payglobe.js"></script>
2. Initialize with Your API Key
// Initialize PayGlobe with your publishable key
const payglobe = PayGlobe('pk_test_PayGlobe123456789TestPublishable');
3. Create a Payment
// Create a new payment
const payment = await payglobe.payments.create({
amount: 50.00,
email: 'customer@example.com', // Customer email for payment receipt
description: 'Order #12345' // optional
});
console.log('Payment created:', payment.id);
4. Redirect to Checkout
// Option A: Full page redirect
payglobe.redirectToCheckout({
paymentId: payment.id
});
// Option B: Load in iframe
payglobe.redirectToCheckout({
paymentId: payment.id,
element: '#payment-container' // CSS selector for iframe container
});
5. Check Payment Status
// Retrieve payment status
const status = await payglobe.payments.retrieve(payment.id);
console.log('Payment status:', status.status);
console.log('Amount:', status.totalAmount);
console.log('Gateway Payment ID:', status.paymentId);
console.log('Voucher Amount:', status.voucherAmount);
Complete Example
<!DOCTYPE html>
<html>
<head>
<title>PayGlobe Checkout</title>
<script src="https://api.payglobe.it/paymentgw/js/payglobe.js"></script>
</head>
<body>
<button id="checkout-button">Pay 50.00 EUR</button>
<div id="payment-container"></div>
<script>
const payglobe = PayGlobe('pk_test_PayGlobe123456789TestPublishable');
document.getElementById('checkout-button').addEventListener('click', async () => {
try {
// Create payment
const payment = await payglobe.payments.create({
amount: 50.00,
email: 'customer@example.com', // Customer email for receipt
description: 'Order #12345' // Your internal order number
});
// Redirect to checkout (in iframe)
await payglobe.redirectToCheckout({
paymentId: payment.id,
element: '#payment-container'
});
} catch (error) {
console.error('Payment error:', error);
alert('Payment failed: ' + error.message);
}
});
</script>
</body>
</html>
pk_test_ or pk_live_).
Try the Live Example
See the above code in action with a working demo:
This example demonstrates a simple payment flow using PayGlobe.js with a working payment button.High-Level Architecture
Payment Flow (5 Steps)
Your System β Gateway β Card Processor
Preauthorize full amount on card (hold funds)
Customer decides:
β’ Pay with card only
β’ Use meal vouchers (partial/full)
Gateway β Voucher Provider
VALIDATE β SPEND β NOTIFY
Gateway β Card Processor:
β’ No vouchers: Capture full amount
β’ Partial vouchers: Capture difference
β’ Full vouchers: Void preauth
Gateway β Your Webhook
JSON with card + voucher details
Payment Flow (3 Steps)
Your System β Gateway β Card Processor
Single call: Auth + Capture in one step
Not applicable - no voucher option
Not applicable - card only
Already captured in step 1
Gateway β Your Webhook
JSON with payment details
| Feature | PREAUTH + CAPTURE | SALE DIRECT |
|---|---|---|
| Steps | 5 steps (complex flow) | 2-3 steps (simple flow) |
| Meal Vouchers | β Supported | β Not available |
| Card Operation | Preauth β Capture/Void | Single Sale (Auth+Capture) |
| User Interaction | Voucher choice page | Direct payment |
| Settlement | After capture | Immediate |
| Payment Mode Config | PREAUTH_VOUCHER_CAPTURE |
SALE_DIRECT |
Integration Flow
Step 1: Initialize Payment
POST/api/payment/init
{
"amount": 150.00,
"notifyUrl": "https://yourserver.com/api/igfs-callback",
"errorUrl": "https://yourserver.com/payment/error",
"callbackUrl": "https://yourserver.com/payment/{transactionId}/voucher-choice"
}
Response:
{
"success": true,
"transactionId": "abc-123-def-456",
"totalAmount": 150.00,
"redirectUrl": "https://paymentpage.igfs.payglobe.com?paymentID=XYZ",
"igfs": {
"paymentId": "XYZ",
"status": "PENDING"
}
}
Step 2: Get Transaction Info
GET/api/payment/transaction/{transactionId}
Response:
{
"success": true,
"transactionId": "abc-123-def-456",
"status": "CARD_PREAUTH_SUCCESS",
"cardGateway": {
"paymentId": "XYZ",
"preauthorizedAmount": 150.00,
"status": "SUCCESS"
},
"breakdown": {
"cardAmount": 150.00,
"voucherAmount": 0.00,
"paymentMethod": "CARD_ONLY"
}
}
After Step 1, the system automatically handles:
- Card Preauthorization (via configured payment gateway)
- Voucher selection page for the customer
- Meal voucher processing (if customer enters OTP)
- Final capture on card (total amount or difference)
- Notification to your webhook with complete details
For advanced integrations requiring granular control, use the Direct API endpoints.
See Postman collection for complete Direct API examples.
Payment Links - Send Shareable Payment Links with QR Codes
Create payment links for invoicing, social commerce, QR code payments, or remote sales. No checkout integration required!
https://api.payglobe.it/paymentgw/payment/checkout/{transactionId} β the same
multi-method checkout as your online store (card in an iframe, plus every other method
enabled on your account). The buyer never sees a PSP URL, and the gateway session is opened
when the buyer opens the link, not when you create it. When the payment completes, the PSP
notifies us and we call your webhook URL, signed with
X-PayGlobe-Signature β nothing to configure on the PSP side.
Step 1: Create Payment Link
POST/api/payment-links/create
Headers:
Authorization: Bearer sk_test_YourSecretKey
Content-Type: application/json
{
"amount": 50.00,
"currency": "EUR",
"description": "Payment for Order #12345",
"customerEmail": "customer@example.com",
"customerName": "John Doe",
"linkType": "LINK",
"expirationDays": 30
}
Response:
{
"success": true,
"transactionId": "LINK_A1B2C3D4E5F67890",
"linkUrl": "https://api.payglobe.it/paymentgw/payment/checkout/LINK_A1B2C3D4E5F67890",
"qrcodeUrl": "/api/payment-links/LINK_A1B2C3D4E5F67890/qrcode",
"amount": 50.00,
"status": "PENDING",
"delivered": false,
"expiresAt": "2025-12-09T12:00:00"
}
Link Types
| Type | Description | Use Case |
|---|---|---|
LINK |
Shareable URL (recommended for QR codes) | QR Code POS, WhatsApp, Email |
MAIL |
PayGlobe emails the link to the customer (delivered: true in the response confirms it left) |
Automated invoicing |
SMS |
No SMS channel: the link is returned (delivered: false) and you send it yourself |
Mobile payments |
Step 2: Display QR Code (No Auth Required)
<!-- Embed QR Code in HTML -->
<img src="https://api.payglobe.it/paymentgw/api/payment-links/LINK_ABC123/qrcode?size=300"
alt="Payment QR Code">
<!-- Or use in email template -->
<h2>Invoice Payment</h2>
<p>Scan QR code or click link to pay β¬50.00</p>
<img src="{{qrcodeUrl}}" alt="QR Code">
<a href="{{linkUrl}}">Pay Now</a>
Step 3: Check Payment Status
GET/api/payment-links/{transactionId}
Headers:
Authorization: Bearer sk_test_YourSecretKey
Response:
{
"success": true,
"transactionId": "LINK_A1B2C3D4E5F67890",
"status": "PAID",
"paymentId": "IGFS_PAYMENT_ID",
"authCode": "123456",
"paidAt": "2025-11-09T12:30:00",
"amount": 50.00
}
Step 4: Verify Status from Payment Gateway
POST/api/payment-links/{transactionId}/verify
Headers:
Authorization: Bearer sk_test_YourSecretKey
Description: Refreshes payment status from gateway and updates local database
Additional Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/payment-links | List all payment links (filter: ?status=PAID) |
| POST | /api/payment-links/{id}/cancel | Cancel a pending payment link |
Status Values
| Status | Description |
|---|---|
PENDING |
Link created, awaiting payment |
PAID |
Payment completed successfully |
EXPIRED |
Link expired (past expiresAt date) |
CANCELLED |
Link manually cancelled |
Live Demo - Interactive Payment Links
Webhook Notifications (HMAC Signature)
PayGlobe sends server-to-server webhooks to notify you of payment events using HMAC-SHA256 signatures for security.
webhookSecret (e.g., whsec_test_...) for signature verification.
Step 1: Configure Webhook in Merchant Dashboard
- Go to Merchant Login and authenticate with your API key
- In the Webhook Configuration section, enter your webhook URL (e.g.,
https://yourdomain.com/api/payment-webhook) - Copy the Webhook Secret (shown once, format:
whsec_test_abc123...) - Save the secret in your backend environment variables
Step 2: Verify HMAC Signature (Java Example)
// Receive webhook
String signature = request.getHeader("X-PayGlobe-Signature");
String rawBody = request.getBody(); // RAW JSON string
// Verify signature with your webhook secret
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(webhookSecret.getBytes(), "HmacSHA256");
mac.init(secretKey);
byte[] hash = mac.doFinal(rawBody.getBytes());
String expectedSignature = Base64.getEncoder().encodeToString(hash);
if (expectedSignature.equals(signature)) {
// β
Webhook authentic - process payment
PaymentNotification payment = objectMapper.readValue(rawBody, PaymentNotification.class);
System.out.println("Payment received: " + payment.transactionId);
} else {
// β Invalid signature - reject
return Response.status(401).entity("Invalid signature").build();
}
Step 3: Verify HMAC Signature (Node.js Example)
const crypto = require('crypto');
app.post('/api/payment-webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-payglobe-signature'];
const rawBody = req.body.toString(); // RAW body string
// Verify signature
const hmac = crypto.createHmac('sha256', process.env.PAYGLOBE_WEBHOOK_SECRET);
hmac.update(rawBody);
const expectedSignature = hmac.digest('base64');
if (expectedSignature !== signature) {
return res.status(401).send('Invalid signature');
}
// β
Webhook verified - process payment
const payment = JSON.parse(rawBody);
console.log('Payment received:', payment.transactionId);
res.status(200).send('OK');
});
Webhook Payload Example:
POSThttps://yourdomain.com/api/payment-webhook
Headers:
X-PayGlobe-Signature: Kx7j9mP4vR2nQ8sT... (HMAC-SHA256 signature)
Content-Type: application/json
{
"transactionId": "TXN_20251107_123456",
"totalAmount": 150.00,
"cardAmount": 100.00,
"voucherAmount": 50.00,
"paymentMethod": "CARD_AND_VOUCHER",
"status": "SUCCESS",
"sessionStart": "2025-11-07T10:00:00Z",
"sessionEnd": "2025-11-07T10:05:23Z",
"sessionDurationSeconds": 323,
"vouchersUsed": 1,
"cardGatewayDetails": {
"paymentId": "PAY_789",
"transactionId": "IGFS_123456789",
"operationType": "CAPTURE",
"preauthorizedAmount": 150.00,
"capturedAmount": 100.00,
"responseCode": "00",
"responseMessage": "Transaction approved"
},
"voucherDetails": {
"serialNumber": "2189CA1L7823",
"operationNumber": "OP123456",
"spentAmount": 50.00,
"responseCode": "000",
"responseMessage": "Transaction OK"
},
"createdAt": "2025-11-07T10:00:00Z",
"completedAt": "2025-11-07T10:05:23Z"
}
- Always verify the signature before processing webhooks
- Use the RAW request body for signature verification (not parsed JSON)
- Store
webhookSecretsecurely (environment variable, secrets manager) - Return
200 OKquickly; process payment asynchronously if needed - Webhooks are sent for: PREAUTH success/fail, CAPTURE success/fail, VOID, Voucher processing
Testing Webhooks:
Use webhook.site or ngrok to test webhook delivery:
- Go to webhook.site and copy unique URL
- Configure this URL as webhook in Admin Panel
- Copy webhook secret shown
- Make a test payment
- Verify webhook received with
X-PayGlobe-Signatureheader
Stripe Webhook Compatibility
PayGlobe webhooks follow Stripe's Event object structure, allowing you to reuse existing Stripe webhook code and libraries without modifications.
Why Stripe-Compatible?
- Standard Format: Same JSON structure as Stripe webhook events
- Event Types: Uses Stripe naming conventions (
payment_intent.succeeded,payment_intent.payment_failed, etc.) - Drop-in Replacement: Works with existing Stripe webhook libraries (Node.js stripe, Python stripe, etc.)
- Enhanced Data: Includes additional PayGlobe features like voucher details and multi-gateway support
Webhook Event Structure:
PayGlobe wraps payment data in a Stripe-compatible Event object:
{
"id": "evt_1a2b3c4d5e6f7g8h9i0j", // Unique event ID (evt_...)
"object": "event", // Always "event"
"api_version": "2023-10-16", // API version
"created": 1699372800, // Unix timestamp
"type": "payment_intent.succeeded", // Event type
"livemode": false, // Test/live mode
"pending_webhooks": 0, // Delivery queue count
"data": {
"object": {
// Your payment data here (PaymentNotificationDto)
"transactionId": "TXN_20251107_123456",
"totalAmount": 150.00,
"cardAmount": 100.00,
"voucherAmount": 50.00,
"paymentMethod": "CARD_AND_VOUCHER",
"status": "SUCCESS",
"cardGatewayDetails": { ... },
"voucherDetails": { ... }
}
},
"request": null // Null for automatic events
}
Event Types:
| Event Type | When Triggered | Status |
|---|---|---|
payment_intent.succeeded |
Payment completed successfully | Active |
payment_intent.payment_failed |
Payment failed (preauth/capture/voucher failed) | Active |
payment_intent.processing |
Payment in progress | Active |
charge.refunded |
Refund processed (full or partial) | Active |
checkout.session.completed |
Checkout session completed (payment page closed) | Active |
Using Stripe Libraries (Node.js Example):
const stripe = require('stripe');
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'] || req.headers['x-payglobe-signature'];
let event;
try {
// Works with Stripe's constructEvent method!
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.WEBHOOK_SECRET // whsec_test_...
);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle event types
switch (event.type) {
case 'payment_intent.succeeded':
const payment = event.data.object;
console.log('Payment succeeded:', payment.transactionId);
console.log('Card amount:', payment.cardAmount);
console.log('Voucher amount:', payment.voucherAmount);
break;
case 'payment_intent.payment_failed':
console.log('Payment failed:', event.data.object);
break;
case 'charge.refunded':
const refund = event.data.object;
console.log('Refund processed:', refund.transactionId);
console.log('Refund amount:', refund.refundAmount);
break;
case 'checkout.session.completed':
console.log('Checkout completed:', event.data.object);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).send('OK');
});
Using Stripe Libraries (Python Example):
import stripe
@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.get_data()
sig_header = request.headers.get('Stripe-Signature') or \
request.headers.get('X-PayGlobe-Signature')
try:
# Works with Stripe's construct_event method!
event = stripe.Webhook.construct_event(
payload, sig_header, webhook_secret
)
except ValueError as e:
return 'Invalid payload', 400
except stripe.error.SignatureVerificationError as e:
return 'Invalid signature', 400
# Handle event
if event['type'] == 'payment_intent.succeeded':
payment = event['data']['object']
print(f"Payment succeeded: {payment['transactionId']}")
print(f"Card: {payment['cardAmount']}, Voucher: {payment['voucherAmount']}")
elif event['type'] == 'payment_intent.payment_failed':
print(f"Payment failed: {event['data']['object']}")
elif event['type'] == 'charge.refunded':
refund = event['data']['object']
print(f"Refund processed: {refund['transactionId']}")
print(f"Refund amount: {refund.get('refundAmount', 0)}")
elif event['type'] == 'checkout.session.completed':
print(f"Checkout completed: {event['data']['object']}")
return '', 200
PayGlobe sends both signature headers for maximum compatibility:
X-PayGlobe-Signature- PayGlobe standard headerStripe-Signature- Stripe-compatible header (same value)
This allows Stripe libraries to work without modifications!
PayGlobe Advantages Over Stripe:
| Feature | Stripe | PayGlobe |
|---|---|---|
| Webhook Format | Event object | Same Event object |
| Signature Verification | HMAC-SHA256 | HMAC-SHA256 (same) |
| Library Compatibility | Stripe libraries only | Works with Stripe libraries |
| Voucher Support | None | Yes (Assiopay/Monni) |
| Multi-Gateway | Stripe only | IGFS, Stripe, Nexi, Worldline, etc. |
| Detailed Gateway Info | Limited | Full details (cardGatewayDetails, voucherDetails) |
| Automatic Retries | Limited attempts | 10 attempts, 5 min intervals |
| Delivery Tracking | Basic dashboard | Full audit trail in database |
If you're migrating from Stripe, your existing webhook code will work with PayGlobe with minimal changes:
- Replace
STRIPE_WEBHOOK_SECRETwith PayGlobe'swebhookSecret - Update webhook URL in your configuration
- That's it! Your code continues to work.
Merchant Configuration & Onboarding
Managed Service
PayGlobe provides API keys and manages all backend configurations for you. You don't need to access any admin panel.
How It Works:
- Contact PayGlobe to open a merchant account
- Provide your business details and integration requirements
- PayGlobe configures all payment gateway credentials (IGFS, Stripe, Assiopay, etc.) on your behalf
- Receive your API keys:
pk_test_...- Publishable key (safe for frontend)sk_test_...- Secret key (backend only)
- Configure your webhook via Merchant Dashboard (self-service)
- Start accepting payments using our APIs
Error Handling
| Error Type | Status | Action |
|---|---|---|
| Card Preauth Failed | CARD_PREAUTH_FAILED | Inform user, retry or cancel |
| Voucher Invalid | VOUCHER_FAILED | Allow retry with different voucher or proceed without |
| Capture Failed | CARD_CAPTURE_FAILED | Contact support, transaction will auto-rollback |
| Transaction Timeout | FAILED | Check status, may need manual intervention |
API Endpoints & Authentication
Authentication with API Keys
All API requests require authentication using API keys. You will receive two keys:
- Publishable Key (
pk_test_xxxorpk_live_xxx): Safe to use in frontend code - Secret Key (
sk_test_xxxorsk_live_xxx): Must be kept secret, use only on your server
How to Authenticate
Include API key in Authorization header:
Authorization: Bearer pk_test_PayGlobe123456789TestPublishable
Example with JavaScript:
const response = await fetch('https://api.payglobe.it/paymentgw/api/payment/initiate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer pk_test_PayGlobe123456789TestPublishable'
},
body: JSON.stringify({
amount: 150.00,
email: 'customer@example.com',
description: 'Order #12345'
})
});
Example with cURL:
curl -X POST https://api.payglobe.it/paymentgw/api/payment/initiate \
-H "Authorization: Bearer pk_test_PayGlobe123456789TestPublishable" \
-H "Content-Type: application/json" \
-d '{"amount": 150.00, "email": "customer@example.com", "description": "Order #12345"}'
pk_test_*) for development. No real charges will be made.
Contact support@payglobe.com to receive your API keys.
Production Environment
- Base URL: https://api.payglobe.it/paymentgw
- Protocol: HTTPS only (TLS 1.2+)
- Content-Type: application/json
Testing
- Test Cards: Use test merchant credentials provided by PayGlobe
- Test Vouchers: Contact support for test voucher codes
- Sandbox: Contact support for test environment access
API Keys: Security & Permissions
π Two Types of API Keys
PayGlobe provides two distinct API keys with different permission levels:
β Safe for client-side (JavaScript)
Allowed Operations:
- Create new payments
- Check payment status
- Get checkout URLs
β Cannot perform:
- Refunds/Storni
- Capture operations
- Access sensitive data
- Modify configurations
Usage: Embed in your frontend JavaScript code
β οΈ Server-side ONLY - Never expose publicly
Full Permissions:
- All Publishable Key operations
- Refunds & Storni
- Manual captures
- Void preauthorizations
- Webhook signature validation
- Access transaction details
Usage: Store in environment variables on your backend server
Refunds & Storni (Reversals)
- 200 β the gateway confirmed it. The money moved.
- 400 β the request was refused before reaching the gateway (wrong state, amount above what is refundable, transaction not yours). Nothing was sent to the gateway.
- 402 β the gateway refused (declined). The body carries
gatewayResultCodeandgatewayMessagewith the PSP's own reason. No money moved and the transaction is left untouched.
amount to refund the full remaining balance.
| Gateway | Refund | Partial refund |
|---|---|---|
| Stripe | β | β |
| IGFS | β | β β full reversal only |
| Bizum | β | β |
| Skrill | β | β |
| Satispay | β | β |
| PayPal | β | β |
| Amazon Pay | β | β (settled asynchronously) |
| Zonda (crypto) | β β issue it from the Zonda panel | β |
Idempotency-Key header is accepted but
not yet honoured. Repeating a full refund is refused because no
balance remains, but a repeated partial refund is indistinguishable from
a legitimate second one and will refund again. Do not retry automatically.
1. Full Refund (Storno Totale)
Refund the entire payment amount:
Endpoint: POST /api/payment/refund/{transactionId}
Headers:
Authorization: Bearer sk_test_YourSecretKeyHere β Secret Key required!
Content-Type: application/json
Request Body:
{
"reason": "Customer requested refund"
}
Example with cURL:
curl -X POST https://api.payglobe.it/paymentgw/api/payment/refund/abc123 \
-H "Authorization: Bearer sk_test_YourSecretKeyHere" \
-H "Content-Type: application/json" \
-d '{"reason": "Customer requested refund"}'
Response (Success):
{
"success": true,
"transactionId": "abc123",
"refundAmount": 150.00,
"status": "REFUNDED",
"refundedAt": "2025-11-05T20:15:00Z"
}
2. Partial Refund (Storno Parziale)
Refund only a portion of the payment:
Endpoint: POST /api/payment/refund/{transactionId}
Headers:
Authorization: Bearer sk_test_YourSecretKeyHere β Secret Key required!
Content-Type: application/json
Request Body:
{
"amount": 50.00,
"reason": "Partial refund for damaged item"
}
Example with Node.js:
const response = await fetch(
`https://api.payglobe.it/paymentgw/api/payment/refund/${transactionId}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAYGLOBE_SECRET_KEY}`, // sk_test_...
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: 50.00,
reason: 'Partial refund for damaged item'
})
}
);
const refund = await response.json();
console.log('Refund processed:', refund);
Response (Success):
{
"success": true,
"transactionId": "abc123",
"refundAmount": 50.00,
"remainingAmount": 100.00,
"status": "PARTIALLY_REFUNDED",
"refundedAt": "2025-11-05T20:15:00Z"
}
3. Get Refund History
Retrieve all refunds for a transaction:
Endpoint: GET /api/payment/refunds/{transactionId}
Headers:
Authorization: Bearer sk_test_YourSecretKeyHere β Secret Key required!
Example:
curl https://api.payglobe.it/paymentgw/api/payment/refunds/abc123 \
-H "Authorization: Bearer sk_test_YourSecretKeyHere"
Response:
{
"transactionId": "abc123",
"originalAmount": 150.00,
"totalRefunded": 50.00,
"remainingAmount": 100.00,
"refunds": [
{
"refundId": "ref_xyz789",
"amount": 50.00,
"reason": "Partial refund for damaged item",
"status": "SUCCESS",
"createdAt": "2025-11-05T20:15:00Z"
}
]
}
Backend Implementation Example (Node.js/Express)
// Store secret key in environment variable
const PAYGLOBE_SECRET_KEY = process.env.PAYGLOBE_SECRET_KEY; // sk_test_...
// Refund endpoint on your server
app.post('/admin/refund/:transactionId', async (req, res) => {
try {
const { transactionId } = req.params;
const { amount, reason } = req.body;
// Call PayGlobe API with SECRET KEY
const response = await fetch(
`https://api.payglobe.it/paymentgw/api/payment/refund/${transactionId}`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${PAYGLOBE_SECRET_KEY}`, // SECRET!
'Content-Type': 'application/json'
},
body: JSON.stringify({ amount, reason })
}
);
const refund = await response.json();
if (refund.success) {
// Log the refund in your database
await logRefund(transactionId, refund);
res.json({ success: true, refund });
} else {
res.status(400).json({ error: refund.message });
}
} catch (error) {
res.status(500).json({ error: error.message });
}
});
β οΈ Security Best Practices
- NEVER expose your Secret Key in client-side code (JavaScript, mobile apps)
- ALWAYS store Secret Keys in environment variables or secure secret managers
- NEVER commit Secret Keys to version control (Git)
- Use Publishable Keys for frontend operations only
- Implement refund operations only on your backend server with proper authentication
- Rotate keys immediately if a Secret Key is compromised
Summary: Which Key to Use?
Frontend (JavaScript - Publishable Key):
const payglobe = PayGlobe('pk_test_...'); β
Public, safe to expose
await payglobe.payments.create({
amount: 100,
email: 'customer@example.com', // Customer email for receipt
description: 'Order #12345' // Your internal order number
});
Backend (Server - Secret Key):
const secretKey = process.env.PAYGLOBE_SECRET_KEY; β
Private, server-only
Authorization: Bearer sk_test_...
Operations Requiring Secret Key:
- POST /api/payment/refund/{id} β Full or partial refund
- POST /api/payment/capture/{id} β Manual capture (full or partial)
- POST /api/payment/void/{id} β Void preauthorization (release hold)
- Webhook signature validation β Verify HMAC
A publishable key on these endpoints is rejected with HTTP 403.
Refund history is not exposed as a separate endpoint: the refund response
returns refundedTotal and remainingRefundable, and GET /api/payment/status/{id}
reports the current state.
Dispute & Chargeback Management
How Disputes Work
Customer contacts their bank to dispute a transaction
IGFS, Stripe, or PayPal sends webhook to our
/webhook/psp/dispute endpoint
The dispute is visible in Merchant Dashboard β Disputes tab (you don't create it manually)
Submit evidence (receipts, delivery proof, etc.) before the deadline
Bank decides: WON (funds returned) or LOST (funds kept by customer)
Dispute API Endpoints
1. List Disputes
GET/api/merchant/disputes
Headers:
Authorization: Bearer sk_test_YourSecretKey
Query Parameters:
?status=OPEN - Filter by status (OPEN, UNDER_REVIEW, WON, LOST, EXPIRED)
?startDate=2025-01-01 - Filter from date
?endDate=2025-12-31 - Filter to date
?page=0 - Page number (0-indexed)
?size=20 - Page size
Response:
{
"disputes": [
{
"disputeId": "dp_1abc123def456",
"originalTransactionId": "TXN_20251107_123456",
"amountDisputed": 50.00,
"currency": "EUR",
"reasonCode": "FRAUDULENT",
"reasonDescription": "Customer claims unauthorized charge",
"status": "OPEN",
"pspSource": "IGFS",
"responseDeadline": "2025-12-15T23:59:59Z",
"daysUntilDeadline": 12,
"isUrgent": false,
"createdAt": "2025-12-01T10:30:00Z"
}
],
"currentPage": 0,
"totalPages": 1,
"totalItems": 1
}
2. Get Dispute Statistics
GET/api/merchant/disputes/stats
Headers:
Authorization: Bearer sk_test_YourSecretKey
Response:
{
"open": 3,
"underReview": 1,
"won": 5,
"lost": 2,
"totalAtRisk": 150.00,
"totalLost": 75.00
}
3. Respond to Dispute
POST/api/merchant/disputes/{disputeId}/respond
Headers:
Authorization: Bearer sk_test_YourSecretKey
Content-Type: application/json
Request Body:
{
"response": "This transaction was legitimate. Customer placed order on 2025-11-01 and received delivery confirmation on 2025-11-03. Attached: order receipt, shipping tracking, delivery signature."
}
Response:
{
"success": true,
"disputeId": "dp_1abc123def456",
"status": "UNDER_REVIEW",
"message": "Response submitted successfully"
}
Dispute Status Values
| Status | Description | Action Required |
|---|---|---|
OPEN |
New dispute requiring response | Submit evidence before deadline |
UNDER_REVIEW |
Evidence submitted, awaiting bank decision | Wait for resolution |
EVIDENCE_REQUIRED |
Additional evidence requested | Submit more documentation |
WON |
Dispute resolved in your favor | None - funds returned |
LOST |
Customer won the dispute | None - funds not recovered |
CANCELLED |
Dispute withdrawn by customer/bank | None |
EXPIRED |
Response deadline passed | Automatically lost |
Dispute Reason Codes
| Reason Code | Description | Recommended Evidence |
|---|---|---|
FRAUDULENT |
Customer claims unauthorized charge | IP address, device info, order history |
PRODUCT_NOT_RECEIVED |
Customer didn't receive goods/services | Shipping tracking, delivery signature |
PRODUCT_UNACCEPTABLE |
Product differs from description | Product photos, description, communications |
DUPLICATE |
Customer charged multiple times | Transaction logs, refund records |
SUBSCRIPTION_CANCELLED |
Customer cancelled but still charged | Cancellation policy, terms of service |
CREDIT_NOT_PROCESSED |
Refund not received | Refund confirmation, bank records |
GENERAL |
Other/unspecified reason | Any relevant documentation |
- Disputes appear automatically - You don't create them; they come from PSP webhooks
- Response deadlines are critical - Missing the deadline usually means automatic loss
- isUrgent flag - Set to
truewhen less than 3 days remain - Secret Key required - All dispute endpoints require
sk_test_*orsk_live_*
Webhook: Receive Dispute Updates
When a dispute status changes, PayGlobe sends a webhook to your configured URL:
POSThttps://your-server.com/webhook/payglobe
Event Type: charge.dispute.created | charge.dispute.updated | charge.dispute.closed
Payload:
{
"id": "evt_dispute_123",
"type": "charge.dispute.created",
"created": 1701432000,
"data": {
"object": {
"disputeId": "dp_1abc123def456",
"originalTransactionId": "TXN_20251107_123456",
"amountDisputed": 50.00,
"status": "OPEN",
"reasonCode": "FRAUDULENT",
"responseDeadline": "2025-12-15T23:59:59Z"
}
}
}
Token Payments (Card on File)
Save customer cards for one-click payments and recurring billing. Supports both IGFS and Shift4 gateways.
PREAUTH_VOUCHER_CAPTURE- Classic flow with meal vouchers (default)SALE_DIRECT- Direct sale without vouchersSALE_WITH_TOKENIZATION- Direct sale + save card for future payments
1. List Saved Cards
Get all saved cards for a customer:
GET/api/token-payment/cards?customerId={customerId}
Headers:
Authorization: Bearer sk_test_YourSecretKey
Response:
{
"success": true,
"customerId": "cust_123",
"cards": [
{
"id": 1,
"alias": "VISA ****4242",
"brand": "VISA",
"last4": "4242",
"expiry": "12/26",
"gateway": "IGFS",
"isDefault": true,
"lastUsedAt": "2025-12-08T10:00:00",
"usageCount": 5
}
],
"count": 1
}
2. Pay with Saved Card (One-Click)
Charge a saved card without requiring customer to re-enter details:
POST/api/token-payment/pay
Headers:
Authorization: Bearer sk_test_YourSecretKey
Content-Type: application/json
Request Body:
{
"cardId": 1,
"amount": 50.00,
"customerId": "cust_123",
"customerEmail": "customer@example.com",
"description": "Subscription renewal"
}
Response (Success):
{
"success": true,
"transactionId": "TKN_ABC123DEF456",
"paymentId": "IGFS_PAY_789",
"gateway": "IGFS",
"status": "COMPLETED",
"cardAlias": "VISA ****4242",
"amount": 50.00
}
Response (Failed):
{
"success": false,
"transactionId": "TKN_ABC123DEF456",
"status": "FAILED",
"error": "Card declined"
}
3. Save Card from Transaction
After a successful payment with tokenization enabled, save the card:
POST/api/token-payment/save-card
Headers:
Authorization: Bearer sk_test_YourSecretKey
Content-Type: application/json
Request Body:
{
"transactionId": "TXN_20251208_123456",
"customerId": "cust_123",
"gateway": "IGFS"
}
Response:
{
"success": true,
"cardId": 1,
"alias": "VISA ****4242",
"brand": "VISA",
"last4": "4242",
"isDefault": true
}
4. Set Default Card
POST/api/token-payment/cards/{cardId}/set-default?customerId={customerId}
Headers:
Authorization: Bearer sk_test_YourSecretKey
Response:
{
"success": true,
"cardId": 1,
"alias": "VISA ****4242",
"isDefault": true
}
5. Delete Saved Card
DELETE/api/token-payment/cards/{cardId}
Headers:
Authorization: Bearer sk_test_YourSecretKey
Response:
{
"success": true,
"message": "Card deleted successfully"
}
Integration Flow
First Payment (with tokenization): 1. Create payment with paymentMode = SALE_WITH_TOKENIZATION 2. Customer completes payment on gateway page 3. Gateway returns card token 4. Call POST /api/token-payment/save-card to store card 5. Card is now available for future payments Subsequent Payments (one-click): 1. Call GET /api/token-payment/cards to get customer's cards 2. Display saved cards to customer 3. Customer selects a card 4. Call POST /api/token-payment/pay with cardId 5. Payment is processed server-to-server (no redirect)
- Card tokens are gateway-specific (IGFS or Shift4)
- Tokens can only be used with the same merchant that created them
- Always obtain customer consent before saving cards (GDPR compliance)
- Implement proper customer authentication before showing saved cards
IGFS Adapter β migrate without changing code
If you integrate directly against IGFS (N&TS/Nexi PGW REST), you can move onto PayGlobe by repointing three configuration values. The adapter speaks the native IGFS wire protocol on the inbound side and routes to PayGlobe behind it. This is the IGFS counterpart of Mandala, our Stripe-compatible adapter.
What changes on your side
| Value | Before (real IGFS) | After (adapter) |
|---|---|---|
| Base URL | https://β¦netsgroup.com/MONEYNET_CG_SERVICES | https://api.payglobe.it/MONEYNET_CG_SERVICES |
merId (= signature keyId) | assigned by N&TS | issued by PayGlobe β max 16 chars per the spec |
| secret (HMAC) | assigned by N&TS | issued by PayGlobe |
Everything else is byte-identical: same paths, same txHead/txReq/txRes JSON, same HTTP-Signature (draft-cavage, HMAC-SHA256), resultCode: "IGFS_000" on success, amounts in cents. The context path stays /MONEYNET_CG_SERVICES because the signature covers (request-target) β only the host moves.
Operations
| Endpoint | Status |
|---|---|
POST /pgw/payment/init | Available |
GET /pgw/payment/verify (alias /verification) | Available |
POST /pgw/payment/capture | Available |
POST /pgw/payment/reversal | Available |
POST /pgw/payment/refund | Available |
GET /pgw/tokenizer/check, DELETE /pgw/tokenizer/delete | Coming soon |
/pgw/payment/selector, pay-by-mail, checkout | Not yet β returns 404 |
Signature β unchanged
Keep computing the signature exactly as you do today: same scheme, same algorithm, same libraries. We do not restate the specification here β it is in your N&TS documentation. Only two practical notes about pointing it at us:
- The
Hostheader you send and the host value you sign must be the same string, and both must beapi.payglobe.it. Derive the host from the Base URL rather than configuring it separately β a leftover host from the previous endpoint is by far the most common cause of a 401. - The signature
keyIdmust equal themerIdin the request body; a mismatch is rejected.
- The
redirectURLpoints to the PayGlobe checkout instead of the Nets host β follow it as usual. - The
orderblock (addresses, products, invoice, VAT) and buyer identity fields are not forwarded yet. Those feed 3DS2 and anti-fraud, so expect more authentication challenges than with direct IGFS. If you track conversion on direct IGFS, keep those numbers as your baseline. paymentIdis currently the PayGlobe identifier (36 chars) where the spec allows 32 and IGFS returns 18 numeric digits. Treat it as an opaque string.- The async notification may be disabled on your merchant depending on webhook configuration.
GET /pgw/payment/verifyis authoritative in IGFS regardless β poll it.
Full walkthrough, architecture and migration steps: igfs-adapter.html. For credentials, contact support@payglobe.it.
Subscriptions & Recurring Payments
Netflix-style recurring billing with shareable subscribe links, dunning/retry policies, custom checkout fields, and merchant-side reference tracking. Manage everything from /paymentgw/merchant-dashboard.html β tab Subscriptions.
ZERO_AUTH token source returns 501 not_implemented β use CHARGE_NOW (with optional setup fee) or /import-token instead.
Plans
A plan defines what you sell on a recurring basis: price, billing interval, optional trial, fees, retry policy, custom checkout fields. Plans are merchant-scoped and identified by a plan_code (lowercase, underscores).
POST /api/subscriptions/plans
Authorization: Bearer sk_test_...
Content-Type: application/json
{
"code": "plan_premium_monthly",
"name": "Premium Monthly",
"description": "Full premium access",
"price": 9.99,
"currency": "EUR",
"interval": "MONTH",
"intervalCount": 1,
"setupFee": 0,
"cancelFee": 0,
"cancelFeeMinCycles": 0,
"duration": "INFINITE", // INFINITE | FIXED_CYCLES | FIXED_DATE
"durationCycles": 0,
"durationAfter": "END", // END | RENEW | SWITCH
"durationDate": null,
"trial": false,
"trialDays": 0,
"trialRequireCard": false,
"retry": "DEFAULT", // DEFAULT | CUSTOM (with retryPolicy nested)
"proration": "PRORATE",
"tax": "EXCLUSIVE",
"customFields": [
{ "key": "order_number", "label": "Order number", "type": "text", "required": true, "primary": true }
]
}
Customer onboarding link (the share URL)
Each plan has a public landing page. Generate the URL from the dashboard Share button on a plan card (which auto-fills the publishable key), or build it manually:
GET https://api.payglobe.it/paymentgw/subscribe.html?plan={plan_code}
&pk={publishable_key} // required: pk_test_... or pk_live_... (safe to expose)
&ref={your_internal_id} // optional, becomes merchantReference
&customer={customer_email} // optional, pre-fills the form
The landing page calls two public endpoints that authenticate via the publishable key:
GET /api/public/subscriptions/plans/{code}?pk=...β returns plan info (no internal fields)POST /api/public/subscriptions?pk=...β creates the subscription withpaymentMethod.source=CHARGE_NOWonly; returnscheckoutUrlfor the IGFS-hosted card capture
The customer:
- Sees the plan summary (price, trial if any, fees if any)
- Fills email + name + any plan custom fields
- Enters card details (tokenized via PayGlobe.js / IGFS / Shift4 β same flow as one-shot payments)
- Subscribes β receives a confirmation email + access to the customer portal
Identifying the customer in your system: merchantReference
You have two ways to tag a subscription with your internal ID. Both populate merchantReference on the subscription record and in every webhook:
A. URL parameter (recommended for guided flows)
Best when your CRM/ERP generates the link per customer.
/subscribe.html?plan=plan_premium_monthly
&ref=ORDER-12345
&customer=customer@example.com
B. Custom field marked primary
Best when the link is published generically (QR on receipts, social, posters). Customer fills the field at checkout.
In the plan, mark one type=text, required=true field with primary=true β its value populates merchantReference automatically when ?ref= is absent.
Webhook payload (subscription.created)
When a customer subscribes, your webhookUrl receives a signed POST (see Webhook Notifications section above). Subscription events:
POST {merchant.webhookUrl}
X-PayGlobe-Signature: {HMAC-SHA256 of body using your webhookSecret}
Content-Type: application/json
{
"event": "subscription.created",
"subscriptionId": "sub_a1b2c3d4",
"planCode": "plan_premium_monthly",
"merchantReference": "ORDER-12345", // from ?ref= or primary custom field
"customFields": {
"order_number": "ORDER-12345"
},
"customer": {
"email": "customer@example.com",
"name": "Mario Rossi"
},
"card": { "brand": "VISA", "last4": "4242", "expiry": "12/27" },
"status": "ACTIVE", // or TRIALING
"nextBilling": "2026-06-08",
"amount": 9.99,
"currency": "EUR",
"createdAt": "2026-05-08T10:23:45Z"
}
Other subscription events you can subscribe to:
subscription.createdsubscription.activated(after trial)subscription.renewed(each successful charge)subscription.payment_failed(each retry attempt)subscription.past_due(entered dunning)subscription.recovered(back to ACTIVE after retry)subscription.canceled(by customer or after final retry per policy)subscription.paused/subscription.resumedsubscription.ended(reached end of fixed duration)
Token sources for recurring charges
A subscription needs a card token that supports MIT (Merchant Initiated Transactions). PayGlobe accepts three sources:
| Source | How | Use case |
|---|---|---|
| Charge & tokenize | Customer pays the first invoice via the hosted page; the resulting token is reused for renewals. | Standard SaaS / streaming / paid service |
| Zero-amount auth | Card is authorized for β¬0 (or 1 cent in some schemes) just to obtain a token; first charge happens on the trial-end date. | Trials / "free for X days, then charge" |
| Imported token (POS / external) | You already obtained a token from a physical POS or another integration. Inject it via POST /api/subscriptions/import-token. |
Phygital sales (in-store sign-up for ongoing service), B2B contracts signed in person |
POST /api/subscriptions/import-token
Authorization: Bearer sk_test_...
Content-Type: application/json
{
"token": "tok_pos_xxxxxxxxxx",
"gateway": "IGFS", // IGFS | SHIFT4
"customer": { "email": "...", "name": "..." },
"planCode": "plan_premium_monthly",
"merchantReference": "ORDER-12345",
"customFields": { "order_number": "ORDER-12345" }
}
Lifecycle & status transitions
TRIALING β(trial ends, charge ok)ββ ACTIVE
ACTIVE β(charge fails)βββββββββββ PAST_DUE
PAST_DUE β(retry succeeds)βββββββββ ACTIVE
PAST_DUE β(all retries fail)βββββββ {policy.after: CANCELED | PAUSED | UNPAID}
ACTIVE β(merchant pauses)ββββββββ PAUSED
PAUSED β(merchant resumes)βββββββ ACTIVE
ACTIVE β(merchant or cust. cancels)β CANCELED
ACTIVE β(reaches FIXED_CYCLES end)ββ ENDED | renew | switch (per plan.durationAfter)
Retry policy (dunning)
Define when failed charges are retried. Per-merchant default + optional per-plan override.
{
"max": 4, // max attempts after the original failure
"schedule": [1, 3, 5, 7], // days after failure for attempts 1..N
"after": "CANCEL", // CANCEL | PAUSE | MARK_UNPAID
"emails": true, // send dunning emails to customer
"webhook": false // also fire webhook on each retry
}
Endpoints overview
| Method | Path | Purpose |
|---|---|---|
| POST | /api/subscriptions/plans | Create plan |
| GET | /api/subscriptions/plans | List plans |
| GET | /api/subscriptions/plans/{code} | Read plan (also used by subscribe page) |
| PUT | /api/subscriptions/plans/{code} | Update plan |
| DELETE | /api/subscriptions/plans/{code} | Delete / archive plan |
| POST | /api/subscriptions | Create subscription (server-side, with charge or zero-auth) |
| POST | /api/subscriptions/import-token | Create subscription using a pre-existing card token |
| GET | /api/subscriptions | List subscriptions (filters: status, plan, ref, customer) |
| GET | /api/subscriptions/{id} | Read subscription detail |
| POST | /api/subscriptions/{id}/pause | Pause |
| POST | /api/subscriptions/{id}/resume | Resume |
| POST | /api/subscriptions/{id}/cancel | Cancel (immediate or end-of-cycle) |
| POST | /api/subscriptions/{id}/change-plan | Switch to another plan (proration: PRORATE | FULL | NONE) |
| POST | /api/subscriptions/{id}/update-card | Replace the stored token (source=EXISTING_TOKEN only in V1) |
| POST | /api/subscriptions/{id}/retry | Manual retry of a PAST_DUE charge |
| GET | /api/subscriptions/retry-policy | Read merchant default retry policy |
| PUT | /api/subscriptions/retry-policy | Update merchant default retry policy |
| Customer-facing (publishable key auth) | ||
| GET | /api/public/subscriptions/plans/{code}?pk=... | Read plan from subscribe.html (no auth header, pk in query) |
| POST | /api/public/subscriptions?pk=... | Enroll a customer (CHARGE_NOW only) β returns checkoutUrl |
Renewal & retry engine
Two background jobs run on a configurable cadence (defaults: renewal every 10 min, retry every 15 min, offset by 5):
- Renewal cycle β picks every
ACTIVE/TRIALINGsub whosenextBillingAtis in the past, charges it viaIgfsService.initSaleWithToken(server-to-server, no 3DS, ~1 sec). On success: advances the period, incrementscyclesCompleted, firessubscription.renewed. On failure: opens aSubscriptionInvoice, transitions toPAST_DUE, schedules the first retry per policy. - Retry cycle β for
PAST_DUEsubs whosenextRetryAtis due, replays the open invoice. Each attempt is recorded insubscription_retry_attempts. Afterpolicy.maxfailures, appliespolicy.after(CANCEL / PAUSE / MARK_UNPAID) and fires the matching webhook.
Cron strings are tunable via payglobe.subscriptions.renewal-cron / payglobe.subscriptions.retry-cron in application.properties. Set payglobe.subscriptions.scheduler.enabled=false to disable both.
See SUBSCRIPTIONS.md in the project root for the full design doc with examples in Node, PHP, and Python.
Support
π Documentation
OpenAPI YAML specification with complete API reference
π§ͺ Postman Collection
Ready-to-use API tests for all integration scenarios
π¬ Technical Support
Contact: support@payglobe.com