Ayuda

Conectar tu sistema con Twarz: API y webhooks

Tu sistema crea solicitudes con una clave API y se entera del resultado por un webhook: Twarz le hace un POST firmado a tu URL cuando la persona aprueba, rechaza o deja vencer la solicitud. Después lee los datos compartidos con la misma clave.

Las claves y los webhooks los configura un supervisor en la consola, en Acceso API.

1. Crear una clave API

  1. En la consola, entrá a Acceso API → Claves API → Nueva clave.
  2. Elegí solo los permisos que tu sistema necesita. Para el circuito completo:

    • Crear solicitudes y Leer solicitudes;
    • Leer plantillas, para elegir con qué plantilla pedir;
    • Leer datos compartidos, para leer lo que la persona compartió.
  3. Copiá la clave: se muestra una sola vez. Guardala como un secreto, nunca en el código del navegador ni de una app.

Tu sistema la manda en el header X-Api-Key en cada llamada. La API está en https://api.twarz.uy/api, y su referencia en api.twarz.uy/api/docs: lista justo las operaciones que una clave puede llamar, y podés probarlas desde ahí pegando tu clave en Authentication.

2. Crear una solicitud

Toda solicitud sale de una plantilla de tu empresa: la plantilla define qué datos se piden y con qué nivel de autenticación. Listá las tuyas con GET /v1/coreo/templates y usá su id.

POST https://api.twarz.uy/api/v1/coreo/petitions
X-Api-Key: <tu clave>
Content-Type: application/json

{
  "targetUsername": "twid.ury.12345678",
  "templateId": "<id de la plantilla>",
  "providerName": "Nombre que ve la persona",
  "message": "Opcional: un mensaje para la persona"
}

targetUsername es twid.<país>.<número de documento>, sin puntos ni guiones (ury o arg). La persona tiene que estar registrada en Twarz. La respuesta trae el petitionId; guardalo para cruzarlo con el webhook.

3. Configurar el webhook

  1. En Acceso API → Webhooks → Nueva suscripción, poné una etiqueta, la URL de tu sistema (tiene que ser HTTPS) y los eventos que querés recibir.

  2. Copiá el secreto de firma: también se muestra una sola vez. Con él verificás que cada entrega viene de Twarz.

  3. La suscripción queda Pendiente. Tocá Verificar: Twarz le manda a tu URL un evento endpoint.verification y tu sistema tiene que contestar 2xx en menos de 10 segundos devolviendo el mismo challenge que recibe en data.challenge, como {"challenge":"…"} o el valor solo. No se siguen redirecciones. Cuando contesta bien, la suscripción pasa a Activa y empieza a recibir entregas.

4. Qué llega en cada entrega

Un POST a tu URL con un cuerpo JSON:

{
  "id": "0199f2a1-7c3e-7a10-9b2f-3c4d5e6f7a8b",
  "type": "petition.approved",
  "created_at": "2026-10-16T14:05:09+00:00",
  "data": {
    "petitionId": "01a13de4-ce29-7911-82f8-13862de445b5",
    "grantedCategories": ["B", "B+", "D"]
  },
  "metadata": {}
}
Evento Cuándo data
petition.approved La persona aprobó la solicitud petitionId, grantedCategories (lo que compartió)
petition.rejected La persona la rechazó petitionId, grantedCategories: []
petition.timeout Venció sin respuesta petitionId, grantedCategories: []
grant.revoked La persona dejó de compartir un dato grantId, categoryCode, petitionId

El webhook nunca trae los datos de la persona, ni el motivo de un rechazo: solo qué pasó y qué quedó compartido. created_at es cuándo ocurrió el evento, no cuándo se envió.

Headers de cada entrega:

Header Qué es
X-Twarz-Event El tipo de evento, igual que type
X-Twarz-Delivery-Id El id del evento, igual que id. Es el mismo en cada reintento
X-Twarz-Timestamp Segundos Unix del momento del envío. Cambia en cada reintento
X-Twarz-Signature v1= seguido de la firma
X-Twarz-Payload-Version La versión del formato: v1

5. Verificar la firma

  1. Armá el texto {X-Twarz-Timestamp}.{cuerpo}: el timestamp, un punto y el cuerpo exactamente como llegó, sin volver a serializarlo.

  2. Calculá HMAC-SHA256 de ese texto usando el secreto de firma como texto, tal cual lo copiaste (no lo decodifiques).

  3. Pasalo a hexadecimal en minúsculas, anteponé v1= y comparalo con X-Twarz-Signature en tiempo constante.

  4. Rechazá la entrega si el timestamp difiere más de 5 minutos de tu reloj.

Un receptor mínimo en Node.js:

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.TWARZ_WEBHOOK_SECRET; // el secreto, tal cual lo copiaste

// El cuerpo crudo: la firma se calcula sobre los bytes exactos que llegaron.
app.post('/webhooks/twarz', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-Twarz-Timestamp');
  const signature = req.get('X-Twarz-Signature') ?? '';
  const body = req.body.toString('utf8');

  // Más de 5 minutos de diferencia: una entrega vieja reutilizada.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(401);

  const expected = 'v1=' + crypto
    .createHmac('sha256', SECRET)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  // Comparación en tiempo constante, nunca con ===.
  const valid = signature.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!valid) return res.sendStatus(401);

  const event = JSON.parse(body);

  // La verificación del endpoint: devolvé el challenge que llega.
  if (event.type === 'endpoint.verification') {
    return res.json({ challenge: event.data.challenge });
  }

  // Respondé enseguida y procesá aparte. Descartá un event.id que ya procesaste.
  res.sendStatus(204);
});

6. Qué tenés que responder

7. Duplicados y orden

Una entrega puede llegar más de una vez. Guardá los X-Twarz-Delivery-Id que ya procesaste y descartá los repetidos. No hay orden garantizado: un reintento puede llegar después de un evento más nuevo, así que usá created_at para ordenar.

8. Leer los datos compartidos

  1. Leé la solicitud con tu clave:

    GET https://api.twarz.uy/api/v1/coreo/petitions/{petitionId}/provider
    X-Api-Key: <tu clave>

    Si fue aprobada, sharedData trae un grantToken (vale 2 minutos) y los resources que abre.

  2. Leé los datos con la misma clave y ese token:

    GET https://api.twarz.uy/api/v1/biometric/records/shared?include=biographic,document
    X-Api-Key: <tu clave>
    X-Grant-Token: <grantToken>

    include lleva los resources que querés, separados por coma. Si el token venció, volvé al paso 1 para obtener uno nuevo. Cada lectura queda registrada.

Si la persona revocó un dato (grant.revoked), los tokens nuevos ya no lo incluyen. Uno que ya tenías sigue valiendo hasta que vence, a los 2 minutos.

Referencia completa

Todas las operaciones que una clave puede llamar, con sus campos y respuestas, están en api.twarz.uy/api/docs. El mismo contenido como documento OpenAPI, para generar un cliente: Integration v1.

¿Nada de esto funcionó?

Pedile ayuda a un supervisor de tu empresa. Si el supervisor sos vos, escribinos a soporte@wearedor.com con el id de la solicitud o de la suscripción y la hora aproximada de lo que pasó.