Esc
to see everything again.
- Download Postman collection and YAML spec
- Configure your notification webhook URL
- Integrate the Wrapper API (4 endpoints only)
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);
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.Payment Flow (3 Steps)
Your System β Gateway β Card Processor
Preauthorize the amount on the card (hold funds)
Your System β Gateway (API, when you decide):
β’
POST /api/payment/capture/{id} β collect the full or a partial amountβ’
POST /api/payment/void/{id} β release the hold
Gateway β Your Webhook
JSON with payment details
Payment Flow (2 Steps)
Your System β Gateway β Card Processor
Single call: Auth + Capture in one step
Not needed - already captured in step 1
Gateway β Your Webhook
JSON with payment details
| Feature | PREAUTH + CAPTURE | SALE DIRECT |
|---|---|---|
| Steps | 3 steps (capture/void via API) | 2 steps (simple flow) |
| Card Operation | Preauth β Capture/Void | Single Sale (Auth+Capture) |
| User Interaction | Direct payment; you capture or release later | Direct payment |
| Settlement | After capture | Immediate |
| Payment Mode Config | PREAUTH_VOUCHER_CAPTURE (explicit opt-in; the name is historical) |
SALE_DIRECT (default) |
Step 1: Initialize Payment
POST/api/payment/initiate
{
"amount": 150.00,
"orderId": "ORD-1029",
"description": "Order #1029",
"email": "buyer@example.com",
"language": "it"
}
Response:
{
"transactionId": "af6d4def-ae25-43e0-b4f4-b19d4015b604",
"status": "IGFS_INPUT_PRESENTED",
"gateway": "IGFS",
"checkoutUrl": "https://api.payglobe.it/paymentgw/payment/checkout/af6d4def-...",
"igfsRedirectUrl": "https://testpaye.netsgroup.com/...",
"igfsPaymentId": "237840183108800560"
}
checkoutUrl, not to igfsRedirectUrl.
checkoutUrl is hosted by PayGlobe: the buyer never sees the
PSP's domain, and the return to your site is handled for you. igfsRedirectUrl
is the underlying PSP page, and is only useful if you are building your own checkout.
There is no field named redirectUrl without a prefix.
Step 2: Get Transaction Info
GET/api/payment/status/{transactionId}
Response:
{
"transactionId": "af6d4def-ae25-43e0-b4f4-b19d4015b604",
"status": "COMPLETED",
"totalAmount": 150.00,
"createdAt": "2026-08-25T12:24:11",
"updatedAt": "2026-08-25T12:29:03",
"orderId": "ORD-1029",
"description": "Order #1029"
}
orderId, description and errorMessage appear only when set.
A transaction owned by another merchant returns 404, not 403.
After Step 1, the system automatically handles:
- Card payment on the hosted checkout (via configured payment gateway)
- Direct sale (default), or a pre-authorization hold that you capture or release via API if your merchant is configured for it
- 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"
}
customerEmail is required only with linkType: "MAIL"
(we send the email). A LINK can be created without it β for example a restaurant bill
printed with the QR code, paid at the table from the customer's phone.
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 |
| POST | /api/payment-links/{id}/send-email | Send (or resend) the payment email of a pending link. Optional body
{"email": "..."}: a new address replaces the stored one; without it the
stored address is used. Works also for a LINK created without email (e.g. a
bill printed with a QR code). Response carries emailSent; a paid, cancelled or
expired link answers 409. |
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
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
http or
https (use https in production), at most 500 characters, with no
spaces or quotes, and its host must resolve to a public address.
localhost, private networks (10.x, 172.16–31.x,
192.168.x), link-local 169.254.x.x (cloud metadata), carrier-grade
NAT 100.64.0.0/10 and IPv6 private ranges are refused, and so is a host name that
does not resolve in DNS. Saving such a URL returns HTTP 400 with the reason, e.g.
webhookUrl resolves to internal address 10.0.0.5: only public addresses are allowed.
The check is repeated before every delivery: if your host later starts resolving to an internal address, that notification is not sent and not retried, and the delivery is recorded as blocked. A DNS lookup that temporarily fails does not block it: the normal retries apply. The same rules apply to the URLs of the Send Webhook, Webhook Branching and Wait for External workflow nodes, checked after the
{{variables}} are filled in; a blocked call counts as failed (the
“no” branch). To receive webhooks on a development machine, expose it with a public
tunnel (e.g. ngrok) or use webhook.site.
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": 150.00,
"voucherAmount": 0.00,
"paymentMethod": "CARD",
"status": "SUCCESS",
"sessionStart": "2025-11-07T10:00:00Z",
"sessionEnd": "2025-11-07T10:05:23Z",
"sessionDurationSeconds": 323,
"vouchersUsed": 0,
"cardGatewayDetails": {
"paymentId": "PAY_789",
"transactionId": "IGFS_123456789",
"operationType": "CAPTURE",
"preauthorizedAmount": 150.00,
"capturedAmount": 150.00,
"responseCode": "00",
"responseMessage": "Transaction approved"
},
"voucherDetails": null,
"createdAt": "2025-11-07T10:00:00Z",
"completedAt": "2025-11-07T10:05:23Z"
}
Note: voucherAmount, vouchersUsed and
voucherDetails stay in the payload for backward compatibility, but the meal-voucher connector is
closed: they are always 0 / null.
- 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
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
If your code is written against Stripe - the Stripe SDK, a Magento 2 or WooCommerce
Stripe plugin - use the Mandala adapter at
https://api.payglobe.it/mandala/. It speaks Stripe properly: Stripe endpoints,
Stripe event types, and a real Stripe-Signature in the
t={timestamp},v1={hex} format that constructEvent() accepts.
Nothing in your integration changes except the base URL and the keys.
This page documents the native PayGlobe API. It is the one to integrate against when you are writing new code rather than porting a Stripe integration.
The event envelope below has the same shape as a Stripe event, so the code that
reads the payload maps over. The signature does not: it is
Base64(HMAC-SHA256(raw body, webhookSecret)) in
X-PayGlobe-Signature.
X-PayGlobe-Signature is the only signature header this API
sends. Verify it yourself, with a constant-time comparison, over the raw body before any
parsing - about ten lines, shown below.
Webhook Event Structure:
Payment data is wrapped in an 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": 150.00,
"voucherAmount": 0.00, // always 0: voucher connector closed
"paymentMethod": "CARD",
"status": "SUCCESS",
"cardGatewayDetails": { ... },
"voucherDetails": null // always null
}
},
"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 failed) | Active |
payment_intent.processing |
Payment in progress | Active |
charge.refunded |
Refunds fire no webhook. Re-read GET /api/payment/status/{transactionId},
which becomes REFUNDED or PARTIAL_REFUNDED. |
Not sent |
checkout.session.completed |
Not sent. A completed payment arrives as payment_intent.succeeded. |
Not sent |
Verifying a Webhook (Node.js):
const crypto = require('crypto');
// express.raw is required: the signature covers the RAW body.
// Any JSON parse-and-reserialize before this point changes the bytes
// and the signature will never match.
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const received = req.headers['x-payglobe-signature'];
const expected = crypto
.createHmac('sha256', process.env.PAYGLOBE_WEBHOOK_SECRET) // whsec_...
.update(req.body)
.digest('base64');
const a = Buffer.from(expected);
const b = Buffer.from(received || '');
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(400).send('Invalid signature');
}
const event = JSON.parse(req.body);
switch (event.type) {
case 'payment_intent.succeeded': {
const payment = event.data.object;
console.log('Paid:', payment.transactionId, payment.totalAmount);
console.log('Card:', payment.cardAmount);
break;
}
case 'payment_intent.payment_failed':
console.log('Failed:', event.data.object.transactionId);
break;
case 'payment_intent.processing':
// Intermediate state - not an outcome. Wait for a later event.
break;
}
// Answer 2xx quickly: we retry 10 times, 5 minutes apart, until you do.
res.status(200).send('OK');
});
There is no charge.refunded and no
checkout.session.completed. In particular a refund fires no webhook
at all: after refunding, re-read
GET /api/payment/status/{transactionId}, which becomes
REFUNDED or PARTIAL_REFUNDED.
Verifying a Webhook (Python):
import hashlib, hmac, base64, json
@app.route('/webhook', methods=['POST'])
def webhook():
# get_data() gives the RAW body. The signature covers those exact bytes.
payload = request.get_data()
received = request.headers.get('X-PayGlobe-Signature', '')
expected = base64.b64encode(
hmac.new(WEBHOOK_SECRET.encode(), payload, hashlib.sha256).digest()
).decode()
if not hmac.compare_digest(expected, received):
return 'Invalid signature', 400
event = json.loads(payload)
if event['type'] == 'payment_intent.succeeded':
payment = event['data']['object']
print(f"Paid: {payment['transactionId']} {payment['totalAmount']}")
print(f"Card: {payment['cardAmount']}")
elif event['type'] == 'payment_intent.payment_failed':
print(f"Failed: {event['data']['object']['transactionId']}")
elif event['type'] == 'payment_intent.processing':
pass # intermediate state, not an outcome
return '', 200
Stripe-Signature header? It is gone.
Until 25 August 2026 this API also sent the same value under that name. It was
never Stripe's t={timestamp},v1={hex} format, so no Stripe SDK could verify it
anyway. If your handler reads Stripe-Signature, switch it to
X-PayGlobe-Signature - same value, same verification. For an integration a
Stripe SDK drives end to end, use the Mandala adapter instead of this API.
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 | Payload maps over; signature verified with ~10 lines (see above). For a Stripe SDK end to end, use the Mandala adapter. |
| Multi-Gateway | Stripe only | IGFS, Stripe, Nexi, Worldline, etc. |
| Detailed Gateway Info | Limited | Full details (cardGatewayDetails) |
| 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.
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, PayPal, 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 Type | Status | Action |
|---|---|---|
| Card Preauth Failed | CARD_PREAUTH_FAILED | Inform user, retry or cancel |
| Capture Failed | CARD_CAPTURE_FAILED | Contact support, transaction will auto-rollback |
| Transaction Timeout | FAILED | Check status, may need manual intervention |
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
- Sandbox: Contact support for test environment access
π 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 on every call
that moves money. A repeat carrying the same key returns the original response
instead of executing again, so a lost connection costs you nothing.
It matters most on partial refunds. Repeating a full refund is refused anyway, because no balance remains; but a repeated partial refund is indistinguishable from a legitimate second one and, without a key, will refund again.
One fresh key per operation. Reusing a key with a different body returns
409: that is a client bug, not a new operation. If a call fails
without a confirmed outcome, retry it with the same key rather
than building a new request — that is the whole point of the header.
Honoured on:
/api/payment/capture|refund|void/{id},
/api/merchant/payment/refund|capture|void/{id},
/api/token-payment/pay and
/api/payment-links/create. Keys are remembered for 30 days.
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. Check What Was Refunded
There is no refund-history endpoint. The refund response already carries the totals, and the transaction status reflects them afterwards:
The refund response already tells you:
{
"refundedNow": 50.00,
"refundedTotal": 50.00,
"remainingRefundable": 100.00,
"status": "PARTIAL_REFUNDED"
}
Afterwards, re-read the transaction:
curl https://api.payglobe.it/paymentgw/api/payment/status/abc123 \
-H "Authorization: Bearer sk_test_YourSecretKeyHere"
status becomes REFUNDED or PARTIAL_REFUNDED.
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.
Connect an AI assistant to your PayGlobe account and let it read your transactions, build payment links, or issue refunds — with only the permissions you grant and a spending limit you set. PayGlobe exposes a Model Context Protocol (MCP) server, so any MCP client can talk to it.
Agents get their own credential, never your secret key
An agent authenticates with an agent key (ak_live_… /
ak_test_…), issued from AI Agents in your dashboard. You have
one secret key, and it is the one your server integrations use. Handing it to an assistant would
mean that revoking the assistant forces you to rotate the key that everything else depends on
— so in practice you would never revoke it.
An agent key is separate by design: revoking one agent changes nothing else.
| Key | Who holds it | What it can do |
|---|---|---|
pk_ | your web pages, publicly visible | create and read payments |
sk_ | your servers only | everything, including moving money |
ak_ | one AI agent | only the permissions you granted, within the limits you set |
Permissions
Grant the narrowest set that does the job. Read-only agents cannot move money no matter what happens to them.
| Scope | Grants |
|---|---|
transactions:read | Read your transactions, including customer details. |
stats:read | Read revenue and volume figures. |
links:read | Read your payment links. |
disputes:read | Read disputes and chargebacks. |
subscriptions:read | Read subscriptions and their billing state. |
links:write | Create and cancel payment links in your name. |
voids:write | Release held funds on a pre-authorization. This cancels revenue. |
subscriptions:retry | Retries a failed renewal. This charges your customer. |
refunds:write | Sends money back to your customer. |
captures:write | Charges held funds. |
stored_cards:charge | Charges a card your customer has saved, against an order you created. See below — it has rules of its own. |
Spending limits are mandatory, not optional
Grant refunds:write or captures:write and you must set a
maximum per operation; a daily maximum is also available. Both are enforced on
every single call, before anything reaches the payment provider.
What an agent can never do
Agent access is allow-listed: a route is unavailable to an agent unless it has been explicitly opened. Some operations are never delegable, whatever permissions you grant, because they have no remedy after the fact:
- rotate or read your API keys
- change your two-factor setup
- change your payment provider credentials or your webhook URL
- take a payment over the phone (MO/TO)
- list, save or delete your customers’ stored cards — charging one is
possible with
stored_cards:charge, but reading the card list never is - create another agent — otherwise revoking one would close nothing
Expiry, revocation, audit
- Every agent key expires, at most 90 days out. Credentials that never expire are the ones that get forgotten in someone else’s configuration.
- Revocation takes effect on the agent’s next call, and it also kills any OAuth token issued for that agent.
- Every call is logged — allowed and refused, with the operation, the amount and the outcome. Refusals are the interesting half: an agent repeatedly asking for permissions it does not have is the sign that something is manipulating it.
What the assistant can do
Ten tools, each tied to a permission. An assistant sees only the ones your grant covers — if you grant read access, the refund tool does not appear at all.
| Tool | Permission | What it does |
|---|---|---|
search_transactions | transactions:read | Search by date, status or method. |
get_payment | transactions:read | One payment by transaction id or your own order id, with how much is still refundable. |
get_transaction_stats | stats:read | Collected and failed totals over a period. |
list_payment_links | links:read | Your payment links. |
create_payment_link | links:write | Creates a shareable link. It does not email it — you send it. |
list_disputes | disputes:read | Chargebacks, with the response deadline. Read only. |
list_subscriptions | subscriptions:read | Filter by status to find renewals that failed to collect. |
get_subscription | subscriptions:read | One subscription in full: plan, amount, next billing, retry state. |
retry_renewal | subscriptions:retry | Charges the customer. The assistant must state the amount, and it is checked against what is actually due first. |
refund_payment | refunds:write | Sends money back. Full or partial. |
Letting an agent buy: orders, and what it may buy
An agent can charge a card your customer has already saved — a reorder, a refill, a standing arrangement — without anyone at a screen. Two things make that safe enough to offer, and both are worth understanding before you grant it.
1. You create the order. The agent only confirms it.
The agent does not tell PayGlobe how much to charge. You do, with your secret key, before the agent acts:
POST /api/agent-orders Authorization: Bearer sk_live_...
{
"customerId": "cust_42",
"amount": 24.90,
"category": "groceries",
"description": "Weekly coffee refill",
"expiresInMinutes": 60
}
→ { "orderRef": "aord_Xk3p...", "status": "OPEN", "expiresAt": "..." }
The agent then settles it, and states no amount at all:
POST /api/token-payment/pay Authorization: Bearer ak_live_...
{ "cardId": 7, "customerId": "cust_42", "agentOrderRef": "aord_Xk3p..." }
groceries next to a television. An assistant reads text written by other
people, and that text can tell it to do exactly that. So the figures come from you, and the agent
only says yes.
- Agents cannot create orders.
/api/agent-ordersis closed to them, whatever permissions you grant — otherwise an agent would be writing its own permission slip. - One order, one payment. Two concurrent attempts on the same order: one wins,
the other gets
409. If the card is declined the order reopens by itself. - Orders are short-lived — 60 minutes by default, 24 hours at most. An order holds a price, so it must not outlive it.
- If the agent does send an
amount, it must match the order exactly. A mismatch is refused rather than silently overridden: it means the agent thinks it is buying something else.
2. Limits per merchandise category
On top of the per-operation and daily ceilings, you can say what the agent is allowed to buy, in your own category words:
| Limit | Example | What it stops |
|---|---|---|
| Amount per purchase | groceries, max EUR 30 | one purchase that is too big |
| Amount per day | groceries, max EUR 90/day | an agent that keeps going |
| Purchases per day | max 3 | small amounts, many times — ten EUR 3 reorders break no money ceiling and are still ten deliveries nobody asked for |
| The list itself | only groceries, household | everything else |
* as the
category to cover everything you have not named.
Two more rules worth knowing: a category limit can only tighten the agent’s
ceilings, never widen them (that is checked when you issue the credential, not when the money
moves); and the category on the order is matched against your rules exactly, so a
typo does not nearly match — it falls through to *, or it is refused.
What this is, and what it is not
This is your agent, charging your customer’s saved card, on an order you wrote. It is not a buyer’s shopping agent arriving with its own payment credential — that does not work anywhere today, and not for want of code.
Practical consequences you should weigh before granting it: the charge carries no 3-D Secure, so chargebacks stay with you; and the card can only be charged on the channel where your customer saved it.
Reading what comes back
Every transaction carries three fields that exist so a machine reads it correctly:
statusis the raw gateway code — there are 77 of them, composed per provider.statusFamilyis the translation:COMPLETED,AUTHORIZED(funds held),PENDING,FAILED,CANCELLED,REFUNDED,DISPUTED. Read this one.isTerminalsays whether it can still change, so an assistant knows whether checking again later is worth anything.
Dates are ISO 8601 with offset. Amounts are decimal EUR.
Connect an MCP client
The MCP endpoint is POST https://api.payglobe.it/paymentgw/mcp. For Claude Desktop,
add this to claude_desktop_config.json and restart the app:
{
"mcpServers": {
"payglobe": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://api.payglobe.it/paymentgw/mcp",
"--header", "Authorization: Bearer ak_live_YOUR_AGENT_KEY"]
}
}
}
Your assistant sees only the tools your permissions allow. Grant read-only access and the refund tool does not appear at all — so the assistant will not attempt it and will not ask you to enable it.
Third-party agents: OAuth 2.1
If the agent is operated by someone else, they should not hold a key of yours at all. PayGlobe is an OAuth 2.1 authorization server: the application registers itself, you approve it on a consent screen where you pick the permissions and the limits, and it receives a token instead of a key.
GET /.well-known/oauth-protected-resource discovery for the MCP server GET /.well-known/oauth-authorization-server metadata (RFC 8414) POST /oauth/register dynamic client registration (RFC 7591) GET /oauth/authorize consent screen POST /oauth/token authorization_code + PKCE (S256), refresh_token POST /oauth/revoke revocation (RFC 7009)
Most MCP clients discover all of this on their own: an unauthenticated call to the MCP endpoint
answers 401 with a WWW-Authenticate header pointing at the metadata.
Errors you should handle
Every refusal carries retryable, and it is derived from the error code rather than
guessed. An ambiguous error sends an agent into a loop, and a loop against a payment gateway costs
real money.
| Error | Retryable | Meaning |
|---|---|---|
RATE_LIMITED | yes | Too many calls this minute. It clears by itself. |
SCOPE_MISSING | no | The agent does not have the permission that operation needs. |
ROUTE_NOT_DELEGABLE | no | Not available to agents at all. Use a secret key. |
AMOUNT_REQUIRED | no | A money-moving call arrived without an explicit amount. |
AMOUNT_INVALID | no | The amount was zero or negative. |
PER_CALL_LIMIT_EXCEEDED | no | Over the per-operation ceiling you set. |
DAILY_LIMIT_EXCEEDED | no | Over the daily ceiling. The message says how much has already moved today. It resets at midnight, but waiting is a decision for a person, not a retry. |
AGENT_REVOKED / AGENT_EXPIRED | no | The credential is no longer valid. Issue a new one. |
AMOUNT_MISMATCH | no | The stated amount is not what is actually due — the subscription rate on retry_renewal, or the order total on a saved-card charge. Nothing was charged. |
ORDER_REQUIRED | no | A saved-card charge arrived without agentOrderRef. Ask the merchant’s system for an order. |
ORDER_NOT_FOUND | no | No such order for this merchant. |
ORDER_NOT_SETTLEABLE | no | Expired, cancelled, or already paid. The message says which. |
ORDER_ALREADY_SETTLED | no | Something else paid this order first. Do not create a second one without checking whether the first went through. |
CUSTOMER_NOT_DELEGATED | no | The mandate covers one customer, and this is not them. |
CATEGORY_NOT_ALLOWED | no | The agent may not buy in that category at all. |
CATEGORY_PER_CALL_LIMIT_EXCEEDED | no | Over the per-purchase ceiling for that category. |
CATEGORY_DAILY_LIMIT_EXCEEDED | no | Over the daily ceiling for that category. |
CATEGORY_COUNT_LIMIT_EXCEEDED | no | Already made the maximum number of purchases in that category today. |
MOTO_NOT_DELEGABLE | no | The request carried the moto flag. Agents never use the telephone terminal. Remove the field. |
409 on the next attempt rather than repeating the call. Check before
retrying, and never work around it by changing the amount or the wording — that is a
second, different operation, and it will go through.
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"
}
}
}
Save customer cards for one-click payments and recurring billing. Supports both IGFS and Shift4 gateways.
SALE_DIRECT- Direct sale, authorization and capture in one step (default)PREAUTH_VOUCHER_CAPTURE- Pre-authorization (hold), captured or released later via API; explicit opt-in, e.g. deposits and booking guarantees (the name is historical, it does not involve vouchers)SALE_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
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.
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.