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

# Set Up Webhooks for Lead Intake and Event Processing

> Configure inbound and outbound webhooks in AgentFlow to receive lead submissions and send qualified lead events to your downstream systems securely.

Webhooks are AgentFlow's primary event interface — they are how leads arrive from your forms and tools, and how qualified lead decisions leave AgentFlow and reach your CRM, automation platform, or internal services. Understanding both directions of webhook flow lets you build a reliable, auditable pipeline from first contact to downstream action.

## The two webhook directions

AgentFlow operates two distinct webhook directions, each with its own configuration and security model:

| Direction    | Who sends               | Who receives                   | Purpose                                            |
| ------------ | ----------------------- | ------------------------------ | -------------------------------------------------- |
| **Inbound**  | Your form, tool, or app | AgentFlow                      | Deliver new leads into AgentFlow for qualification |
| **Outbound** | AgentFlow               | Your CRM, ops tool, or service | Notify your systems of qualified lead decisions    |

Configure both directions to complete the end-to-end flow: intake → qualify → decide → hand off.

***

## Configure an inbound webhook

An inbound webhook lets any external tool POST lead data directly into AgentFlow's qualification pipeline.

<Warning>
  AgentFlow only accepts inbound webhook connections over HTTPS. Plain HTTP endpoints are rejected at the transport layer. Ensure your sending tool or service supports HTTPS and that your network path does not terminate TLS before the request reaches AgentFlow.
</Warning>

<Steps>
  <Step title="Locate your inbound endpoint URL">
    In your AgentFlow dashboard, navigate to **Settings → Webhooks → Inbound**. Copy the unique endpoint URL assigned to your workspace. This URL is specific to your account — treat it as a credential and do not publish it.
  </Step>

  <Step title="Structure your payload">
    Send a `POST` request with `Content-Type: application/json`. AgentFlow expects the following fields at minimum:

    ```json theme={null}
    {
      "name": "Jordan Ellis",
      "email": "jordan.ellis@stacklane.co",
      "company": "Stacklane",
      "message": "We're evaluating tools to handle inbound demo requests at scale. Currently doing it manually.",
      "source": "website_contact_form",
      "timestamp": "2024-11-14T08:44:00Z"
    }
    ```

    | Field       | Required    | Description                                                 |
    | ----------- | ----------- | ----------------------------------------------------------- |
    | `name`      | Yes         | Full name of the lead                                       |
    | `email`     | Yes         | Lead's email address — used as the primary identifier       |
    | `company`   | Recommended | Company or organization name                                |
    | `message`   | Recommended | The lead's inquiry text — strongly improves scoring quality |
    | `source`    | Recommended | Where the lead originated (form name, channel, campaign)    |
    | `timestamp` | Recommended | ISO 8601 submission timestamp from the sending system       |

    Additional fields you include are stored on the lead record and surfaced in the operator dashboard, but they do not affect required validation.
  </Step>

  <Step title="Authenticate your inbound requests">
    AgentFlow verifies inbound webhook authenticity using a **shared secret**. Find your inbound webhook secret in **Settings → Webhooks → Inbound → Signing Secret**.

    Include a signature header with every POST:

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

      secret = "your_inbound_signing_secret"
      payload = json.dumps(lead_data, separators=(',', ':'))
      timestamp = str(int(time.time()))
      signed_content = f"{timestamp}.{payload}"

      signature = hmac.new(
          secret.encode(),
          signed_content.encode(),
          hashlib.sha256
      ).hexdigest()

      headers = {
          "Content-Type": "application/json",
          "X-AgentFlow-Timestamp": timestamp,
          "X-AgentFlow-Signature": f"sha256={signature}"
      }
      ```

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

      const secret = 'your_inbound_signing_secret';
      const payload = JSON.stringify(leadData);
      const timestamp = Math.floor(Date.now() / 1000).toString();
      const signedContent = `${timestamp}.${payload}`;

      const signature = crypto
        .createHmac('sha256', secret)
        .update(signedContent)
        .digest('hex');

      const headers = {
        'Content-Type': 'application/json',
        'X-AgentFlow-Timestamp': timestamp,
        'X-AgentFlow-Signature': `sha256=${signature}`,
      };
      ```
    </CodeGroup>

    AgentFlow rejects requests with missing, malformed, or invalid signatures. It also rejects requests where the `X-AgentFlow-Timestamp` is more than five minutes old, protecting against replay attacks.
  </Step>

  <Step title="Test your inbound connection">
    Send a test POST to your inbound endpoint and check the **Inbound Logs** panel in the dashboard. A successful intake shows status `received` and triggers the qualification pipeline. An authentication failure shows status `rejected` with a reason code.
  </Step>
</Steps>

***

## Configure an outbound webhook

AgentFlow sends outbound webhook events to your configured URL whenever a lead reaches a terminal qualification state. Your downstream system receives the event and acts on it — creating a CRM record, triggering a Zapier zap, or notifying a Slack channel.

