Documentation API Authentification

Authentification

Every API request must be authenticated with a signature. This page explains exactly how.

Overview

GerminoPay uses HMAC-SHA256 to sign each request. Unlike simple API keys (which can be stolen and reused), HMAC signs the request content with your secret, ensuring:

API Keys

Key Format Usage
Public Key pk_live_xxxxxxxxxxxx Sent in every request via the X-Public-Key header
Secret Key sk_live_xxxxxxxxxxxx Never sent. Used to compute the HMAC signature.
Never expose your secret key It must live only on your server (environment variable). Never put it in client-side JavaScript, mobile code, or Git repos.

Signature HMAC

To sign a request, concatenate these elements in this exact order:

  1. HTTP method in uppercase (GET, POST, etc.)
  2. Request path (without domain) — e.g. /v1/paiement/initier
  3. Raw request body (JSON, exactly as sent)
  4. Unix timestamp (seconds)

Then compute:

Pseudo-code
signature = HMAC_SHA256(
    key    = secret_key,
    message = METHOD + PATH + BODY + TIMESTAMP
)
Important The body must be exactly the same string that was sent. Even a single extra space changes the signature.

Required headers

Header Value Required
Content-Type application/json Yes
X-Public-Key Your public key Yes
X-Timestamp Unix timestamp (seconds) Yes
X-Signature Hex HMAC-SHA256 signature Yes

Examples by language

<?php
function germinoSign(string $method, string $path, string $body, string $secret): array
{
    $timestamp = time();
    $message = $method . $path . $body . $timestamp;
    $signature = hash_hmac("sha256", $message, $secret);

    return [
        "X-Timestamp" => $timestamp,
        "X-Signature" => $signature,
    ];
}

// Utilisation
$body = json_encode(["montant" => 5000, "devise" => "XOF"]);
$headers = germinoSign("POST", "/v1/paiement/initier", $body, getenv("GERMINOPAY_SECRET"));
const crypto = require('crypto');

function germinoSign(method, path, body, secret) {
  const timestamp = Math.floor(Date.now() / 1000);
  const message = `${method}${path}${body}${timestamp}`;
  const signature = crypto
    .createHmac('sha256', secret)
    .update(message)
    .digest('hex');

  return {
    'X-Timestamp': timestamp.toString(),
    'X-Signature': signature,
  };
}

// Usage
const body = JSON.stringify({ montant: 5000, devise: 'XOF' });
const headers = germinoSign('POST', '/v1/paiement/initier', body, process.env.GERMINOPAY_SECRET);
import hashlib
import hmac
import time

def germino_sign(method, path, body, secret):
    timestamp = int(time.time())
    message = f"{method}{path}{body}{timestamp}"
    signature = hmac.new(
        secret.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()

    return {
        "X-Timestamp": str(timestamp),
        "X-Signature": signature,
    }

# Usage
import json
body = json.dumps({"montant": 5000, "devise": "XOF"}, separators=(',', ':'))
headers = germino_sign("POST", "/v1/paiement/initier", body, os.environ["GERMINOPAY_SECRET"])
require 'openssl'
require 'time'

def germino_sign(method, path, body, secret)
  timestamp = Time.now.to_i.to_s
  message   = "#{method}#{path}#{body}#{timestamp}"
  signature = OpenSSL::HMAC.hexdigest('SHA256', secret, message)

  {
    'X-Timestamp' => timestamp,
    'X-Signature' => signature
  }
end

# Usage
body = '{"montant":5000,"devise":"XOF"}'
headers = germino_sign('POST', '/v1/paiement/initier', body, ENV['GERMINOPAY_SECRET'])
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

public class GerminoAuth {
    public static Map<String, String> sign(String method, String path, String body, String secret) throws Exception {
        String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
        String message = method + path + body + timestamp;

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] hash = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));

        StringBuilder hex = new StringBuilder();
        for (byte b : hash) {
            hex.append(String.format("%02x", b));
        }

        Map<String, String> headers = new HashMap<>();
        headers.put("X-Timestamp", timestamp);
        headers.put("X-Signature", hex.toString());
        return headers;
    }
}
using System;
using System.Security.Cryptography;
using System.Text;
using System.Collections.Generic;

public class GerminoAuth
{
    public static Dictionary<string, string> Sign(
        string method, string path, string body, string secret)
    {
        var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var message = $"{method}{path}{body}{timestamp}";

        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
        var signature = BitConverter.ToString(hash).Replace("-", "").ToLower();

        return new Dictionary<string, string>
        {
            { "X-Timestamp", timestamp },
            { "X-Signature", signature }
        };
    }
}

Anti-replay

Because the timestamp is part of the signature, an attacker who intercepts a request cannot reuse it later. Any request with a timestamp more than 5 minutes old is automatically rejected.

Clock synchronization Your server clock must be synchronized (NTP). A drift of more than 5 minutes will invalidate all requests.

Best practices