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

# Webhooks

> Receive real-time notifications when requests are completed using callback URLs. Configure callbacks on a per-request basis for instant updates.

# Webhooks

Instead of polling the API to check request status, you can provide a `callback_url` when creating a request. HITL.sh will send an HTTP POST to your callback URL when the request is completed, timed out, or cancelled.

## How Callback URLs Work

When creating a request, include the `callback_url` parameter:

```python theme={null}
import requests

request_data = {
    "processing_type": "time-sensitive",
    "type": "markdown",
    "priority": "high",
    "request_text": "Please review this user comment",
    "response_type": "single_select",
    "response_config": {
        "options": ["Approve", "Reject"]
    },
    "default_response": "Reject",
    "timeout_seconds": 3600,
    "callback_url": "https://your-app.com/webhook/hitl/completed",
    "platform": "api"
}

response = requests.post(
    f"https://api.hitl.sh/v1/api/loops/{loop_id}/requests",
    headers={"Authorization": f"Bearer {api_key}"},
    json=request_data
)
```

## Webhook Payload

When a request is completed, HITL.sh sends a POST request to your `callback_url` with the following JSON payload:

```json theme={null}
{
  "event": "request.completed",
  "request_id": "65f1234567890abcdef12348",
  "loop_id": "65f1234567890abcdef12345",
  "status": "completed",
  "response_data": {
    "selected_value": "Approve"
  },
  "response_by": {
    "user_id": "65f1234567890abcdef12346",
    "name": "John Doe",
    "email": "john@example.com"
  },
  "response_at": "2024-03-15T10:45:00Z",
  "response_time_seconds": 245.5,
  "created_at": "2024-03-15T10:41:00Z"
}
```

### Payload Fields

<ResponseField name="event" type="string">
  Event type - always `"request.completed"`, `"request.timeout"`, or `"request.cancelled"`
</ResponseField>

<ResponseField name="request_id" type="string">
  Unique identifier for the request
</ResponseField>

<ResponseField name="loop_id" type="string">
  ID of the loop that processed this request
</ResponseField>

<ResponseField name="status" type="string">
  Final status: `"completed"`, `"timeout"`, or `"cancelled"`
</ResponseField>

<ResponseField name="response_data" type="object">
  The actual response from the reviewer (format varies by response\_type). Null if timed out or cancelled.
</ResponseField>

<ResponseField name="response_by" type="object">
  Information about the reviewer who responded. Null if timed out or cancelled.
</ResponseField>

<ResponseField name="response_at" type="string">
  ISO 8601 timestamp when the response was submitted. Null if timed out or cancelled.
</ResponseField>

<ResponseField name="response_time_seconds" type="number">
  Time taken from creation to response in seconds. Null if timed out or cancelled.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp when the request was created
</ResponseField>

## Event Types

<AccordionGroup>
  <Accordion title="request.completed">
    Sent when a reviewer successfully completes the request.

    ```json theme={null}
    {
      "event": "request.completed",
      "status": "completed",
      "response_data": { /* reviewer's response */ },
      "response_by": { /* reviewer info */ },
      "response_at": "2024-03-15T10:45:00Z"
    }
    ```
  </Accordion>

  <Accordion title="request.timeout">
    Sent when no reviewer responds within the timeout period.

    ```json theme={null}
    {
      "event": "request.timeout",
      "status": "timeout",
      "response_data": null,
      "response_by": null,
      "response_at": null
    }
    ```

    Your application should use the `default_response` value you specified when creating the request.
  </Accordion>

  <Accordion title="request.cancelled">
    Sent when the request is cancelled via the API before completion.

    ```json theme={null}
    {
      "event": "request.cancelled",
      "status": "cancelled",
      "response_data": null,
      "response_by": null,
      "response_at": null
    }
    ```
  </Accordion>
</AccordionGroup>

## Implementing a Webhook Endpoint

### Basic Endpoint

<CodeGroup>
  ```python Python (Flask) theme={null}
  from flask import Flask, request, jsonify
  import hmac
  import hashlib

  app = Flask(__name__)

  @app.route('/webhook/hitl/completed', methods=['POST'])
  def hitl_webhook():
      # Get the webhook payload
      payload = request.get_json()

      request_id = payload['request_id']
      event = payload['event']
      status = payload['status']

      if event == 'request.completed':
          # Handle completed request
          response_data = payload['response_data']
          reviewer = payload['response_by']

          print(f"Request {request_id} completed by {reviewer['name']}")
          print(f"Response: {response_data}")

          # Update your database, trigger workflows, etc.
          process_completed_request(request_id, response_data)

      elif event == 'request.timeout':
          # Handle timeout
          print(f"Request {request_id} timed out")
          use_default_response(request_id)

      elif event == 'request.cancelled':
          # Handle cancellation
          print(f"Request {request_id} was cancelled")
          mark_request_cancelled(request_id)

      # Return 200 OK to acknowledge receipt
      return jsonify({"status": "received"}), 200

  if __name__ == '__main__':
      app.run(port=5000)
  ```

  ```javascript Node.js (Express) theme={null}
  const express = require('express');
  const app = express();

  app.use(express.json());

  app.post('/webhook/hitl/completed', (req, res) => {
      const payload = req.body;

      const requestId = payload.request_id;
      const event = payload.event;
      const status = payload.status;

      if (event === 'request.completed') {
          // Handle completed request
          const responseData = payload.response_data;
          const reviewer = payload.response_by;

          console.log(`Request ${requestId} completed by ${reviewer.name}`);
          console.log(`Response:`, responseData);

          // Update your database, trigger workflows, etc.
          processCompletedRequest(requestId, responseData);

      } else if (event === 'request.timeout') {
          // Handle timeout
          console.log(`Request ${requestId} timed out`);
          useDefaultResponse(requestId);

      } else if (event === 'request.cancelled') {
          // Handle cancellation
          console.log(`Request ${requestId} was cancelled`);
          markRequestCancelled(requestId);
      }

      // Return 200 OK to acknowledge receipt
      res.status(200).json({ status: 'received' });
  });

  app.listen(5000, () => {
      console.log('Webhook server running on port 5000');
  });
  ```
