Response Webhooks are a way to receive notifications when an approval is requested or completed.

This is an advanced feature that requires a bit of setup on your end. If you’re interested in getting started quickly, we recommend using one of the following methods instead:

Response Webhooks are currently in beta. If you’d like to help us test and improve the feature, please reach out to us at contact@humanlayer.dev

Overview

Using response webhooks generally involves a few steps:

  1. Choose a way you’ll uniquely identify the operation
  2. Create an operation
  3. Store your agent’s workflow state
  4. Create a webhook endpoint
  5. Configure webhook in HumanLayer
  6. Handle webhook responses

You can find a complete working example in our FastAPI Webhooks Example.

1. Choose an Operation Identifier

First, decide how you’ll uniquely identify each operation in your system. You have several options:

  • Use a server-generated call_id
  • Use an external identifier (like OpenAI’s tool_call_id)
  • Create your own custom identifier system

2. Create the Operation

Use the appropriate creation endpoint based on your needs:

# Create a pending approval
pending_approval = await hl.create_function_call(
    spec=FunctionCallSpec(
        fn="purchase_materials",
        kwargs={"material": "wood", "quantity": 10},
    )
)

# optional - store the call_id for later use
call_id = pending_approval.call_id

3. Store Operation State

Store your agent’s workflow state so you can resume it when the webhook arrives. Common approaches include:

Dictionary Storage

# In-memory storage (for demonstration)
operations = {}
operations[call_id] = pending_approval

Database Storage

# Using your preferred database
await db.operations.insert({
    "call_id": call_id,
    "object": pending_approval.spec.model_dump(mode="json"),
    "created_at": datetime.now()
})

Using HumanLayer to store your state

When making function calls or human contacts, you can include a state object that will be preserved and returned in webhooks. This allows your application to be stateless while maintaining context across the request lifecycle.

Example:

# Store the current state when requesting approval
function_call = FunctionCall(
    run_id="run_123",
    call_id="call_456",
    spec=FunctionCallSpec(
        fn="send_email",
        kwargs={"to": "user@example.com"},
        state={
            "conversation_history": previous_messages,
            "user_preferences": preferences,
        }
    )
)

When the webhook arrives, the state will be returned in the webhook payload, allowing you to restore the conversation context.

For a complete example of using state preservation with webhooks, see the FastAPI Email Example which demonstrates maintaining conversation state across multiple email interactions.

4. Create Webhook Endpoint

Create an endpoint in your application to receive webhook notifications:

Set up an endpoint on your server that will:

  • Receive POST requests from HumanLayer
  • Validate webhook signatures
  • Process the incoming webhook data
@app.post("/webhook/function-call-completed")
async def webhook_inbound(webhook: FunctionCall) -> Dict[str, str]:
    # todo validate webhook signature
    ...

    # Retrieve the operation using call_id
    call_id = webhook.call_id

    # Update operation status
    operations[call_id] = webhook

    # Handle approval/rejection
    if webhook.status is not None and webhook.status.approved:
        # Execute the approved operation
        await execute_operation(webhook.spec.fn, webhook.spec.kwargs)
    else:
        print(f"Operation {call_id} denied")
        # or append the rejection to an LLM context window and let it try again

    return {"status": "ok"}

Webhooks will be sent with the fully completed FunctionCall or HumanContact object, which will include the call_id you set during creation, as well as the resolved response in status, and any state you set in the spec.

5. Configure Webhook in Dashboard

In the HumanLayer Dashboard:

  1. Navigate to Response Webhooks
  2. Create a new endpoint with your URL
  3. Subscribe to relevant events:
    • function_call.completed
    • human_contact.completed

6. Approve the request

Approve the request using any of the supported channels in HumanLayer, e.g. the web app, slack, email, etc.

7. Handle the payload

The approval should trigger a webhook to your endpoint, which will include the fully completed FunctionCall or HumanContact object.

Complete Example

For a complete working example including ngrok setup for local development, see our FastAPI Webhooks Example.

The example demonstrates:

  • Setting up a FastAPI application with webhook handling
  • Managing operation state
  • Processing approvals and rejections
  • Local development with ngrok