Skip to content

Accept card payments server-to-server

Overview

Server-to-Server (S2S) integration allows you to collect card details on your platform and process payments through Glomo's infrastructure. This integration is designed for PCI DSS certified merchants who want to maintain control over the payment experience while leveraging Glomo's cross-border payment processing capabilities.

Prerequisites

  • PCI DSS Certification: You must be PCI DSS certified to collect and transmit card details
  • Glomo Account: Active merchant account
  • API Keys: Secret keys for both sandbox (test) and production (live) environments

Environment Configuration

API Endpoint

All API requests use the same base URL for both environments:

https://api.glomopay.com

Authentication

The secret key in your Authorization header determines the environment:

  • Sandbox Key → Creates orders and payments in test mode
  • Production Key → Creates orders and payments in live mode
Authorization: Bearer YOUR_SECRET_KEY

Getting Your API Keys

  1. Navigate to: https://app.glomopay.com/api-keys-and-webhooks/api-keys
  2. Use the toggle to switch between Test Mode and Live Mode
  3. Reveal / Regenerate the API key

For more information on Secret Keys, please refer to Authentication


Integration Flow

Step 1: Before accepting payment, you need to create an order or a subscription in Glomo's system.

One Time Payment - Orders

To learn what an order is, refer to Orders.

For the full API reference, refer to Create Order API.

Subscriptions

With as_presented subscriptions, subsequent payments are merchant-initiated — no customer action is required. Card details are automatically carried over from the initial payment, so the card, callback_url, and method fields can be omitted.

To learn what a subscription is, refer to: Subscriptions.

For the full API reference, refer to: Create a Subscription API.


Step 2: Initiate Payment

Once you have the order_id/subscription_id generated from Step 1, submit card details to initiate payment.

Request Body:

curl -i -X POST \
  https://api.glomopay.com/api/v1/payment \
  -H 'Authorization: Bearer <YOUR_JWT_HERE>' \
  -H 'Content-Type: application/json' \
  -d '{
    "order_id": "order_68c00b7btsthf",
    "subscription_id": "sub_5JU9yv0lGSUP",
    "method": "card",
    "sequence": "initial",
    "card": {
      "holder_name": "John Doe",
      "number": "4111111111111111",
      "expiry_month": "09",
      "expiry_year": "30",
      "cvv": "123"
    },
    "callback_url": "https://server.yoursite.com/payment/callback",
    "notes": {
      "internal_ref": "ref_12345"
    }
  }'

Request Body Parameters:

ParameterTypeRequiredDescription
order_idstringYes*Order ID. Either order_id or subscription_id must be provided, but not both.
subscription_idstringYes*Subscription ID. Either order_id or subscription_id must be provided, but not both.
methodstringYes (initial)Payment method. Must be card. Not required for subsequent payments (defaults to card).
sequencestringNoPayment sequence: initial (default) or subsequent. Use subsequent for merchant-initiated payments on as_presented subscriptions.
cardobjectYes (initial)Card details (holder_name, number, expiry_month, expiry_year, cvv). Required for initial payments. Not required for subsequent payments — the card from the last successful payment is reused.
callback_urlstringYes (initial)HTTPS URL for redirect after payment. Required for initial payments. Not required for subsequent payments as there is no customer redirect involved.
request_idstringYes (subsequent)Idempotency key. Required for subsequent payments.
amountintegerYes (subsequent)Payment amount in minor units. Required for subsequent subscription payments. Cannot exceed the subscription's max_amount.
currencystringYes (subsequent)Currency code. Must match the subscription's currency for subsequent payments.
notesobjectNoAdditional key-value metadata.

Response:

{
  "payment_id": "payt_691eeb9aV79Uk",
  "status": "pending",
  "next_steps": [
    {
      "action": "redirect",
      "payload": {
        "url": "https://secure.glomopay.com?paymentId=payt_123&authToken=ey...&redirectUrl=encoded_url"
      }
    },
    {
      "action": "poll",
      "payload": {
        "url": "https://api.glomopay.com/api/v1/payment/payt_691eeb9aV79Uk",
        "interval_in_ms": 5000
      }
    }
  ]
}

Step 3: Handle Redirect

The response contains a redirect URL where the user needs to complete authentication (3DS, OTP, etc.).