</CodeGroup>

## Best Practices

### 1. Return 200 OK Quickly

Always return a `200 OK` response immediately after receiving the webhook. Process the payload asynchronously to avoid timeouts:

```python theme={null}
@app.route('/webhook/hitl/completed', methods=['POST'])
def hitl_webhook():
    payload = request.get_json()

    # Queue the payload for async processing
    task_queue.enqueue(process_webhook, payload)

    # Return immediately
    return jsonify({"status": "received"}), 200
```

### 2. Handle Retries Gracefully

HITL.sh will retry failed webhook deliveries up to 3 times with exponential backoff. Make your endpoint idempotent to handle duplicate deliveries:

```python theme={null}
def process_webhook(payload):
    request_id = payload['request_id']

    # Check if already processed
    if is_already_processed(request_id):
        print(f"Webhook for {request_id} already processed, skipping")
        return

    # Process the webhook
    handle_request_completion(payload)

    # Mark as processed
    mark_as_processed(request_id)
```

### 3. Validate Webhook Authenticity

While HITL.sh callback URLs are set per-request and only known to you, you should still validate incoming webhooks:

```python theme={null}
def validate_webhook_source(request):
    # Check source IP (optional)
    allowed_ips = ['52.25.180.123', '54.245.23.45']  # HITL.sh IPs
    source_ip = request.remote_addr

    if source_ip not in allowed_ips:
        return False

    # Verify required fields are present
    payload = request.get_json()
    required_fields = ['event', 'request_id', 'status']

    return all(field in payload for field in required_fields)
```

### 4. Use HTTPS

Always use HTTPS endpoints for your `callback_url` to ensure webhook payloads are encrypted in transit:

```python theme={null}
# Good
callback_url = "https://your-app.com/webhook/hitl"

# Bad - don't use HTTP
callback_url = "http://your-app.com/webhook/hitl"  # ❌ Insecure
```

### 5. Handle Errors Gracefully

If your webhook endpoint fails, HITL.sh will retry. Log errors for debugging:

```python theme={null}
@app.route('/webhook/hitl/completed', methods=['POST'])
def hitl_webhook():
    try:
        payload = request.get_json()
        process_webhook(payload)
        return jsonify({"status": "received"}), 200

    except Exception as e:
        logger.error(f"Webhook processing failed: {str(e)}", exc_info=True)
        # Return 500 to trigger retry
        return jsonify({"error": str(e)}), 500
```

## Testing Webhooks

### Local Development with ngrok

Use ngrok to expose your local server for webhook testing:

```bash theme={null}
# Start your local server
python app.py  # Running on localhost:5000

# In another terminal, start ngrok
ngrok http 5000

# Use the ngrok URL as your callback_url
# Example: https://abc123.ngrok.io/webhook/hitl/completed
```

### Manual Testing

Create a test request with your callback URL:

```python theme={null}
import requests

test_request = {
    "processing_type": "time-sensitive",
    "type": "markdown",
    "priority": "low",
    "request_text": "Test webhook - please select any option",
    "response_type": "single_select",
    "response_config": {
        "options": ["Option A", "Option B"]
    },
    "default_response": "Option A",
    "timeout_seconds": 300,  # 5 minutes
    "callback_url": "https://your-ngrok-url.ngrok.io/webhook/hitl/completed",
    "platform": "api"
}

response = requests.post(
    f"https://api.hitl.sh/v1/api/loops/{loop_id}/requests",
    headers={"Authorization": f"Bearer {api_key}"},
    json=test_request
)

print(f"Test request created: {response.json()['data']['request_id']}")
print("Respond to it in the HITL mobile app to trigger the webhook")
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook Not Received">
    **Possible causes:**

    * Callback URL is not publicly accessible
    * Using HTTP instead of HTTPS
    * Firewall blocking incoming requests
    * Webhook endpoint returned error status

    **Solutions:**

    * Test your endpoint with curl or Postman
    * Ensure HTTPS is used
    * Check firewall rules
    * Return 200 OK status code
  </Accordion>

  <Accordion title="Duplicate Webhooks">
    **Cause:** Webhook delivery retries after temporary failures

    **Solution:** Implement idempotency by tracking processed request IDs
  </Accordion>

  <Accordion title="Webhook Timeout">
    **Cause:** Your endpoint takes too long to respond

    **Solution:** Return 200 OK immediately and process payload asynchronously
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Create Request API" icon="code" href="/api-reference/requests/create-request">
    Learn how to create requests with callback URLs
  </Card>

  <Card title="Request Monitoring" icon="chart-line" href="/concepts/requests">
    Understand request lifecycle and polling alternatives
  </Card>

  <Card title="Integration Examples" icon="puzzle-piece" href="/examples/content-moderation">
    See complete examples with webhook integration
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation" href="/api-reference/errors">
    Handle webhook delivery failures and retries
  </Card>
</CardGroup>
