> ## 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.

# Outgoing Webhooks

> Receive HTTP notifications from Sixtyfour when async jobs and workflows complete.

## Use Case

Sixtyfour POSTs to your URL when work completes — so you can react in real time instead of polling. Outgoing webhooks come from two sources, both delivered through the same signed, retried HTTP pipeline:

| Source | Trigger | Payload |
| - | - | - |
| **Async-job webhook** | Pass `webhook_url` in the body of an async endpoint | Job result envelope |
| **Workflow `outgoing_webhook` block** | Add the block to a workflow | Workflow run envelope with results |

Headers, signing, retry behavior, and verification are identical for both sources — only the payload shape differs.

## Source 1: Async-job webhooks

All async enrichment endpoints accept an optional `webhook_url` parameter. When the job completes, Sixtyfour POSTs the result to that URL. Poll `/job-status/{task_id}` if no callback arrives within your application's deadline.

### Supported endpoints

| Endpoint | Description |
| - | - |
| `/find-email-async` | Single email enrichment |
| `/find-phone-async` | Single phone enrichment |
| `/find-email-bulk-async` | Bulk email enrichment (up to 100 leads) |
| `/find-phone-bulk-async` | Bulk phone enrichment (up to 100 leads) |
| `/people-intelligence-async` | People intelligence |
| `/company-intelligence-async` | Company intelligence |
| `/research-agent-async` | Research agent |
| `/qa-agent-async` | QA agent evaluation |

### Request

Add the optional `webhook_url` parameter to your async request:

```json theme={null}
POST /find-email-async
{
  "lead": {
    "name": "John Doe",
    "company": "Acme Inc",
    "linkedin_url": "https://linkedin.com/in/johndoe"
  },
  "webhook_url": "https://your-server.com/webhooks/sixtyfour"
}
```

The async endpoint immediately returns a `task_id`:

```json theme={null}
{
  "task_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "RUNNING",
  "total_leads": 1,
  "mode": "PROFESSIONAL"
}
```

### Payload — successful job

```json theme={null}
{
  "task_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "task_type": "find_email",
  "result": {
    "leads": [
      {
        "name": "John Doe",
        "company": "Acme Inc",
        "email": [["john.doe@acme.com", "OK", "COMPANY"]],
        "other_emails": []
      }
    ],
    "total_leads": 1,
    "emails_found": 1,
    "cost_cents": 5,
    "mode": "PROFESSIONAL"
  },
  "error": null
}
```

Find Email and Find Phone callbacks wrap records in `result.leads`, including single-record requests. Single-record sync and `/job-status` responses unwrap the record.

<Note>Find Phone callbacks currently carry email-style metadata: `task_type: "find_email"`, `emails_found`, and an email-based `cost_cents`. Read phone numbers from `result.leads`, correlate callbacks by `task_id`, and retrieve `charge_amount` from `/job-status/{task_id}`.</Note>

### Job failures

Find Email and Find Phone, including their bulk endpoints, do not send a failure callback when enrichment fails. Failure callbacks are not guaranteed for other endpoints, cancellations, terminations, or job timeouts. Poll `/job-status/{task_id}` if no callback arrives within your application's deadline.

### Payload fields

| Field | Type | Description |
| - | - | - |
| `task_id` | string | Unique identifier for the job. |
| `status` | string | `"completed"` for successful job callbacks. |
| `task_type` | string | Job type metadata. Match callbacks to requests using `task_id`. |
| `result` | object | Completed job result data. |
| `error` | string \| null | Null on success. Retrieve job failures through `/job-status/{task_id}`. |

## Source 2: Workflow `outgoing_webhook` block

Add an `outgoing_webhook` block to a workflow to POST the full run result to an external URL when the workflow completes. One request per workflow run, not per row.

### Block specs

| Field | Type | Default | Description |
| - | - | - | - |
| `url` | string | required | Destination URL. Validated against SSRF rules — must be a public host. |
| `headers` | object | none | Optional HTTP headers to include in the request. Header names are sanitized (CR/LF stripped). |
| `timeout_seconds` | float | `10` | Per-attempt request timeout. Range `0 < x ≤ 300`. Internally capped at 150s to fit the activity heartbeat window. |
| `include_csv_download_link` | boolean | `false` | If true, waits up to 120s for the previous block's CSV result and includes a signed `results_download_url` in the payload. |

The block also adds three columns to the workflow output dataset: `webhook_status_code`, `webhook_response`, and `webhook_success`.

### Payload envelope

```json theme={null}
{
  "event_id": "f7c2a4e0-9b5d-4a1f-8d2c-1e3b5f7a9c0d",
  "event": "outgoing_webhook.completed",
  "status": "success",
  "workflow_id": "wf_abc123",
  "run_id": "run_xyz789",
  "organization_id": "org_456",
  "row_count": 95,
  "previous_block_id": "blk_2",
  "previous_block_name": "lead_enrichment",
  "previous_block_number": 2,
  "completed_at": "2026-04-22T20:00:00Z",
  "duration_ms": 1234,
  "attempt": 1,
  "results_download_url": "https://...signed-csv-url...",
  "results": [
    { "name": "John Doe", "email": "john@acme.com" }
  ]
}
```

