# Recibe y verifica el callback

Ejecuta un pequeño receptor en Node o Python, comprueba la firma de cada callback con el secreto de firma de tu espacio de trabajo, y lee las respuestas que formbase envía cuando una solicitud termina.

## Recibe y verifica el callback

Una solicitud con callbackUrl termina con un POST firmado a esa URL: las respuestas cuando el destinatario la envía, o el aviso de que expiró o se canceló. Esta guía ejecuta un receptor en tu máquina, comprueba la firma, e imprime las respuestas.

<p>
  Necesitas un token de API y un formulario publicado, como en{' '}
  <a href="/es/guides/rest-api/send-a-request">Envía una solicitud con la API REST</a>, y Node o Python en tu máquina. Qué lleva un
  callback, y cómo funcionan los reintentos, está en <a href="/es/requests/callbacks">Callbacks y firma</a>.
</p>

<h2 id="secret">1. Copia el secreto de firma</h2>

<p>
  formbase firma cada callback con el secreto de firma de solicitudes de tu espacio de trabajo. Abre <strong>OAuth y claves de API</strong>{' '}
  en la barra lateral del espacio de trabajo y busca la tarjeta <strong>Secreto de firma de solicitudes</strong>:
</p>

<p>Haz clic en el botón de copiar y guarda el secreto en una variable de entorno en la terminal donde se ejecutará tu receptor:</p>

```
export FORMBASE_SIGNING_SECRET='rqs_...'
```

> ⚠️ **No lo regeneres**
> <p>
>     El tercer botón crea un secreto nuevo, y el antiguo deja de funcionar de inmediato, también para los callbacks que ya están en camino.
>     No lo necesitas para seguir esta guía.
>   </p>

<h2 id="receiver">2. Escribe el receptor</h2>

<p>
  Guarda la función <code>verify</code> de <a href="/es/requests/callbacks#verify">Callbacks y firma</a> junto a tu receptor: como{' '}
  <code>verify.mjs</code> para Node, ya que usa <code>import</code>, o como <code>verify.py</code> para Python. El receptor lee el cuerpo en
  bruto, comprueba la firma, y solo entonces analiza el JSON:
</p>

  
    
```
import http from 'node:http'
const secret = process.env.FORMBASE_SIGNING_SECRET
http
  .createServer((req, res) => {
    const chunks = []
    req.on('data', (chunk) => chunks.push(chunk))
    req.on('end', () => {
      const rawBody = Buffer.concat(chunks).toString('utf8')
      const signature = req.headers['x-formbase-signature'] ?? ''
      if (!verifyFormbaseCallback(rawBody, signature, secret)) {
        console.log('rejected: bad signature')
        res.writeHead(401).end()
        return
      }
      const event = JSON.parse(rawBody)
      console.log(event.type, event.data.request.externalId, JSON.stringify(event.data.answers))
      res.writeHead(200).end()
    })
  })
  .listen(8787)
```

  
  
    
```
import json, os
from http.server import BaseHTTPRequestHandler, HTTPServer
from verify import verify_formbase_callback
SECRET = os.environ['FORMBASE_SIGNING_SECRET']
class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        raw_body = self.rfile.read(int(self.headers['Content-Length']))
        signature = self.headers.get('X-formbase-Signature', '')
        if not verify_formbase_callback(raw_body, signature, SECRET):
            print('rejected: bad signature', flush=True)
            self.send_response(401)
            self.end_headers()
            return
        event = json.loads(raw_body)
        print(event['type'], event['data']['request'].get('externalId'), event['data'].get('answers'), flush=True)
        self.send_response(200)
        self.end_headers()
HTTPServer(('', 8787), Handler).serve_forever()
```

  

<p>
  Calcula el hash del cuerpo exactamente como llegó. Analizarlo y volver a convertirlo en JSON cambia los bytes, y la firma deja de
  coincidir.
</p>

<h2 id="tunnel">3. Ejecútalo y dale una URL pública</h2>

<p>Arranca el receptor:</p>

```
node receiver.mjs      # or: python3 receiver.py
```

<p>
  formbase solo llama a direcciones HTTPS públicas, así que <code>localhost</code> no sirve. Mientras pruebas, un túnel le da una a tu
  máquina.{' '}
  <a href="https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/">
    El túnel rápido de Cloudflare
  </a>{' '}
  no necesita cuenta. Ejecútalo en una segunda terminal:
