Integration guide

Take cards and wallets through one integration.

One gateway in front of several payment providers: integrate once, change acquirer without rewriting your checkout. Search for what you need, copy it, go.

Are you a partner bringing several merchants? Your entry points (portal, API, earnings) are all in one place: Partner Program → where to log in.

/
Base URL
https://api.payglobe.it/paymentgw
Authentication
Authorization: Bearer sk_live_...
First call
POST /api/payment/initiate
No section matches those words. Try a single term, or press Esc to see everything again.
start

Quick Start

#
Get started in 3 steps:
  1. Download Postman collection and YAML spec
  2. Configure your notification webhook URL
  3. Integrate the Wrapper API (4 endpoints only)
sdk

PayGlobe.js - JavaScript Library

#
Simplified Integration: Use our JavaScript library for easy client-side integration with modern payment gateways

1. Include the Library

Include PayGlobe.js HTML β–Ό
<!-- 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 JS β–Ό
// Initialize PayGlobe with your publishable key
const payglobe = PayGlobe('pk_test_PayGlobe123456789TestPublishable');

3. Create a Payment

Create Payment JS β–Ό
// 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);
πŸ“§ Email Parameter: The customer's email address is used to send payment confirmation receipts. This field is recommended for a better customer experience. If not provided, a default email will be used.

4. Redirect to Checkout

Redirect to Checkout JS β–Ό
// 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

Check Payment Status JS β–Ό
// 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

Full HTML Example HTML β–Ό
<!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>
Note: PayGlobe.js handles all API communication and error handling automatically. You only need your publishable key (starts with pk_test_ or pk_live_).
Try the Live Example

See the above code in action with a working demo:

Open Live Demo β†’

This example demonstrates a simple payment flow using PayGlobe.js with a working payment button.
concepts

High-Level Architecture

#
PREAUTH + CAPTURE Hold funds, settle later
Payment Flow (3 Steps)
1. PREAUTHORIZATION
Your System β†’ Gateway β†’ Card Processor
Preauthorize the amount on the card (hold funds)
2. CAPTURE OR RELEASE
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
3. NOTIFICATION
Gateway β†’ Your Webhook
JSON with payment details
βœ“ Best for: Deposits, booking guarantees, amounts known only after the service
SALE DIRECT Fast Checkout
Payment Flow (2 Steps)
1. AUTHORIZATION + CAPTURE
Your System β†’ Gateway β†’ Card Processor
Single call: Auth + Capture in one step
CAPTURE OR RELEASE
Not needed - already captured in step 1
2. NOTIFICATION
Gateway β†’ Your Webhook
JSON with payment details
βœ“ Best for: E-commerce, subscriptions, fast checkout
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)
concepts

Integration Flow

#

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"
}
Send the buyer to 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.
Automatic Process
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.

Note: Direct API requires managing transaction state yourself. Use Wrapper API unless you need specific control.

See Postman collection for complete Direct API examples.

api

Webhook Notifications (HMAC Signature)

#

PayGlobe sends server-to-server webhooks to notify you of payment events using HMAC-SHA256 signatures for security.

Self-Service Configuration: Configure your webhook URL in the Merchant Dashboard and receive a unique webhookSecret (e.g., whsec_test_...) for signature verification.
Step 1: Configure Webhook in Merchant Dashboard
  1. Go to Merchant Login and authenticate with your API key
  2. In the Webhook Configuration section, enter your webhook URL (e.g., https://yourdomain.com/api/payment-webhook)
  3. Copy the Webhook Secret (shown once, format: whsec_test_abc123...)
  4. Save the secret in your backend environment variables
Which webhook URLs are accepted. The URL must be 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.

⚠️ Important:
  • Always verify the signature before processing webhooks
  • Use the RAW request body for signature verification (not parsed JSON)
  • Store webhookSecret securely (environment variable, secrets manager)
  • Return 200 OK quickly; 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:

  1. Go to webhook.site and copy unique URL
  2. Configure this URL as webhook in Admin Panel
  3. Copy webhook secret shown
  4. Make a test payment
  5. Verify webhook received with X-PayGlobe-Signature header
migration

Coming from Stripe?

#
Stripe integrations belong on Mandala, not on this API.

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.

On this API, do not verify webhooks with a Stripe library.

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');
});
These three are the only event types.

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
Reading the 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
Migration from Stripe:

If you're migrating from Stripe, your existing webhook code will work with PayGlobe with minimal changes:

  1. Replace STRIPE_WEBHOOK_SECRET with PayGlobe's webhookSecret
  2. Update webhook URL in your configuration
  3. That's it! Your code continues to work.
