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/exchange

Requête

Headers

HeaderRequisDescription
Content-TypeOuiapplication/json
X-Partner-IDOuiVotre Partner ID
X-Partner-TimestampOuiTimestamp Unix en secondes
X-Partner-NonceOuiUUID v4 unique par requête
X-Partner-SignatureOuiSignature HMAC-SHA256 (base64url)

Body

{
  "grant_code": "g_abc123def456..."
}
ChampTypeRequisDescription
grant_codestringOuiCode 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"
  }
}
ChampTypeDescription
pass_tokenstringToken à stocker pour vérification future (préfixe p_)
expires_inintegerDurée de validité en secondes (4 heures par défaut)
token_typestringType de token (Bearer)
age_over_18booleantrue si l'utilisateur a ≥ 18 ans
scopesstring[]Liste des scopes demandés et vérifiés
attributesobjectAttributs de vérification (voir ci-dessous)

Champs attributes :

ChampTypeDescription
age_over_18booleantrue si majeur
is_malebooleantrue si sexe masculin (si scope isMale demandé)
is_femalebooleantrue si sexe féminin (si scope isFemale demandé)
is_frenchbooleantrue si nationalité française (si scope isFrench demandé)
is_eubooleantrue si citoyen UE (si scope isEU demandé)
nationalitystringCode ISO 3166-1 alpha-3 (ex: "FRA", si scope revealNationality demandé)
birth_yearintegerAnnée de naissance (si scope revealBirthYear demandé)
nullifierstringIdentifiant unique par app (si scope isUnique demandé)

Erreur (400/401/403/500)

{
  "error": "GRANT_INVALID",
  "message": "Grant code is invalid or expired"
}
ChampTypeDescription
errorstringCode d'erreur
messagestringMessage d'erreur lisible

Codes d'erreur

CodeStatus HTTPDescription
MISSING_HEADERS401Un ou plusieurs headers requis manquent
INVALID_PARTNER403Partner ID non reconnu
TIMESTAMP_SKEW401Le timestamp est décalé de plus de 5 minutes
INVALID_SIGNATURE401La signature HMAC ne correspond pas
REPLAY_DETECTED401Le nonce a déjà été utilisé
INVALID_REQUEST400Body invalide ou grant_code manquant
INVALID_GRANT400Format de grant invalide
GRANT_INVALID401Grant code inexistant ou expiré
INTERNAL_ERROR500Erreur 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-446655440000

Ré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

ErreurCauseSolution
INVALID_SIGNATURE avec body hash différentpartner_id inclus dans le bodyBody = {"grant_code":"..."} UNIQUEMENT
INVALID_SIGNATURE avec même body hashSecret non décodé depuis base64Buffer.from(secret, 'base64')
INVALID_SIGNATUREMauvais ordre dans canonical stringOrdre: bodyHash.timestamp.partnerId.nonce
MISSING_HEADERSHeaders manquants ou mal nommésUtilisez X-Partner-ID (pas partner_id)
404 Not FoundMauvais endpointURL: 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')).