---
title: "Callback de aprobación"
description: "Aprueba o rechaza cada operación del cosigner desde tu propio servidor mediante JWT firmados con RS256."
url: https://docs.fystack.io/es/cosigner/approval-callback
last_updated: 2026-11-13
---

> ## Documentation index
> Fetch the complete documentation index at: https://docs.fystack.io/llms.txt
> Use it to discover all available pages before exploring further.

# Callback de aprobación

Antes de unirse a cualquier sesión, el cosigner envía una solicitud firmada a tu servidor y espera un `approve` firmado. Tu servidor recibe los bytes exactos que se van a firmar, por lo que puede verificar la transacción de forma independiente de Fystack.

_Diagram: El cosigner envía una solicitud firmada al callback, que devuelve approve o reject_

## Cómo funciona

1. El cosigner verifica el `session_start` del orquestador.
2. El cosigner hace `POST` de un JWT RS256 a tu URL (`Content-Type: application/jwt`).
3. Tu servidor lo verifica, ejecuta su política y devuelve `200` con un JWT RS256.
4. El cosigner verifica la respuesta y el `request_id`, y luego se une a la sesión.

Se envían solicitudes para `KEYGEN`, `SIGN` y `RESHARE`. Aplica la política de transacciones a `SIGN`.

> **Note:** El cosigner falla de forma cerrada. Un rechazo, una respuesta distinta de 200, una redirección, un timeout, un cuerpo demasiado grande, una firma incorrecta o un `request_id` equivocado significan que no hay firma. La sesión falla con `CALLBACK_REJECTED`.

## Configuración

Dos pares de claves RSA-4096. Solo se intercambian las claves públicas.

- **Clave del cosigner**: la crea `cosigner-cli` y firma las solicitudes. Entrega `cosigner-public.pem` a tu servidor.
- **Clave del servidor**: se crea en tu servidor o en KMS y firma las respuestas. Entrega `server-public.pem` al cosigner.

**Mismo host:** apunta a la carpeta de claves del servidor. La CLI lee `server-public.pem` y escribe `cosigner-public.pem`. La URL predeterminada es `http://host.docker.internal:8089/cosigner/approve`.

```bash
cosigner-cli callback add ./node-callback-server/keys
```

**Servidor remoto:**

```bash
cosigner-cli callback add https://risk.example.com/cosigner/approve \
  --server-public-key ./server-public.pem
```

La CLI muestra la ruta y la huella digital de `cosigner-public.pem`; instálalo en tu servidor. Agrega `--ca ./ca.pem` si usas una CA privada.

`callback add` actualiza `config.yaml`, establece `require_callback: true` y reinicia el cosigner (omítelo con `--no-restart`). `cosigner-cli info` muestra la URL y la huella digital actuales.

```yaml
approval:
  require_callback: true
callback:
  url: https://risk.example.com/cosigner/approve
  timeout: 30s
  retry_count: 1
  private_key:
    type: file
    config:
      path: /run/secrets/cosigner-private.der.hex
  response_public_key_path: /run/secrets/server-public.pem
  # ca_cert_path: /run/secrets/callback-ca.pem
```

- `approval.require_callback` (boolean): Rechaza todas las sesiones si no hay callback. Mantenlo en `true` en producción.
- `callback.url` (string): Solo HTTPS. Se permite HTTP sin cifrar para `host.docker.internal`. Si no hay ruta, se agrega `/cosigner/approve`.
- `callback.timeout` (duration): Tiempo total disponible, incluido el reintento. También es la expiración del JWT de la solicitud. Predeterminado: `30s`.
- `callback.retry_count` (integer): Reintentos ante errores de red o `5xx`, con el mismo token. Máximo y predeterminado: `1`.
- `callback.private_key` (secret provider): Clave de firma RSA-4096 del cosigner, en hex PKCS#8 DER.
- `callback.response_public_key_path` (string): Clave pública RSA-4096 de tu servidor (PEM).
- `callback.ca_cert_path` (string): Certificado de CA para un certificado de servidor privado o autofirmado.

## Solicitud

Header JWT `{"alg": "RS256", "typ": "JWT"}`. Acepta solo `RS256` y verifica contra `cosigner-public.pem` antes de leer los claims.

- `request_id` (string (UUID)): Único por solicitud, también es el `jti`. Devuélvelo en la respuesta. Úsalo como clave de idempotencia.
- `iat, exp` (number): `exp` = `iat` + timeout.
- `cosigner_id, workspace_id, participant_id` (string): Identidad del cosigner.
- `session_id, orchestrator_id` (string): La sesión y el orquestador que la inició.
- `coordinator_sequence, coordinator_signature` (number, string): La firma del orquestador ya verificada. Regístrala para la trazabilidad.
- `protocol` (string): `ECDSA` o `EdDSA`.
- `operation` (string): `KEYGEN`, `SIGN`, `RESHARE` o `PING`.
- `threshold, participants` (number, array): El `participant_id`, `party_key` e `identity_public_key` de cada participante.
- `key_id` (string): Clave de la billetera.
- `signing_input` (string (base64)): Solo `SIGN`. Bytes exactos que se van a firmar.
- `signing_input_sha256` (string (hex)): Solo `SIGN`. SHA-256 de `signing_input`.
- `signing_context` (object): Solo `SIGN`, opcional. Detalles de la transacción proporcionados por Fystack, máximo 64 KiB.
- `derivation` (object): Solo `SIGN`, opcional. `path` y `delta` ECDSA no endurecidos.
- `new_threshold, new_participants` (number, array): Solo `RESHARE`.

