API Reference
Grant Exchange API

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')).