setup

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:
  1. Contact PayGlobe to open a merchant account
  2. Provide your business details and integration requirements
  3. PayGlobe configures all payment gateway credentials (IGFS, Stripe, PayPal, etc.) on your behalf
  4. Receive your API keys:
    • pk_test_... - Publishable key (safe for frontend)
    • sk_test_... - Secret key (backend only)
  5. Configure your webhook via Merchant Dashboard (self-service)
  6. Start accepting payments using our APIs
Admin Panel Access: The Admin Panel is exclusively for PayGlobe internal use to configure merchant accounts, gateway credentials, and system settings. Merchants manage their own webhooks and view transactions through the Merchant Dashboard.
Need Help? Contact PayGlobe support at support@payglobe.it for account setup, configuration changes, or technical assistance.
api

Error Handling

#
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
api

API Endpoints & Authentication

#

Authentication with API Keys

πŸ” API Key Authentication
All API requests require authentication using API keys. You will receive two keys:
  • Publishable Key (pk_test_xxx or pk_live_xxx): Safe to use in frontend code
  • Secret Key (sk_test_xxx or sk_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"}'
Test Keys: Use test keys (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
security

API Keys: Security & Permissions

#
πŸ”‘ Two Types of API Keys

PayGlobe provides two distinct API keys with different permission levels:

Publishable Key (pk_test_* / pk_live_*)

βœ… 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

Secret Key (sk_test_* / sk_live_*)

⚠️ 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)

πŸ”’ Security Note: Refund operations require the Secret Key for authentication. These operations can ONLY be performed from your backend server, never from client-side JavaScript.
How to read the response β€” this determines whether money moved:
  • 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 gatewayResultCode and gatewayMessage with the PSP's own reason. No money moved and the transaction is left untouched.
Never treat a non-2xx response as "probably fine": a 402 means the refund did not happen and needs a human decision, not an automatic retry.
Partial refunds are not supported by every gateway. Where the underlying PSP can only reverse the full captured amount, a partial request is rejected with a clear error rather than silently refunding everything. Omit amount to refund the full remaining balance.
GatewayRefundPartial refund
Stripeβœ…βœ…
IGFSβœ…βŒ β€” full reversal only
Bizumβœ…βœ…
Skrillβœ…βœ…
Satispayβœ…βœ…
PayPalβœ…βœ…
Amazon Payβœ…βœ… (settled asynchronously)
Zonda (crypto)❌ β€” issue it from the Zonda panelβ€”
Retries: send an 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.
agents

AI Agents & MCP

#

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.

Your assistant operates your payments. This is about giving your tools access to your account. It is not a way for a buyer’s shopping agent to pay you: that is a different thing and it is not what this endpoint does.

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.

KeyWho holds itWhat it can do
pk_your web pages, publicly visiblecreate and read payments
sk_your servers onlyeverything, including moving money
ak_one AI agentonly 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.

ScopeGrants
transactions:readRead your transactions, including customer details.
stats:readRead revenue and volume figures.
links:readRead your payment links.
disputes:readRead disputes and chargebacks.
subscriptions:readRead subscriptions and their billing state.
links:writeCreate and cancel payment links in your name.
voids:writeRelease held funds on a pre-authorization. This cancels revenue.
subscriptions:retryRetries a failed renewal. This charges your customer.
refunds:writeSends money back to your customer.
captures:writeCharges held funds.
stored_cards:chargeCharges 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.

An agent must always state an amount. A refund without an explicit amount means “refund everything” — an amount no per-operation limit can check before the money has already moved. PayGlobe refuses those instead of guessing.

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.

ToolPermissionWhat it does
search_transactionstransactions:readSearch by date, status or method.
get_paymenttransactions:readOne payment by transaction id or your own order id, with how much is still refundable.
get_transaction_statsstats:readCollected and failed totals over a period.
list_payment_linkslinks:readYour payment links.
create_payment_linklinks:writeCreates a shareable link. It does not email it — you send it.
list_disputesdisputes:readChargebacks, with the response deadline. Read only.
list_subscriptionssubscriptions:readFilter by status to find renewals that failed to collect.
get_subscriptionsubscriptions:readOne subscription in full: plan, amount, next billing, retry state.
retry_renewalsubscriptions:retryCharges the customer. The assistant must state the amount, and it is checked against what is actually due first.
refund_paymentrefunds:writeSends money back. Full or partial.
The assistant is told its own limits when it connects. Not “limits exist” but “your ceiling is EUR 2 per operation, EUR 10 per day, EUR 3 already used today”. It knows before it tries, so it does not promise you something that will then be refused.

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..." }
This is the whole reason the limits below mean anything. If the agent could state the amount and the category itself, then “EUR 50 in groceries” would be worked around by writing 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-orders is 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:

LimitExampleWhat it stops
Amount per purchasegroceries, max EUR 30one purchase that is too big
Amount per daygroceries, max EUR 90/dayan agent that keeps going
Purchases per daymax 3small amounts, many times — ten EUR 3 reorders break no money ceiling and are still ten deliveries nobody asked for
The list itselfonly groceries, householdeverything else
The list is the permission, not a set of exceptions. Name even one category and it becomes closed: anything you have not listed is refused, whatever the amount. A new category in your catalogue does not become spendable because nobody remembered to forbid it. Use * 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:

  • status is the raw gateway code — there are 77 of them, composed per provider.
  • statusFamily is the translation: COMPLETED, AUTHORIZED (funds held), PENDING, FAILED, CANCELLED, REFUNDED, DISPUTED. Read this one.
  • isTerminal says 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.

ErrorRetryableMeaning
RATE_LIMITEDyesToo many calls this minute. It clears by itself.
SCOPE_MISSINGnoThe agent does not have the permission that operation needs.
ROUTE_NOT_DELEGABLEnoNot available to agents at all. Use a secret key.
AMOUNT_REQUIREDnoA money-moving call arrived without an explicit amount.
AMOUNT_INVALIDnoThe amount was zero or negative.
PER_CALL_LIMIT_EXCEEDEDnoOver the per-operation ceiling you set.
DAILY_LIMIT_EXCEEDEDnoOver 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_EXPIREDnoThe credential is no longer valid. Issue a new one.
AMOUNT_MISMATCHnoThe 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_REQUIREDnoA saved-card charge arrived without agentOrderRef. Ask the merchant’s system for an order.
ORDER_NOT_FOUNDnoNo such order for this merchant.
ORDER_NOT_SETTLEABLEnoExpired, cancelled, or already paid. The message says which.
ORDER_ALREADY_SETTLEDnoSomething else paid this order first. Do not create a second one without checking whether the first went through.
CUSTOMER_NOT_DELEGATEDnoThe mandate covers one customer, and this is not them.
CATEGORY_NOT_ALLOWEDnoThe agent may not buy in that category at all.
CATEGORY_PER_CALL_LIMIT_EXCEEDEDnoOver the per-purchase ceiling for that category.
CATEGORY_DAILY_LIMIT_EXCEEDEDnoOver the daily ceiling for that category.
CATEGORY_COUNT_LIMIT_EXCEEDEDnoAlready made the maximum number of purchases in that category today.
MOTO_NOT_DELEGABLEnoThe request carried the moto flag. Agents never use the telephone terminal. Remove the field.
Repeated writes are deduplicated. The same money-moving call from the same agent — same operation, same amount — repeated within 15 minutes returns the first answer again and does not move money a second time. You do not have to send an idempotency key: for agents one is derived, because an assistant asked to invent a key invents a new one on every attempt, which is worse than none.
An outcome you could not read is not an outcome you can assume. If a call fails while PayGlobe is reading the provider’s answer, the money may well have moved. That case returns 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.
Treat an agent as untrusted, because it can be talked into things. An assistant reads text written by other people — an order description, an email, a support ticket — and that text can try to instruct it. Permissions, ceilings and expiry are what keep a manipulated agent contained; they are enforced by PayGlobe on every call, but you choose them, so choose them tightly.
api

Dispute & Chargeback Management

#
What are Disputes? A dispute (chargeback) occurs when a customer contacts their bank to contest a charge. PayGlobe receives these automatically from PSPs and lets you respond with evidence.

How Disputes Work

1. Customer Contests
Customer contacts their bank to dispute a transaction
2. PSP Notifies PayGlobe
IGFS, Stripe, or PayPal sends webhook to our /webhook/psp/dispute endpoint
3. Dispute Appears in Dashboard
The dispute is visible in Merchant Dashboard β†’ Disputes tab (you don't create it manually)
4. Merchant Responds
Submit evidence (receipts, delivery proof, etc.) before the deadline
5. Resolution
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
Important Notes:
  • 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 true when less than 3 days remain
  • Secret Key required - All dispute endpoints require sk_test_* or sk_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"
    }
  }
}
api

