API d'échange de Grant
Échangez un code grant contre un pass token. Ceci doit être fait côté serveur pour protéger votre partner secret.
Endpoint
POST https://api.zykay.com/v1/exchangeRequête
Headers
| Header | Requis | Description |
|---|---|---|
Content-Type | Oui | application/json |
X-Partner-ID | Oui | Votre Partner ID |
X-Partner-Timestamp | Oui | Timestamp Unix en secondes |
X-Partner-Nonce | Oui | UUID v4 unique par requête |
X-Partner-Signature | Oui | Signature HMAC-SHA256 (base64url) |
Body
{
"grant_code": "g_abc123def456..."
}| Champ | Type | Requis | Description |
|---|---|---|---|
grant_code | string | Oui | Code grant reçu du widget (commence par g_) |
Génération de la signature
La signature HMAC protège contre les attaques par rejeu et garantit l'intégrité de la requête.
const crypto = require('crypto');
function base64url(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
function signRequest(partnerId, partnerSecret, body) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const bodyString = JSON.stringify(body);
// 1. Hash du body (SHA256, base64url)
const bodyHash = base64url(
crypto.createHash('sha256').update(bodyString, 'utf8').digest()
);
// 2. Chaîne canonique
const canonicalString = `${bodyHash}.${timestamp}.${partnerId}.${nonce}`;
// 3. IMPORTANT: Décoder le secret depuis base64 avant HMAC
const secretBuffer = Buffer.from(partnerSecret, 'base64');
// 4. Signature HMAC-SHA256 (base64url)
const signature = base64url(
crypto.createHmac('sha256', secretBuffer)
.update(canonicalString)
.digest()
);
return {
headers: {
'Content-Type': 'application/json',
'X-Partner-ID': partnerId,
'X-Partner-Timestamp': timestamp,
'X-Partner-Nonce': nonce,
'X-Partner-Signature': signature
},
body: bodyString
};
}
// Exemple d'utilisation
const { headers, body } = signRequest(
'pk_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
'dGVzdF9zZWNyZXRfMzJfYnl0ZXNfbG9uZw==', // base64-encodé
{ grant_code: 'g_xyz789...' }
);
const response = await fetch('https://api.zykay.com/v1/exchange', {
method: 'POST',
headers,
body
});Réponse
Succès (200)
{
"pass_token": "p_xyz789abc123...",
"expires_in": 14400,
"token_type": "Bearer",
"age_over_18": true,
"scopes": ["isAdult", "isFrench", "revealNationality"],
"attributes": {
"age_over_18": true,
"is_french": true,
"nationality": "FRA"
}
}| Champ | Type | Description |
|---|---|---|
pass_token | string | Token à stocker pour vérification future (préfixe p_) |
expires_in | integer | Durée de validité en secondes (4 heures par défaut) |
token_type | string | Type de token (Bearer) |
age_over_18 | boolean | true si l'utilisateur a ≥ 18 ans |
scopes | string[] | Liste des scopes demandés et vérifiés |
attributes | object | Attributs de vérification (voir ci-dessous) |
Champs attributes :
| Champ | Type | Description |
|---|---|---|
age_over_18 | boolean | true si majeur |
is_male | boolean | true si sexe masculin (si scope isMale demandé) |
is_female | boolean | true si sexe féminin (si scope isFemale demandé) |
is_french | boolean | true si nationalité française (si scope isFrench demandé) |
is_eu | boolean | true si citoyen UE (si scope isEU demandé) |
nationality | string | Code ISO 3166-1 alpha-3 (ex: "FRA", si scope revealNationality demandé) |
birth_year | integer | Année de naissance (si scope revealBirthYear demandé) |
nullifier | string | Identifiant unique par app (si scope isUnique demandé) |
Erreur (400/401/403/500)
{
"error": "GRANT_INVALID",
"message": "Grant code is invalid or expired"
}| Champ | Type | Description |
|---|---|---|
error | string | Code d'erreur |
message | string | Message d'erreur lisible |
Codes d'erreur
| Code | Status HTTP | Description |
|---|---|---|
MISSING_HEADERS | 401 | Un ou plusieurs headers requis manquent |
INVALID_PARTNER | 403 | Partner ID non reconnu |
TIMESTAMP_SKEW | 401 | Le timestamp est décalé de plus de 5 minutes |
INVALID_SIGNATURE | 401 | La signature HMAC ne correspond pas |
REPLAY_DETECTED | 401 | Le nonce a déjà été utilisé |
INVALID_REQUEST | 400 | Body invalide ou grant_code manquant |
INVALID_GRANT | 400 | Format de grant invalide |
GRANT_INVALID | 401 | Grant code inexistant ou expiré |
INTERNAL_ERROR | 500 | Erreur serveur |
Notes importantes
Sécurité : Ne jamais exposer votre Partner Secret côté client. L'appel à cet endpoint doit toujours être fait depuis votre serveur.
Expiration : Les grant codes expirent après 5 minutes. Échangez-les immédiatement après réception.
Usage unique : Chaque grant code ne peut être échangé qu'une seule fois. Les tentatives répétées retourneront GRANT_INVALID.
Test de vérification HMAC
Utilisez ces valeurs de test pour valider votre implémentation avant d'intégrer en production. Comparez chaque étape avec les résultats attendus.
Important : Utilisez ces valeurs EXACTEMENT comme indiqué. Une différence d'un seul caractère produira une signature différente.
Valeurs de test
Partner ID: pk_test_example_123
Partner Secret: dGVzdF9zZWNyZXRfMzJfYnl0ZXNfbG9uZw==
Grant Code: g_test_verification_abc123
Timestamp: 1700000000
Nonce: 550e8400-e29b-41d4-a716-446655440000Résultats attendus à chaque étape
Étape 1 - Corps de la requête (UNIQUEMENT grant_code, PAS de partner_id) :
{"grant_code":"g_test_verification_abc123"}Étape 2 - Hash du corps (SHA256, base64url) :
bodyHash = "3lQmxO9DcoXgG0iPyG8wVCxpVbw6q-R-v9MOE0_kd98"Étape 3 - Chaîne canonique (format: bodyHash.timestamp.partnerId.nonce) :
3lQmxO9DcoXgG0iPyG8wVCxpVbw6q-R-v9MOE0_kd98.1700000000.pk_test_example_123.550e8400-e29b-41d4-a716-446655440000Étape 4 - Décodage du secret (base64 → bytes) :
Secret décodé = "test_secret_32_bytes_long" (25 bytes)Étape 5 - Signature finale (HMAC-SHA256, base64url) :
signature = "IiqKqzxLThjE4anzR2UGycy5JrJKSjjD2m3R81RxsW8"Script de validation Node.js
const crypto = require('crypto');
function base64url(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
// Valeurs de test
const grantCode = 'g_test_verification_abc123';
const partnerId = 'pk_test_example_123';
const partnerSecret = 'dGVzdF9zZWNyZXRfMzJfYnl0ZXNfbG9uZw==';
const timestamp = '1700000000';
const nonce = '550e8400-e29b-41d4-a716-446655440000';
// Étape 1: Corps (UNIQUEMENT grant_code!)
const body = JSON.stringify({ grant_code: grantCode });
console.log('1. Body:', body);
// Étape 2: Hash du corps
const bodyHash = base64url(crypto.createHash('sha256').update(body, 'utf8').digest());
console.log('2. Body hash:', bodyHash);
// Étape 3: Chaîne canonique
const canonical = `${bodyHash}.${timestamp}.${partnerId}.${nonce}`;
console.log('3. Canonical:', canonical);
// Étape 4: Décoder le secret (CRITIQUE!)
const secretBuffer = Buffer.from(partnerSecret, 'base64');
console.log('4. Secret decoded:', secretBuffer.toString(), `(${secretBuffer.length} bytes)`);
// Étape 5: Signature
const signature = base64url(crypto.createHmac('sha256', secretBuffer).update(canonical).digest());
console.log('5. Signature:', signature);Erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
INVALID_SIGNATURE avec body hash différent | partner_id inclus dans le body | Body = {"grant_code":"..."} UNIQUEMENT |
INVALID_SIGNATURE avec même body hash | Secret non décodé depuis base64 | Buffer.from(secret, 'base64') |
INVALID_SIGNATURE | Mauvais ordre dans canonical string | Ordre: bodyHash.timestamp.partnerId.nonce |
MISSING_HEADERS | Headers manquants ou mal nommés | Utilisez X-Partner-ID (pas partner_id) |
404 Not Found | Mauvais endpoint | URL: https://api.zykay.com/v1/exchange |
Piège fréquent : Le Partner Secret est encodé en base64. Vous DEVEZ le décoder avant de l'utiliser comme clé HMAC. crypto.createHmac('sha256', secret) est FAUX. Utilisez crypto.createHmac('sha256', Buffer.from(secret, 'base64')).