</p>

```
cloudflared tunnel --url http://localhost:8787
```

<p>
  Imprime una dirección como <code>https://horn-cod-classics-arab.trycloudflare.com</code>. ngrok y herramientas similares funcionan igual.
  En producción, usa la propia URL HTTPS de tu servidor en su lugar.
</p>

<h2 id="request">4. Envía una solicitud con una URL de callback</h2>

<p>
  Crea una solicitud de prueba como en la <a href="/es/guides/rest-api/send-a-request#create">guía anterior</a>, y añade{' '}
  <code>callbackUrl</code> con la dirección del túnel. Usa tu propio ID de formulario:
</p>

```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer $FORMBASE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "method": "requests.create",
    "params": {
      "formId": "jx75hdx8vb5hy1x85gm17nqgkn8f674g",
      "recipient": { "email": "ada@acme.example", "name": "Ada Lovelace" },
      "prefill": { "company_name": "Analytical Engines Ltd" },
      "readonly": ["company_name"],
      "externalId": "supplier-2044",
      "idempotencyKey": "supplier-2044",
      "callbackUrl": "https://horn-cod-classics-arab.trycloudflare.com/formbase",
      "test": true
    }
  }'
```

<p>
  Usa una <code>idempotencyKey</code> nueva: la misma clave con un cuerpo diferente se rechaza. Abre el <code>url</code> de la respuesta,
  responde el formulario, y envíalo.
</p>

<h2 id="see-it">5. Mira llegar el callback</h2>

<p>Unos segundos después de enviar, el receptor imprime el tipo de evento, tu ID externo y las respuestas. El receptor de Node imprime:</p>

```
request.completed supplier-2044 {"certificate_of_incorporation":[{"name":"certificate-of-incorporation.pdf","size":635,"type":"application/pdf","url":"https://api.formbase.so/api/storage/...",...}],"company_name":"Analytical Engines Ltd","contact_email":"ada@acme.example","do_you_accept_our_30_day_payment_terms":"yes","vat_number":"GB123456789"}
```

<p>
  Para ver un rechazo, envía tú mismo cualquier POST al receptor, por ejemplo <code>curl -X POST -d '{}' http://localhost:8787</code>. No
  tiene una firma válida, así que el receptor imprime <code>rejected: bad signature</code> y responde 401.
</p>

<h2 id="production">Antes de pasar a producción</h2>

<ul>
  <li>
    <strong>Responde 2xx en 10 segundos.</strong> formbase marca el callback como entregado con cualquier 2xx. Un 5xx, un 408, un 429 o un
    tiempo de espera agotado se reintenta durante unas cuatro horas; cualquier otro 4xx, como el 401 de arriba, detiene los reintentos.
    Consulta <a href="/es/requests/callbacks#retries">Reintentos</a>.
  </li>
  <li>
    <strong>
      Ramifica según <code>type</code>.
    </strong>{' '}
    La misma URL también recibe <code>request.expired</code> y <code>request.canceled</code>, que no llevan respuestas.
  </li>
  <li>
    <strong>Maneja cada evento una sola vez.</strong> Un reintento lleva el mismo <code>id</code>. Guarda los ids que ya manejaste y salta
    una repetición.
  </li>
  <li>
    <strong>
      Comprueba <code>test</code>.
    </strong>{' '}
    Un callback de una solicitud de prueba lleva <code>"test": true</code>. Quita <code>test</code> de <code>requests.create</code> para
    destinatarios reales.
  </li>
</ul>

<p>
  Si tu receptor estuvo caído más tiempo del que duran los reintentos, las respuestas no se pierden. Léelas con <code>requests.get</code>, o
  envía el callback de nuevo con <a href="/es/developers/rest-api#requests-replay-callback">requests.replayCallback</a> o desde la{' '}
  <a href="/es/requests/managing-requests">página de Solicitudes</a>.
</p>

<h2 id="next">Siguiente</h2>

<div class="not-prose grid gap-3 sm:grid-cols-2 mb-8">
  - [Callbacks y firma](/es/requests/callbacks) — El payload completo, los tres eventos, y el calendario de reintentos.
  - [Métodos de la API](/es/developers/rest-api) — Cada método, con sus parámetros y respuestas.
</div>