### Payload fields

| Field | Type | Description |
| - | - | - |
| `event_id` | string (UUID) | Unique per delivery. Use as your dedupe key — same as the `Sixtyfour-Event-Id` header. |
| `event` | string | Always `outgoing_webhook.completed`. |
| `status` | string | Always `success` for a delivered webhook. Failure is signaled by HTTP status, not this field. |
| `workflow_id` | string | The workflow definition ID. |
| `run_id` | string | The specific run ID. |
| `organization_id` | string | Your organization ID. |
| `row_count` | integer | Number of rows in the result, including rows that failed at an earlier step. |
| `previous_block_id` | string | ID of the block immediately before the webhook block. |
| `previous_block_name` | string | Block type of the previous block (e.g. `lead_enrichment`). |
| `previous_block_number` | integer | Sequence number of the previous block. |
| `completed_at` | string (ISO 8601 Z) | When the webhook was sent. |
| `duration_ms` | integer | How long the webhook block took. |
| `attempt` | integer | Delivery attempt counter (starts at 1, increments on retry). |
| `results_download_url` | string \| null | Signed CSV download URL when `include_csv_download_link=true`. Expires per signing policy. |
| `results` | array \| null | Inline result rows. Present when the result is small enough to embed; otherwise null and `results_download_url` carries the data. |

### Failed rows in the payload

The webhook block receives every row, including rows that failed at an earlier step. A failed row has `Failed` in the columns its failing step would have filled, and no values from the steps after it. The `Status` column described in [Failed rows](/api-reference/workflows/workflows-execution#failed-rows) is added to the saved final result, not to this payload.

The payload has no per-row outcome field, and an ordinary data value can also be `Failed`, so do not rely on that text or on `row_count` to tell which rows failed. To identify individual failed rows, wait for the run to reach a terminal status and read the outcome column in the final result ([Download results](/api-reference/workflows/workflows-execution#download-results)). Its name is `Status`, or `Workflow status` (possibly with a number) if those names are already taken, as described in [Failed rows](/api-reference/workflows/workflows-execution#failed-rows). When every row fails, the run ends as `failed` rather than `completed`, but its saved results can still be downloaded, so fetch them for `failed` runs too. The run's `failed_row_count` and `failed_rows` metrics provide aggregate counts, not the identities of failed rows.

## Shared delivery infrastructure

Everything below applies to **both** sources above.

### Headers

When your org has a signing secret configured, every delivery includes:

| Header | Value |
| - | - |
| `Sixtyfour-Signature` | `t=<unix_seconds>,v1=<hex>[,v1=<hex>]` |
| `Sixtyfour-Event-Id` | UUID per delivery — use as your dedupe key |
| `Sixtyfour-Event-Type` | Routing hint (e.g., `find_email`, `outgoing_webhook.completed`) |
| `Sixtyfour-Delivery-Attempt` | Attempt counter starting at `1`; increments on each retry |

Until you create a signing secret, deliveries are sent unsigned for backward compatibility.

### Signing and verification

One signing secret per organization covers all outgoing webhook traffic — async-job webhooks and `outgoing_webhook` blocks alike. Generate it in the dashboard at [Settings → Webhooks](https://app.sixtyfour.ai/settings/webhooks).

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

### Retry behavior

If your endpoint is unavailable or returns a non-2xx status code, we retry delivery with exponential backoff:

| Setting | Value |
| - | - |
| Attempts | Up to 5 attempts per delivery (1 initial attempt and 4 retries) |
| Backoff | 1s, 2s, 4s, 8s between attempts |
| Timeout | 10 seconds per attempt (async-job webhooks); configurable per `outgoing_webhook` block via `timeout_seconds` |

If all retries fail, the webhook is marked as undelivered. The job or workflow itself remains successful and results can still be retrieved via the polling endpoints.

### Best practices

* Return a 2xx status code within 10 seconds. Process the payload asynchronously if needed — returning non-2xx triggers retries even for events you intend to ignore.
* Dedupe with `Sixtyfour-Event-Id`. Retries can cause the same event to arrive more than once, so treat the event ID as your idempotency key.
* Use HTTPS endpoints for payload security. The `outgoing_webhook` block enforces this through its URL validation.
* Match incoming deliveries against the `task_id` or `run_id` you initiated and drop unexpected payloads silently with a 2xx response.

## Fallback: polling

Webhooks are optional. You can always poll for results if:

* Your infrastructure doesn't support inbound webhooks
* Webhook delivery fails after all retries
* You need to retrieve results at a later time

For async jobs:

```bash theme={null}
GET /job-status/{task_id}
```

For workflow runs, see the status and download endpoints in [Workflow Execution](/api-reference/workflows/workflows-execution).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.