> ## 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

> Receive real-time notifications when tasks change status via callback URLs.

Instead of polling for task updates, you can configure a `callback_url` on each task to receive push notifications when the task status changes.

## Setting up webhooks

Add a `callback_url` and `callback_secret` when creating a task:

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

The `callback_url` must be a publicly accessible HTTPS endpoint. The `callback_secret` is used to sign the webhook payload so you can verify it is authentic.

## Webhook payload

When a task status changes, HumCLI sends a POST request to your callback URL:

```http theme={null}
POST /webhooks/humcli HTTP/1.1
Host: api.myapp.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         | Description                                                            |
| -------------- | ---------------------------------------------------------------------- |
| `X-Signature`  | HMAC-SHA256 signature of the request body using your `callback_secret` |
| `X-Timestamp`  | When the event occurred                                                |
| `Content-Type` | Always `application/json`                                              |

## Verifying signatures

Always verify the `X-Signature` header to ensure the webhook is from HumCLI and has not been tampered with.

<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)

  # In your webhook handler:
  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_my_secret_key_123"):
          return {"error": "Invalid signature"}, 401
      
      payload = request.get_json()
      task_id = payload["task_id"]
      status = payload["status"]
      
      # Process the event
      print(f"Task {task_id} is now {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_my_secret_key_123")) {
      return res.status(401).json({ error: "Invalid signature" });
    }
    
    const payload = JSON.parse(req.body);
    console.log(`Task ${payload.task_id} is now ${payload.status}`);
    
    res.json({ received: true });
  });
  ```
</CodeGroup>

<Warning>
  **Always verify signatures in production.** Without verification, anyone could send fake webhook events to your endpoint.
</Warning>

## Event types

Webhooks fire on every status transition:

| Event                | Status             | Trigger                          |
| -------------------- | ------------------ | -------------------------------- |
| Operator accepted    | `ESTIMATE_PENDING` | Operator submitted time estimate |
| Estimate approved    | `ACCEPTED`         | You approved the estimate        |
| Estimate rejected    | `PENDING`          | You rejected the estimate        |
| Work started         | `IN_PROGRESS`      | Operator started working         |
| Proof submitted      | `SUBMITTED`        | Operator submitted proof         |
| Auto-verified        | `VERIFIED`         | AI Guardian approved             |
| Completed            | `COMPLETED`        | Task finalized, escrow released  |
| Manual review needed | `MANUAL_REVIEW`    | AI Guardian confidence too low   |
| Disputed             | `DISPUTED`         | You rejected the proof           |
| Cancelled            | `CANCELLED`        | You cancelled the task           |

## Best practices

### Return 200 quickly

Your webhook endpoint should return a `200` status code within 5 seconds. Do heavy processing asynchronously:

```python theme={null}
@app.route("/webhooks/humcli", methods=["POST"])
def handle_webhook():
    # Verify signature first
    # ...
    
    payload = request.get_json()
    
    # Queue for async processing
    task_queue.enqueue(process_task_update, payload)
    
    # Return immediately
    return {"received": True}, 200
```

### Handle duplicate events

Network issues can cause the same event to be delivered more than once. Use the `task_id` + `status` combination as an idempotency key:

```python theme={null}
def process_task_update(payload):
    key = f"{payload['task_id']}:{payload['status']}"
    
    if redis.sismember("processed_events", key):
        return  # Already handled
    
    redis.sadd("processed_events", key)
    # Process the event...
```

### Use HTTPS

Your `callback_url` must use HTTPS. HTTP endpoints are rejected.

### Keep your secret secure

Store the `callback_secret` in your secrets manager (AWS Secrets Manager, Vault, environment variables). Never hardcode it.

## Debugging webhooks

During development, you can use tools like [ngrok](https://ngrok.com) or [smee.io](https://smee.io) to expose a local endpoint:

```bash theme={null}
# Terminal 1: Start your local server
python app.py  # Listening on localhost:8000

# Terminal 2: Expose it publicly
ngrok http 8000
# Forwarding: https://abc123.ngrok.io -> localhost:8000
```

Then use the ngrok URL as your `callback_url`:

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

## Next steps

<CardGroup cols={2}>
  <Card title="Sandbox Mode" icon="flask" href="/resources/sandbox">
    Test your webhook integration with simulated events.
  </Card>

  <Card title="Error Reference" icon="circle-exclamation" href="/resources/error-reference">
    Handle errors gracefully in your integration.
  </Card>
</CardGroup>
