Backend (Exchange API)

Exchange the grant_code for an pass_token and the verification results.

Recommended feed

  1. The frontend retrieves #grant_code=... from data-success-path
  2. The frontend sends grant_code to your backend (POST /api/zykay/exchange)
  3. Your backend calls POST https://api.zykay.com/v1/exchange
  4. Your backend creates the application session (cookie/JWT)
⚠️

Never make the api.zykay.com/v1/exchange call from the browser: the Partner Secret must remain server side.

Partner API request

  • URL: https://api.zykay.com/v1/exchange
  • Method: POST
  • Body: {"grant_code":"g_xxx"}
  • Required headers:
    • X-Partner-ID
    • X-Partner-Timestamp (Unix seconds)
    • X-Partner-Nonce (UUID v4)
    • X-Partner-Signature

HMAC signature (canonical)

bodyHash   = base64url(SHA256(rawBody))
canonical  = `${bodyHash}.${timestamp}.${partnerId}.${nonce}`
signature  = base64url(HMAC-SHA256(canonical, base64_decode(partnerSecret)))

Node.js example

import crypto from 'crypto';
 
function base64url(buffer) {
  return buffer.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
 
export async function exchangeGrantCode(grantCode) {
  const partnerId = process.env.ZYKAY_PARTNER_ID;
  const partnerSecret = process.env.ZYKAY_PARTNER_SECRET;
 
  const body = JSON.stringify({ grant_code: grantCode });
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const nonce = crypto.randomUUID();
 
  const bodyHash = base64url(crypto.createHash('sha256').update(body, 'utf8').digest());
  const canonical = `${bodyHash}.${timestamp}.${partnerId}.${nonce}`;
  const secret = Buffer.from(partnerSecret, 'base64');
  const signature = base64url(crypto.createHmac('sha256', secret).update(canonical).digest());
 
  const response = await fetch('https://api.zykay.com/v1/exchange', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Partner-ID': partnerId,
      'X-Partner-Timestamp': timestamp,
      'X-Partner-Nonce': nonce,
      'X-Partner-Signature': signature,
    },
    body,
  });
 
  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.error || `Exchange failed (${response.status})`);
  }
 
  return response.json();
}

Reading the hash on the frontend side

const hash = new URLSearchParams(window.location.hash.slice(1));
const grantCode = hash.get('grant_code');
if (grantCode) {
  await fetch('/api/zykay/exchange', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ grant_code: grantCode }),
  });
}

Typical answer

{
  "pass_token": "p_abc123...",
  "expires_in": 14400,
  "token_type": "Bearer",
  "attributes": {
    "age_over_18": true
  }
}