Skip to main content

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:
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:
{
  "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

event
string
Event type - always "request.completed", "request.timeout", or "request.cancelled"
request_id
string
Unique identifier for the request
loop_id
string
ID of the loop that processed this request
status
string
Final status: "completed", "timeout", or "cancelled"
response_data
object
The actual response from the reviewer (format varies by response_type). Null if timed out or cancelled.
response_by
object
Information about the reviewer who responded. Null if timed out or cancelled.
response_at
string
ISO 8601 timestamp when the response was submitted. Null if timed out or cancelled.
response_time_seconds
number
Time taken from creation to response in seconds. Null if timed out or cancelled.
created_at
string
ISO 8601 timestamp when the request was created

Event Types

Sent when a reviewer successfully completes the request.
{
  "event": "request.completed",
  "status": "completed",
  "response_data": { /* reviewer's response */ },
  "response_by": { /* reviewer info */ },
  "response_at": "2024-03-15T10:45:00Z"
}
Sent when no reviewer responds within the timeout period.
{
  "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.
Sent when the request is cancelled via the API before completion.
{
  "event": "request.cancelled",
  "status": "cancelled",
  "response_data": null,
  "response_by": null,
  "response_at": null
}

Implementing a Webhook Endpoint

Basic Endpoint

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)

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

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
Cause: Webhook delivery retries after temporary failuresSolution: Implement idempotency by tracking processed request IDs
Cause: Your endpoint takes too long to respondSolution: Return 200 OK immediately and process payload asynchronously

Next Steps

Create Request API

Learn how to create requests with callback URLs

Request Monitoring

Understand request lifecycle and polling alternatives

Integration Examples

See complete examples with webhook integration

Error Handling

Handle webhook delivery failures and retries