```json
{
  "request_id": "3f1c2a8e-7b0d-4d0c-9a51-2f6c4f0e9b12",
  "operation": "SIGN",
  "protocol": "ECDSA",
  "key_id": "8b0f3c4e-...",
  "signing_input": "q83vEjRWeJA...",
  "signing_input_sha256": "5d41402abc4b2a76...",
  "signing_context": {
    "version": 1,
    "apex_request_id": "wd_01J9...",
    "reference": { "type": "onchain_tx", "id": "..." },
    "requested_at": "2026-10-12T08:30:00Z",
    "chain": { "network": "ethereum", "chain_id": "1", "protocol": "ECDSA" },
    "wallet": { "wallet_id": "8b0f3c4e-...", "derivation_path": [44, 60, 0, 0, 0] },
    "tx": {
      "raw": "0x02f8...",
      "payload_hash": "0x5d41402a...",
      "verified": true,
      "verification_mode": "evm_transaction",
      "decoded": { "to_address": "0xAbC...", "value": "1000000000000000000" }
    }
  },
  "threshold": 1,
  "participants": [ ... ]
}
```

`verification_mode`: `evm_transaction`, `solana_message`, `tron_transaction`, `bitcoin_transaction` o `digest_only`.

> **Note:** `signing_context` es una pista, no una prueba. El cosigner no lo contrasta con `signing_input`. Decodifica `tx.raw` por tu cuenta, recalcula el hash, compáralo con `signing_input` y luego revisa el destino, el monto y el riesgo. Rechaza lo que no puedas verificar.

## Respuesta

HTTP `200`, `Content-Type: application/jwt`, RS256 con la clave de tu servidor. Máximo 64 KiB. No se siguen las redirecciones.

- `request_id` (string): Debe coincidir con la solicitud.
- `action` (string): `approve` o `reject`. `pong` para `PING`.
- `reason` (string): Obligatorio con `reject`, máximo 1024 bytes, se omite con `approve`. Se escribe en el registro de auditoría.
- `exp` (number, required): Mantenlo corto, p. ej., 60s.

```json
{
  "request_id": "3f1c2a8e-7b0d-4d0c-9a51-2f6c4f0e9b12",
  "action": "reject",
  "reason": "destination_not_allowlisted",
  "iat": 1791794400,
  "exp": 1791794460,
  "jti": "b7e5..."
}
```

## Liveness

El cosigner envía `"operation": "PING"` al iniciar y cada 30 segundos. Responde `"action": "pong"` sin ejecutar la política.

- Si falla el ping de inicio: el cosigner se detiene.
- Si falla un ping posterior: el estado de salud muestra `callback_unavailable` y la firma se rechaza hasta que se recupere.

## Servidor de ejemplo

`cosigner-cli` incluye un ejemplo en Node.js en `examples/node-callback-server` (`express` + `jose`).

```js
app.post('/cosigner/approve', express.text({ type: 'application/jwt' }), async (req, res) => {
  try {
    // RS256 only, signed by the pinned cosigner key, with exp and request_id.
    const request = await verifier.verify(req.body.trim())

    if (request.operation === 'PING') {
      return res.type('application/jwt').send(await signer.sign(request, { action: 'pong' }))
    }

    let decision = { action: 'approve' }
    if (request.operation === 'SIGN' && !(await policyAllows(request))) {
      decision = { action: 'reject', reason: 'destination_not_allowlisted' }
    }

    res.type('application/jwt').send(await signer.sign(request, decision))
  } catch (err) {
    res.sendStatus(401)
  }
})
```

> **Note:** Tal como se distribuye, el ejemplo aprueba todo. Reemplaza la lógica de decisión antes de usar fondos reales.

## Checklist para producción

- Mantén `require_callback: true`. `cosigner-cli callback remove` desactiva la aprobación por completo.
- Deniega por defecto.
- Decodifica `tx.raw` y compáralo con `signing_input`.
- Aplica listas de permitidos, límites y controles AML.
- Sé idempotente según el `request_id`; los reintentos reenvían el mismo token.
- Guarda la clave del servidor en KMS o en un HSM. AWS KMS `RSA_4096` con `RSASSA_PKCS1_V1_5_SHA_256` funciona para RS256.
- Responde con holgura dentro del timeout.
- Configura alertas para `callback_unavailable` y para los rechazos.