MethodUse CaseImplementation
New TabWebwindow.open(redirectUrl, '_blank')
Same TabWebwindow.location.href = redirectUrl
WebViewMobile appsOpen URL in native WebView component

Step 4: Handle Callback

After payment processing, Glomo redirects the user back to your callback_url with query parameters.

Callback URL Format:

https://server.yoursite.com/payment/callback?order_id=order_XXX&payment_id=payt_XXX&status=success&signature=abc123

Query Parameters:

ParameterDescriptionValues
order_idThe order identifierorder_XXX
payment_idThe payment identifierpayt_XXX
statusPayment outcomesuccess, failed
signatureHMAC signature for verificationSHA-256 hash
errorSomething went wrong in the payment journeymessage

Step 5: Verify Signature

Always verify the signature on your server to ensure the callback is authentic. The callback URL should point to your server, where you can perform this verification. The necessary information for verification will be sent in the query parameters. The response of this API call should ideally be a redirect to your frontend.

The signature is the hex HMAC-SHA256 of order_id|payment_id|status. Compute it with each of your active secret keys, and accept the callback if any one of them matches. Your active secret keys are every secret key on your account that has not expired. Use a constant-time comparison.

The signature may be made with any one of your active secret keys, and it can differ between payments. Don't assume it is your newest key. If you hold more than one active key, checking only one of them can reject a genuine payment. When you revoke a key, or it expires, remove it from your list. A stale key left in the list opens no security hole, but keep the list matching the keys in your dashboard.

Each sample loads your active secret keys from a comma-separated GLOMO_SECRET_KEYS environment variable. It trims each key, drops empty entries, and stops with an error if no key is left. An empty key must never count as a match: anyone can compute the signature for an empty key, so it would let a forged callback through.

Sample Code:

JavaScript
const express = require('express');
const crypto = require('crypto');
const router = express.Router();

// Every active Glomo secret key, comma-separated (store in environment variables)
const GLOMO_SECRET_KEYS = (process.env.GLOMO_SECRET_KEYS || '')
  .split(',')
  .map((k) => k.trim())
  .filter(Boolean);
if (GLOMO_SECRET_KEYS.length === 0) throw new Error('GLOMO_SECRET_KEYS is not set');

/**
 * Check the signature against each active secret key
 */
function verifySignature(orderId, paymentId, status, signature, secretKeys) {
  const data = `${orderId}|${paymentId}|${status}`;
  const received = Buffer.from(signature || '');
  return secretKeys.some((secret) => {
    if (!secret) return false; // an empty key never matches
    const expected = Buffer.from(crypto.createHmac('sha256', secret).update(data).digest('hex'));
    return expected.length === received.length && crypto.timingSafeEqual(expected, received);
  });
}

/**
 * Glomo Payment Callback Handler
 */
router.get('/payment/callback', (req, res) => {
  // Extract query parameters
  const { order_id, payment_id, status, signature } = req.query;

  // Verify signature
  if (!verifySignature(order_id, payment_id, status, signature, GLOMO_SECRET_KEYS)) {
    console.error('Signature verification failed');
    return res.redirect(`/payment/error?message=Invalid signature`);
  }

  // Signature is valid - proceed based on payment status
  console.log('Payment verified successfully');

  // Redirect based on payment status
  if (status === 'success') {
    return res.redirect(`/payment/success?order_id=${order_id}&payment_id=${payment_id}`);
  } else if (status === 'failed') {
    return res.redirect(`/payment/failed?order_id=${order_id}`);
  } else {
    return res.redirect(`/payment/error?order_id=${order_id}`);
  }
});

module.exports = router;
PHP
<?php

/**
 * Check the signature against each active secret key
 */
function verifySignature($orderId, $paymentId, $status, $signature, array $secretKeys) {
    $data = "$orderId|$paymentId|$status";
    foreach ($secretKeys as $secret) {
        if ((string) $secret === '') {
            continue; // an empty key never matches
        }
        if (hash_equals(hash_hmac('sha256', $data, $secret), (string) $signature)) {
            return true;
        }
    }
    return false;
}

// Every active Glomo secret key, comma-separated, from your config
$glomoSecretKeys = array_values(array_filter(array_map('trim', explode(',', (string) getenv('GLOMO_SECRET_KEYS')))));
if (count($glomoSecretKeys) === 0) {
    throw new RuntimeException('GLOMO_SECRET_KEYS is not set');
}

