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

# Using Webhooks

> Use webhooks to trigger Sixtyfour workflows and to receive enrichment results in real time.

Sixtyfour webhooks come in two directions:

* **Incoming**: your system POSTs into Sixtyfour to trigger a workflow run.
* **Outgoing**: Sixtyfour POSTs to your URL when an async job or workflow finishes.

Both are optional — you can always trigger work via the standard sync APIs and poll for results — but together they let you wire Sixtyfour into event-driven systems with no glue scripts.

## Which direction do I need?

| You want to...                                                            | Use                                                              |
| ------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Kick off a workflow when something happens in your CRM, scheduler, or app | **Incoming** — POST to `/workflows/run` with a `webhook_payload` |
| Get notified when an async enrichment job finishes (no polling)           | **Outgoing** — set `webhook_url` on the async endpoint           |
| Get the full result of a workflow delivered to your URL when it completes | **Outgoing** — add an `outgoing_webhook` block to the workflow   |
| Both — trigger a workflow externally and have its results delivered back  | **Incoming + outgoing** in the same workflow                     |

## Incoming webhooks (trigger workflows)

Build a workflow whose **first block is a `webhook` source block**, then POST your payload to `/workflows/run`. Each row in the payload becomes one row in the workflow's input dataset.

```bash theme={null}
curl -X POST "https://api.sixtyfour.ai/workflows/run?workflow_id=wf_abc123" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_payload": [
      { "company_name": "Acme Corp", "website": "acme.com" },
      { "company_name": "TechStart Inc", "website": "techstart.io" }
    ]
  }'
```

Sixtyfour responds with a `job_id` you can use to track the run. A few rules to know:

* API key authentication only — JWT (dashboard) sessions are rejected.
* The source block must be `webhook` — the API returns 400 otherwise.
* `read_csv` source blocks are API-triggerable but use a different mechanism — pass `specs_override.resource_handle_id` from `/storage/csv/upload` instead of `webhook_payload`. See [Workflow Execution](/api-reference/workflows/workflows-execution).

See the [Incoming Webhooks reference](/api-reference/webhooks/incoming) for the full spec, validation rules, and configuration options.

## Outgoing webhooks (receive results)

Sixtyfour POSTs to your URL through one signed, retried HTTP pipeline regardless of source. The two sources are:

1. **Async-job webhooks** — pass `webhook_url` in the body of any async endpoint (`/find-email-async`, `/people-intelligence-async`, etc.). Payload is a single job-result envelope.
2. **Workflow `outgoing_webhook` block** — add the block to a workflow. Payload is a richer envelope with `event_id`, `run_id`, inline `results`, and an optional signed `results_download_url`.

### End-to-end flow (async-job example)

```mermaid theme={null}
sequenceDiagram
    participant App as Your App
    participant SF as Sixtyfour
    App->>SF: POST /find-email-async { lead, webhook_url }
    SF-->>App: 200 { task_id }
    Note over SF: Enrichment runs...
    SF->>App: POST webhook_url { task_id, status, result }
    App-->>SF: 200 OK
```

### Step 1: Send an async request with `webhook_url`

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.sixtyfour.ai/find-email-async \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "lead": {
        "name": "Jane Smith",
        "company": "Northgate Labs",
        "linkedin_url": "https://linkedin.com/in/janesmith"
      },
      "webhook_url": "https://your-server.com/webhooks/sixtyfour"
    }'
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.sixtyfour.ai/find-email-async",
      headers={
          "x-api-key": "YOUR_API_KEY",
          "Content-Type": "application/json"
      },
      json={
          "lead": {
              "name": "Jane Smith",
              "company": "Northgate Labs",
              "linkedin_url": "https://linkedin.com/in/janesmith"
          },
          "webhook_url": "https://your-server.com/webhooks/sixtyfour"
      }
  )
  task_id = response.json()["task_id"]
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch("https://api.sixtyfour.ai/find-email-async", {
    method: "POST",
    headers: {
      "x-api-key": "YOUR_API_KEY",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      lead: {
        name: "Jane Smith",
        company: "Northgate Labs",
        linkedin_url: "https://linkedin.com/in/janesmith"
      },
      webhook_url: "https://your-server.com/webhooks/sixtyfour"
    })
  });
  const { task_id } = await response.json();
  ```
</CodeGroup>

The response returns a `task_id`. Store it — you'll match it against the incoming webhook payload.

### Step 2: Handle the webhook

When the job completes, Sixtyfour POSTs the result to your `webhook_url`. Your handler must return a 2xx status code within 10 seconds.

<Warning>Return 2xx even for unknown `task_id` values. Returning a non-2xx triggers retries, which wastes delivery attempts.</Warning>

For the workflow `outgoing_webhook` block, the request shape and headers are identical — only the JSON envelope is different (richer, with workflow metadata and an optional `results_download_url`). See the [Outgoing Webhooks reference](/api-reference/webhooks/outgoing) for both payload shapes side by side, plus signing/verification code samples.

### Signing secrets and verification

Once you generate a signing secret in [Settings → Webhooks](https://app.sixtyfour.ai/settings/webhooks), every outgoing delivery includes a `Sixtyfour-Signature` header (HMAC-SHA256 over the raw body). The same secret signs both async-job webhooks and `outgoing_webhook` block deliveries.

See [Signing Secrets & Verification](/api-reference/webhooks/signing-secrets) for the full algorithm, working Python (Flask) and Node.js (Express) code samples, and rotation behavior.

<Warning>Verify against the raw request body before JSON parsing. Re-serializing a parsed dict changes the bytes and verification will fail.</Warning>

## Testing locally

Your local development server isn't reachable from the internet. Use a tunneling tool to expose it:

```bash theme={null}
# ngrok
ngrok http 3000

# Then use the generated URL as your webhook_url:
# https://abc123.ngrok-free.app/webhooks/sixtyfour
```

Other options include [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) and [localtunnel](https://github.com/localtunnel/localtunnel). Use the HTTPS URL from your tunnel as the `webhook_url` value (or the `outgoing_webhook` block's `url`).

## Production checklist

* Track your tasks and runs by storing `task_id` (async jobs) or `run_id` (workflows) from each request, then validate incoming webhooks against your stored set.
* Dedupe with `Sixtyfour-Event-Id` — retries can deliver the same event more than once, so treat the event ID as your idempotency key.
* Once signing is enabled, reject deliveries with a missing or invalid `Sixtyfour-Signature` header.
* Monitor delivery. If all 5 retry attempts fail, the webhook is marked as undelivered. Results remain available via `GET /job-status/{task_id}` (async jobs) or the workflow result download endpoints — see [Outgoing Webhooks → Retry behavior](/api-reference/webhooks/outgoing#retry-behavior) for the full retry schedule.
