Exemple de demande et vérification de signature
Méthode de requête : les API utilisent principalement `GET` / `POST`; les requêtes JSON utilisent `application/json`.
Authentification : les requêtes OpenAPI sont vérifiées avec des en-têtes et des signatures HMAC-SHA256.
En-têtes
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| x-api-key | string | Oui | Marchand API Key. Utilisez la valeur commençant par mch_ de la page des paramètres API du marchand. |
| x-merchant-uid | string | Oui | Merchant UID. |
| x-timestamp | string | Oui | Milliseconde à 13 chiffres timestamp. La dérive d'horloge autorisée par défaut est de 5 minutes. |
| x-nonce | string | Oui | Chaîne aléatoire unique pour chaque requête, jusqu'à 128 caractères. La réutilisation est rejetée. |
| x-signature | string | Oui | Signature hexadécimale générée avec HMAC-SHA256. La charge utile contient Merchant UID, timestamp, nonce, la méthode, le chemin, le query canonique et le body canonique. |
Règles de signature
- Les requêtes utilisent application/json. Pour les GET API sans body, la partie body dans la signature est une chaîne vide.
- Le chemin de signature doit être le chemin brut API, par exemple /openapi/payin/orders, sans le domaine.
- Les paramètres de requête sont triés par clé ; Les clés JSON body sont triées avant la sérialisation.
- Les échecs de signature courants incluent des timestamp expirés, des valeurs nonce répétées, un ordre de champ incohérent ou des espaces supplémentaires.
Le timestamp, le nonce et les identifiants fixes sont des valeurs de test de signature hors ligne. Pour une requête réelle, utilisez le timestamp actuel en millisecondes et un nouveau nonce aléatoire. Les chemins API n’ont pas de préfixe de version ; v1 désigne uniquement la signature des callbacks.
Exemple de payload de signature
880001
1776193200000
2f5c7b147c3748f0a8b3d9bb38aa91a4
POST
/openapi/payin/orders
{"amount":"100.00","chainCode":"TRON","merchantOrderNo":"M202604150001","notifyUrl":"https://merchant.example.com/api/uugate/payin-notify","tokenSymbol":"USDT"}Exemple complet
POST /openapi/payin/orders HTTP/1.1
Host: api.uugate.com
Content-Type: application/json
x-api-key: mch_xxxxxxxxxxxxxxxxxxxx
x-merchant-uid: 880001
x-timestamp: 1776193200000
x-nonce: 2f5c7b147c3748f0a8b3d9bb38aa91a4
x-signature: 6d7e96fdbf3ec37c1ec515540f46c6c19b08754973c43293e54644c2da910838
{
"chainCode": "TRON",
"tokenSymbol": "USDT",
"merchantOrderNo": "M202604150001",
"amount": "100.00",
"notifyUrl": "https://merchant.example.com/api/uugate/payin-notify"
}Exemples de signature
- Les quatre langages utilisent les mêmes chemins, paramètres et réponses. Les identifiants proviennent de UUGATE_MERCHANT_UID et UUGATE_API_KEY ; UUGATE_BASE_URL vaut https://api.uugate.com par défaut.
- canonicalBody est un JSON compact aux clés triées. Après modification, triez les objets récursivement, conservez l’ordre des tableaux et utilisez UTF-8 sans échappement supplémentaire des barres obliques ou Unicode. Les montants sont des chaînes, les booléens true / false.
- Une réponse réussie renvoie directement l’objet métier, sans enveloppe code / data. Vérifiez d’abord le statut HTTP. Les erreurs métier renvoient code, message, data et requestId ; les refus de sécurité peuvent se limiter à HTTP 401 / 403 avec un corps vide.
- Après un timeout ou HTTP 5xx, consultez la commande avant de répéter sa création ou son envoi. Générez un nouveau timestamp, nonce et une nouvelle signature pour chaque requête.
Exécution : enregistrez Java sous UUGateExample.java (UUGateCallback.java pour les callbacks), puis utilisez java nom du fichier ; PHP : php example.php ; Go : go run example.go ; Node.js : node example.mjs. Les vérificateurs de callback lisent le corps brut sur l’entrée standard.
import { createHmac, randomBytes } from 'node:crypto';
function required(name) {
const value = process.env[name]?.trim();
if (!value) throw new Error(name + ' is required');
return value;
}
const uid = "880001", key = "mch_xxxxxxxxxxxxxxxxxxxx";
const timestamp = "1776193200000", nonce = "2f5c7b147c3748f0a8b3d9bb38aa91a4";
const method = 'POST', path = '/openapi/payin/orders';
const canonicalQuery = '', canonicalBody = "{\"amount\":\"100.00\",\"chainCode\":\"TRON\",\"merchantOrderNo\":\"M202604150001\",\"notifyUrl\":\"https://merchant.example.com/api/uugate/payin-notify\",\"tokenSymbol\":\"USDT\"}";
const payload = [uid, timestamp, nonce, method, path, canonicalQuery, canonicalBody].join('\n');
const signature = createHmac('sha256', key).update(payload, 'utf8').digest('hex');
console.log(signature);import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class UUGateExample {
static String required(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) throw new IllegalArgumentException(name + " is required");
return value.trim();
}
public static void main(String[] args) throws Exception {
String uid = "880001", key = "mch_xxxxxxxxxxxxxxxxxxxx";
String timestamp = "1776193200000", nonce = "2f5c7b147c3748f0a8b3d9bb38aa91a4";
String method = "POST", path = "/openapi/payin/orders";
String canonicalQuery = "", canonicalBody = "{\"amount\":\"100.00\",\"chainCode\":\"TRON\",\"merchantOrderNo\":\"M202604150001\",\"notifyUrl\":\"https://merchant.example.com/api/uugate/payin-notify\",\"tokenSymbol\":\"USDT\"}";
String payload = String.join("\n", uid, timestamp, nonce, method, path, canonicalQuery, canonicalBody);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String signature = HexFormat.of().formatHex(mac.doFinal(payload.getBytes(StandardCharsets.UTF_8)));
System.out.println(signature);
}
}<?php
function required(string $name): string {
$value = trim((string) getenv($name));
if ($value === '') throw new RuntimeException($name . ' is required');
return $value;
}
$uid = "880001"; $key = "mch_xxxxxxxxxxxxxxxxxxxx";
$timestamp = "1776193200000"; $nonce = "2f5c7b147c3748f0a8b3d9bb38aa91a4";
$method = 'POST'; $path = '/openapi/payin/orders';
$canonicalQuery = ''; $canonicalBody = '{"amount":"100.00","chainCode":"TRON","merchantOrderNo":"M202604150001","notifyUrl":"https://merchant.example.com/api/uugate/payin-notify","tokenSymbol":"USDT"}';
$payload = implode("\n", [$uid, $timestamp, $nonce, $method, $path, $canonicalQuery, $canonicalBody]);
$signature = hash_hmac('sha256', $payload, $key);
echo $signature, PHP_EOL;package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"strings"
)
func required(name string) string {
value := strings.TrimSpace(os.Getenv(name))
if value == "" { panic(name + " is required") }
return value
}
func main() {
uid, key := "880001", "mch_xxxxxxxxxxxxxxxxxxxx"
timestamp, nonce := "1776193200000", "2f5c7b147c3748f0a8b3d9bb38aa91a4"
method, path := "POST", "/openapi/payin/orders"
canonicalQuery, canonicalBody := "", "{\"amount\":\"100.00\",\"chainCode\":\"TRON\",\"merchantOrderNo\":\"M202604150001\",\"notifyUrl\":\"https://merchant.example.com/api/uugate/payin-notify\",\"tokenSymbol\":\"USDT\"}"
payload := strings.Join([]string{uid, timestamp, nonce, method, path, canonicalQuery, canonicalBody}, "\n")
mac := hmac.New(sha256.New, []byte(key)); mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
fmt.Println(signature)
}Les erreurs métier renvoient un JSON avec code, message, data et requestId. Une clé, une signature, un timestamp ou un nonce invalide, ou une restriction de sécurité, peut renvoyer HTTP 401 / 403 avec un corps vide sans code métier. Vérifiez le statut HTTP et le corps avant de lire le JSON. Ne fondez pas vos décisions sur le texte traduit de message.
Champs d’erreur
| Champ | Type | Obligatoire | Description |
|---|---|---|---|
| code | number | Oui | Code d'erreur métier. |
| message | string | Oui | Message d'erreur. |
| data | object | null | Oui | Données d'erreur supplémentaires. |
| requestId | string | null | Oui | ID de trace de demande du serveur. |
Exemple d’erreur
HTTP/1.1 401 Unauthorized
Cache-Control: no-store
Content-Length: 0