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.
| Scope | Datos en user |
|---|---|
| (siempre) | uuid: identificador estable del usuario en QvaPay. |
profile | username, name, lastname, avatar (URL o null). |
kyc | kyc (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:
| Campo | Descripción |
|---|---|
auth_date | Fecha de la firma (segundos Unix). |
nonce | 32 caracteres hex aleatorios. Guárdalos si quieres impedir reusos. |
app_uuid | UUID de tu App. Compruébalo. |
mini_app | Slug de tu mini-app. |
scopes | Scopes concedidos, ordenados y separados por coma. |
user | JSON con los datos del usuario. |
hash | Firma HMAC-SHA256 en hexadecimal. |
Verificar la firma
- Separa
hashdel resto de campos. - data_check_string: el resto de campos ordenados alfabéticamente por clave, como
clave=valor(valores ya decodificados), unidos por\n. - secret_key =
HMAC_SHA256(clave = "QvaPayMiniApp", mensaje = tu app-secret)(bytes crudos). - Calcula
hex(HMAC_SHA256(clave = secret_key, mensaje = data_check_string))y compáralo conhashen tiempo constante. - Rechaza firmas viejas (
auth_date), y comprueba queapp_uuides 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.