Token Payments (Card on File)

#

Save customer cards for one-click payments and recurring billing. Supports both IGFS and Shift4 gateways.

Payment Modes: Configure your merchant account payment mode to enable tokenization:
  • 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)
Security Notes:
  • 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
migration

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.

IGFS is a trademark and product of N&TS Group. PayGlobe is not affiliated with N&TS Group and is not authorised to distribute IGFS integration documentation. This section is not, and does not replace, that documentation. It is addressed only to merchants who already run a live IGFS integration and hold the corresponding N&TS documentation, and it describes only what changes in their configuration. Protocol specifications remain with N&TS Group.
Status: Pre-production β€” deployed on the production infrastructure and exercised end-to-end by a real client that changed no code β€” including a Bizum payment, a method IGFS does not offer, returned to the client as an ordinary successful IGFS transaction.

What changes on your side

ValueBefore (real IGFS)After (adapter)
Base URLhttps://…netsgroup.com/MONEYNET_CG_SERVICEShttps://api.payglobe.it/MONEYNET_CG_SERVICES
merId (= signature keyId)assigned by N&TSissued by PayGlobe β€” max 16 chars per the spec
secret (HMAC)assigned by N&TSissued 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

EndpointStatus
POST /pgw/payment/initAvailable
GET /pgw/payment/verify (alias /verification)Available
POST /pgw/payment/captureAvailable
POST /pgw/payment/reversalAvailable
POST /pgw/payment/refundAvailable
GET /pgw/tokenizer/check, DELETE /pgw/tokenizer/deleteComing soon
/pgw/payment/selector, pay-by-mail, checkoutNot 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 Host header you send and the host value you sign must be the same string, and both must be api.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 keyId must equal the merId in the request body; a mismatch is rejected.
Known differences from direct IGFS.
  • The redirectURL points to the PayGlobe checkout instead of the Nets host β€” follow it as usual.
  • The order block (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.
  • paymentId is 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/verify is authoritative in IGFS regardless β€” poll it.

Full walkthrough, architecture and migration steps: igfs-adapter.html. For credentials, contact support@payglobe.it.

api

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.

Status: Live in production. Plans, customer enrollment via the public landing page, lifecycle actions, and the renewal/retry scheduler are all wired against IGFS. V1 caveat: the 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 with paymentMethod.source=CHARGE_NOW only; returns checkoutUrl for the IGFS-hosted card capture

The customer:

  1. Sees the plan summary (price, trial if any, fees if any)
  2. Fills email + name + any plan custom fields
  3. Enters card details (tokenized via PayGlobe.js / IGFS / Shift4 β€” same flow as one-shot payments)
  4. 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.created
  • subscription.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.resumed
  • subscription.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:

SourceHowUse 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" }
}
⚠ MIT compatibility: The token must be flagged as MIT-eligible by the issuing gateway. Tokens generated by some POS terminals are CIT-only (Customer Initiated) and will fail at renewal time. Verify with your acquirer before publishing imported-token subscriptions.

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