// Verify signature
if (!verifySignature($orderId, $paymentId, $status, $signature, $glomoSecretKeys)) {
    // Signature verification failed
}
Ruby
require 'openssl'

# Check the signature against each active secret key
def verify_signature(order_id, payment_id, status, signature, secret_keys)
  data = "#{order_id}|#{payment_id}|#{status}"
  secret_keys.any? do |secret|
    next false if secret.to_s.empty? # an empty key never matches
    OpenSSL.secure_compare(OpenSSL::HMAC.hexdigest('SHA256', secret, data), signature.to_s)
  end
end

# Every active Glomo secret key, comma-separated, from your config
GLOMO_SECRET_KEYS = ENV.fetch('GLOMO_SECRET_KEYS', '').split(',').map(&:strip).reject(&:empty?)
raise 'GLOMO_SECRET_KEYS is not set' if GLOMO_SECRET_KEYS.empty?

# Verify signature
unless verify_signature(order_id, payment_id, status, signature, GLOMO_SECRET_KEYS)
  # Signature verification failed
end
Go
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "errors"
    "fmt"
    "log"
    "os"
    "strings"
)

// loadSecretKeys reads every active Glomo secret key from a comma-separated value
func loadSecretKeys(value string) ([]string, error) {
    var keys []string
    for _, key := range strings.Split(value, ",") {
        key = strings.TrimSpace(key)
        if key != "" {
            keys = append(keys, key)
        }
    }
    if len(keys) == 0 {
        return nil, errors.New("GLOMO_SECRET_KEYS is not set")
    }
    return keys, nil
}

// verifySignature checks the signature against each active secret key
func verifySignature(orderID, paymentID, status, signature string, secretKeys []string) bool {
    data := fmt.Sprintf("%s|%s|%s", orderID, paymentID, status)

    for _, secret := range secretKeys {
        if secret == "" {
            continue // an empty key never matches
        }
        h := hmac.New(sha256.New, []byte(secret))
        h.Write([]byte(data))
        expected := hex.EncodeToString(h.Sum(nil))

        if hmac.Equal([]byte(expected), []byte(signature)) {
            return true
        }
    }
    return false
}

// Every active Glomo secret key, comma-separated, from your config
glomoSecretKeys, err := loadSecretKeys(os.Getenv("GLOMO_SECRET_KEYS"))
if err != nil {
    log.Fatal(err)
}

// Verify signature
if !verifySignature(orderID, paymentID, status, signature, glomoSecretKeys) {
    // Signature verification failed
}
Java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

// Every active Glomo secret key, comma-separated, from your config
private static final List<String> GLOMO_SECRET_KEYS = loadSecretKeys(System.getenv("GLOMO_SECRET_KEYS"));

public static List<String> loadSecretKeys(String value) {
    List<String> keys = new ArrayList<>();
    if (value != null) {
        for (String key : value.split(",")) {
            if (!key.trim().isEmpty()) keys.add(key.trim());
        }
    }
    if (keys.isEmpty()) throw new IllegalStateException("GLOMO_SECRET_KEYS is not set");
    return keys;
}

// Call as verifySignature(orderId, paymentId, status, signature, GLOMO_SECRET_KEYS)
public static boolean verifySignature(String orderId, String paymentId, String status,
                                      String signature, List<String> secretKeys) {
    if (signature == null) return false;
    try {
        byte[] data = (orderId + "|" + paymentId + "|" + status).getBytes(StandardCharsets.UTF_8);
        byte[] received = signature.getBytes(StandardCharsets.UTF_8);

        for (String secret : secretKeys) {
            if (secret == null || secret.isEmpty()) continue; // an empty key never matches

            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));

            StringBuilder hex = new StringBuilder();
            for (byte b : mac.doFinal(data)) hex.append(String.format("%02x", b));

            if (MessageDigest.isEqual(hex.toString().getBytes(StandardCharsets.UTF_8), received)) {
                return true;
            }
        }
        return false;
    } catch (Exception e) {
        return false;
    }
}

Step 6: Handle Webhooks

Webhooks deliver server-to-server notifications about payment events directly to your backend.

For complete webhook implementation details: Webhooks Documentation


Testing

Test Cards

Use these cards in sandbox mode to simulate different scenarios: Test Card Details


For technical support, contact your integration manager or reach out via the merchant dashboard.