QvaPay API
Mini-apps

Identidad del usuario

Pide los datos del usuario con su consentimiento y verifica la firma en tu servidor.

Pedir acceso

const { init_data, hash, scopes } = await QvaPay.auth.requestLogin({ scopes: ['profile', 'kyc'] })
// Envía init_data TAL CUAL a tu servidor
await fetch('/api/login-qvapay', { method: 'POST', body: init_data })

El usuario ve qué datos se comparten. Solo puedes pedir los scopes que QvaPay autorizó para tu mini-app. Si ya los concedió antes, la app puede no volver a preguntar.

ScopeDatos en user
(siempre)uuid: identificador estable del usuario en QvaPay.
profileusername, name, lastname, avatar (URL o null).
kyckyc (identidad verificada, booleano), gold (suscripción GOLD, booleano).

Nunca se comparten el correo, el teléfono ni el saldo.

Formato de init_data

Es una query string (application/x-www-form-urlencoded) con estos campos:

CampoDescripción
auth_dateFecha de la firma (segundos Unix).
nonce32 caracteres hex aleatorios. Guárdalos si quieres impedir reusos.
app_uuidUUID de tu App. Compruébalo.
mini_appSlug de tu mini-app.
scopesScopes concedidos, ordenados y separados por coma.
userJSON con los datos del usuario.
hashFirma HMAC-SHA256 en hexadecimal.

Verificar la firma

  1. Separa hash del resto de campos.
  2. data_check_string: el resto de campos ordenados alfabéticamente por clave, como clave=valor (valores ya decodificados), unidos por \n.
  3. secret_key = HMAC_SHA256(clave = "QvaPayMiniApp", mensaje = tu app-secret) (bytes crudos).
  4. Calcula hex(HMAC_SHA256(clave = secret_key, mensaje = data_check_string)) y compáralo con hash en tiempo constante.
  5. Rechaza firmas viejas (auth_date), y comprueba que app_uuid es el tuyo.

Node.js

import crypto from 'node:crypto'

export function verifyQvaPayInitData(initData, appSecret, appUuid, maxAgeSeconds = 86400) {
  const params = new URLSearchParams(initData)
  const hash = params.get('hash') || ''
  params.delete('hash')

  const dataCheckString = [...params.entries()]
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
    .map(([k, v]) => `${k}=${v}`)
    .join('\n')

  const secretKey = crypto.createHmac('sha256', 'QvaPayMiniApp').update(appSecret).digest()
  const expected = crypto.createHmac('sha256', secretKey).update(dataCheckString).digest('hex')

  const a = Buffer.from(expected, 'hex')
  const b = Buffer.from(hash, 'hex')
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) throw new Error('Firma inválida')

  const authDate = Number(params.get('auth_date'))
  if (Date.now() / 1000 - authDate > maxAgeSeconds) throw new Error('init_data caducado')
  if (params.get('app_uuid') !== appUuid) throw new Error('init_data de otra App')

  return {
    user: JSON.parse(params.get('user')),
    scopes: (params.get('scopes') || '').split(',').filter(Boolean),
    nonce: params.get('nonce'),
  }
}

Una vez verificado, crea tu propia sesión para ese user.uuid. No vuelvas a pedir requestLogin en cada petición.

El usuario puede revocar el acceso desde Ajustes de la app. Una firma ya emitida sigue siendo matemáticamente válida hasta que caduque: usa una ventana auth_date corta si eso te importa.