<Steps>
  <Step title="Register your destination URL">
    Navigate to **Settings → Webhooks → Outbound**. Enter the URL you want AgentFlow to POST events to. This must be an HTTPS endpoint that is publicly reachable by AgentFlow's servers.

    <Tip>
      During initial setup, point your destination URL at a webhook inspection tool like [Webhook.site](https://webhook.site) so you can inspect the exact payload and headers before wiring up your production system.
    </Tip>
  </Step>

  <Step title="Select the event types to receive">
    AgentFlow can send three outbound event types. Subscribe to only the events your downstream system needs to act on:

    | Event type              | When it fires                                                                  |
    | ----------------------- | ------------------------------------------------------------------------------ |
    | `lead.qualified`        | An operator approved the lead, or it met the auto-approve score threshold      |
    | `lead.rejected`         | An operator rejected the lead, or it fell below the auto-reject threshold      |
    | `lead.review_requested` | The lead score fell in the manual-review range and is awaiting operator action |
  </Step>

  <Step title="Verify outbound event signatures">
    Every outbound request from AgentFlow includes an `X-AgentFlow-Signature` header. Verify this signature in your receiving service before processing the payload to confirm the event genuinely originated from AgentFlow.

    Your outbound signing secret is available at **Settings → Webhooks → Outbound → Signing Secret**.

    ```python Signature verification (pseudocode) theme={null}
    def verify_agentflow_signature(request_body: bytes, timestamp: str, signature_header: str, secret: str) -> bool:
        """
        Verify an outbound AgentFlow webhook signature.

        Args:
            request_body: The raw request body bytes (do not parse JSON first)
            timestamp: Value of the X-AgentFlow-Timestamp header
            signature_header: Value of the X-AgentFlow-Signature header (e.g. "sha256=abc123...")
            secret: Your outbound webhook signing secret from AgentFlow settings

        Returns:
            True if the signature is valid; False otherwise
        """
        # 1. Reject requests where the timestamp is more than 5 minutes old
        if abs(current_unix_time() - int(timestamp)) > 300:
            return False

        # 2. Reconstruct the signed content: timestamp + "." + raw body
        signed_content = f"{timestamp}.{request_body.decode('utf-8')}"

        # 3. Compute the expected HMAC-SHA256 signature
        expected = "sha256=" + hmac_sha256(key=secret, message=signed_content)

        # 4. Compare using a constant-time equality check to prevent timing attacks
        return constant_time_compare(expected, signature_header)
    ```

    <Warning>
      Always use a constant-time string comparison when checking signatures. Standard string equality (`==`) is vulnerable to timing attacks that can allow an attacker to forge valid signatures incrementally.
    </Warning>
  </Step>

  <Step title="Return a 2xx response quickly">
    AgentFlow considers a delivery successful when your endpoint returns any `2xx` HTTP status code within ten seconds. If your processing takes longer than that, return `200 OK` immediately and handle the event asynchronously in a background job or queue.

    If AgentFlow receives a non-2xx response or the request times out, it retries delivery with exponential backoff. Check your **Outbound Logs** to see delivery attempts and response codes.
  </Step>
</Steps>

***

## Security best practices

<CardGroup cols={2}>
  <Card title="HTTPS only" icon="lock">
    Both inbound and outbound endpoints must use HTTPS. Never accept or send webhook traffic over plain HTTP — payloads contain lead PII and should be encrypted in transit.
  </Card>

  <Card title="Verify every signature" icon="shield-check">
    Validate the `X-AgentFlow-Signature` header on every inbound event your service receives. Reject any request that fails signature verification before touching the payload.
  </Card>

  <Card title="Reject stale timestamps" icon="clock">
    Check that `X-AgentFlow-Timestamp` is within five minutes of your server's current time. Stale timestamp rejection prevents replay attacks where an attacker resends a previously captured valid request.
  </Card>

  <Card title="Design for idempotency" icon="repeat">
    Use the `lead_id` field as a deduplication key. AgentFlow may retry delivery on timeout or transient failure — your handler should safely ignore or skip duplicate events with the same `lead_id` and event type.
  </Card>
</CardGroup>

***

## Troubleshooting common issues

<Accordion title="Inbound: AgentFlow returns 401 Unauthorized">
  The signature on your inbound request failed validation. Confirm you are signing the raw JSON body (not a re-serialized version) with your inbound signing secret, and that the `X-AgentFlow-Timestamp` value matches the one used in the signed content string. Regenerate your signing secret in settings and update your sender if the issue persists.
</Accordion>

<Accordion title="Inbound: AgentFlow returns 422 Unprocessable Entity">
  The payload is missing a required field or a field value failed format validation. Check that `name` and `email` are present and that `email` is a valid address format. Review the error body in the response for a field-level description.
</Accordion>

<Accordion title="Outbound: My endpoint is not receiving events">
  First, confirm the destination URL is correct and publicly reachable — not a local development address. Check the **Outbound Logs** panel for delivery attempts. If attempts show a connection error, verify that your server's firewall or hosting provider is not blocking inbound requests from AgentFlow's IP ranges.
</Accordion>

<Accordion title="Outbound: I'm receiving events but processing them twice">
  Your handler is likely not idempotent. Use `lead_id` plus the event `type` as a composite deduplication key. Store processed event IDs in a cache or database and skip events you have already handled. AgentFlow may deliver the same event more than once in retry scenarios.
</Accordion>

<Accordion title="Outbound: Signature verification is always failing">
  Confirm you are reading the raw request body bytes — not the parsed JSON object — before computing the expected signature. Parsing and re-serializing JSON can change whitespace or key ordering, producing a different byte sequence than the one AgentFlow signed. Also verify you are using the **outbound** signing secret, not the inbound one.
</Accordion>