MethodPathPurpose
POST/api/subscriptions/plansCreate plan
GET/api/subscriptions/plansList 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/subscriptionsCreate subscription (server-side, with charge or zero-auth)
POST/api/subscriptions/import-tokenCreate subscription using a pre-existing card token
GET/api/subscriptionsList subscriptions (filters: status, plan, ref, customer)
GET/api/subscriptions/{id}Read subscription detail
POST/api/subscriptions/{id}/pausePause
POST/api/subscriptions/{id}/resumeResume
POST/api/subscriptions/{id}/cancelCancel (immediate or end-of-cycle)
POST/api/subscriptions/{id}/change-planSwitch to another plan (proration: PRORATE | FULL | NONE)
POST/api/subscriptions/{id}/update-cardReplace the stored token (source=EXISTING_TOKEN only in V1)
POST/api/subscriptions/{id}/retryManual retry of a PAST_DUE charge
GET/api/subscriptions/retry-policyRead merchant default retry policy
PUT/api/subscriptions/retry-policyUpdate 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/TRIALING sub whose nextBillingAt is in the past, charges it via IgfsService.initSaleWithToken (server-to-server, no 3DS, ~1 sec). On success: advances the period, increments cyclesCompleted, fires subscription.renewed. On failure: opens a SubscriptionInvoice, transitions to PAST_DUE, schedules the first retry per policy.
  • Retry cycle β€” for PAST_DUE subs whose nextRetryAt is due, replays the open invoice. Each attempt is recorded in subscription_retry_attempts. After policy.max failures, applies policy.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

Support

#
Documentation

OpenAPI YAML specification with the core payment API

Download OpenAPI YAML
Postman Collection

Ready-to-use API tests for all integration scenarios

Download Postman Collection
Technical Support

Contact: support@payglobe.com