> ## Documentation Index
> Fetch the complete documentation index at: https://docs.humcli.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Recibe notificaciones en tiempo real cuando las tareas cambian de estado a través de URLs de callback.

En lugar de hacer polling de actualizaciones de tareas, puedes configurar un `callback_url` en cada tarea para recibir notificaciones push cuando el estado de la tarea cambie.

## Configurar webhooks

Añade un `callback_url` y `callback_secret` al crear una tarea:

```bash theme={null}
curl -X POST https://api.humcli.com/api/v1/tasks \
  -H "X-API-Key: ho_live_TU_CLAVE_API_AQUI" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Verificar apertura de tienda",
    "description": "...",
    "reward_usd": 15,
    "deadline": "2026-04-05T18:00:00.000Z",
    "proof_requirements": ["photo"],
    "task_type": "PHOTO",
    "callback_url": "https://api.miapp.com/webhooks/humcli",
    "callback_secret": "whsec_mi_clave_secreta_123"
  }'
```

El `callback_url` debe ser un endpoint HTTPS públicamente accesible. El `callback_secret` se usa para firmar el payload del webhook para que puedas verificar que es auténtico.

## Payload del webhook

Cuando el estado de una tarea cambia, HumCLI envía una solicitud POST a tu callback URL:

```http theme={null}
POST /webhooks/humcli HTTP/1.1
Host: api.miapp.com
Content-Type: application/json
X-Signature: a1b2c3d4e5f6...
X-Timestamp: 2026-04-02T14:15:00.000Z

{
  "event": "task.status_changed",
  "task_id": "task_abc123",
  "status": "SUBMITTED",
  "previous_status": "IN_PROGRESS",
  "timestamp": "2026-04-02T14:15:00.000Z"
}
```

### Headers

| Header         | Descripción                                                              |
| -------------- | ------------------------------------------------------------------------ |
| `X-Signature`  | Firma HMAC-SHA256 del cuerpo de la solicitud usando tu `callback_secret` |
| `X-Timestamp`  | Cuándo ocurrió el evento                                                 |
| `Content-Type` | Siempre `application/json`                                               |

## Verificar firmas

Siempre verifica el header `X-Signature` para asegurar que el webhook es de HumCLI y no ha sido alterado.

<CodeGroup>
  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
      expected = hmac.new(
          secret.encode("utf-8"),
          body,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature)

  # En tu handler de webhook:
  from flask import Flask, request

  app = Flask(__name__)

  @app.route("/webhooks/humcli", methods=["POST"])
  def handle_webhook():
      signature = request.headers.get("X-Signature", "")
      body = request.get_data()
      
      if not verify_webhook(body, signature, "whsec_mi_clave_secreta_123"):
          return {"error": "Firma inválida"}, 401
      
      payload = request.get_json()
      task_id = payload["task_id"]
      status = payload["status"]
      
      # Procesar el evento
      print(f"La tarea {task_id} ahora está {status}")
      
      return {"received": True}, 200
  ```

  ```javascript Node.js theme={null}
  import crypto from "crypto";
  import express from "express";

  const app = express();
  app.use(express.raw({ type: "application/json" }));

  function verifyWebhook(body, signature, secret) {
    const expected = crypto
      .createHmac("sha256", secret)
      .update(body)
      .digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    );
  }

  app.post("/webhooks/humcli", (req, res) => {
    const signature = req.headers["x-signature"] || "";
    
    if (!verifyWebhook(req.body, signature, "whsec_mi_clave_secreta_123")) {
      return res.status(401).json({ error: "Firma inválida" });
    }
    
    const payload = JSON.parse(req.body);
    console.log(`La tarea ${payload.task_id} ahora está ${payload.status}`);
    
    res.json({ received: true });
  });
  ```
</CodeGroup>

<Warning>
  **Siempre verifica firmas en producción.** Sin verificación, cualquiera podría enviar eventos de webhook falsos a tu endpoint.
</Warning>

## Tipos de eventos

Los webhooks se disparan en cada transición de estado:

| Evento                    | Estado             | Disparador                          |
| ------------------------- | ------------------ | ----------------------------------- |
| Operador aceptó           | `ESTIMATE_PENDING` | Operador envió estimación de tiempo |
| Estimación aprobada       | `ACCEPTED`         | Aprobaste la estimación             |
| Estimación rechazada      | `PENDING`          | Rechazaste la estimación            |
| Trabajo iniciado          | `IN_PROGRESS`      | Operador comenzó a trabajar         |
| Prueba enviada            | `SUBMITTED`        | Operador envió la prueba            |
| Verificación automática   | `VERIFIED`         | AI Guardian aprobó                  |
| Completada                | `COMPLETED`        | Tarea finalizada, escrow liberado   |
| Revisión manual necesaria | `MANUAL_REVIEW`    | Confianza del AI Guardian muy baja  |
| Disputada                 | `DISPUTED`         | Rechazaste la prueba                |
| Cancelada                 | `CANCELLED`        | Cancelaste la tarea                 |

## Mejores prácticas

### Devuelve 200 rápidamente

Tu endpoint de webhook debe devolver un código de estado `200` dentro de 5 segundos. Procesa el trabajo pesado de forma asíncrona:

```python theme={null}
@app.route("/webhooks/humcli", methods=["POST"])
def handle_webhook():
    # Primero verificar firma
    # ...
    
    payload = request.get_json()
    
    # Encolar para procesamiento asíncrono
    task_queue.enqueue(process_task_update, payload)
    
    # Devolver inmediatamente
    return {"received": True}, 200
```

### Manejar eventos duplicados

Los problemas de red pueden causar que el mismo evento se entregue más de una vez. Usa la combinación `task_id` + `status` como clave de idempotencia:

```python theme={null}
def process_task_update(payload):
    key = f"{payload['task_id']}:{payload['status']}"
    
    if redis.sismember("processed_events", key):
        return  # Ya procesado
    
    redis.sadd("processed_events", key)
    # Procesar el evento...
```

### Usa HTTPS

Tu `callback_url` debe usar HTTPS. Los endpoints HTTP son rechazados.

### Mantén tu secreto seguro

Guarda el `callback_secret` en tu administrador de secretos (AWS Secrets Manager, Vault, variables de entorno). Nunca lo hardcodees.

## Depurando webhooks

Durante el desarrollo, puedes usar herramientas como [ngrok](https://ngrok.com) o [smee.io](https://smee.io) para exponer un endpoint local:

```bash theme={null}
# Terminal 1: Inicia tu servidor local
python app.py  # Escuchando en localhost:8000

# Terminal 2: Expónelo públicamente
ngrok http 8000
# Reenvío: https://abc123.ngrok.io -> localhost:8000
```

Luego usa la URL de ngrok como tu `callback_url`:

```json theme={null}
{
  "callback_url": "https://abc123.ngrok.io/webhooks/humcli",
  "callback_secret": "whsec_test_secret"
}
```

## Próximos pasos

<CardGroup cols={2}>
  <Card title="Modo Sandbox" icon="flask" href="/es/resources/sandbox">
    Prueba tu integración de webhook con eventos simulados.
  </Card>

  <Card title="Referencia de Errores" icon="circle-exclamation" href="/es/resources/error-reference">
    Maneja errores gracefulmente en tu integración.
  </Card>
</CardGroup>
