# Guide to Wiza Webhooks: Features and Best Practices

Wiza webhooks deliver contact data when it's ready. You ask Wiza to find the work email and other contact details behind a LinkedIn profile, and instead of polling for the result, Wiza sends it to your endpoint when the reveal finishes or fails, ready for your CRM, outreach tool or enrichment pipeline.

This guide covers how Wiza webhooks are configured, the payload, what the `x-auth-key` header is and how to check it, Wiza's retry behaviour, and the practices that keep a Wiza receiver dependable.

## What are Wiza webhooks?

Wiza enrichment is asynchronous. You start an Individual Reveal through the API, and "when a request finishes (or fails), Wiza sends a `POST` request to your configured URL with the enrichment results". The URL is either the default set for your API key, or a `callback_url` you pass on the request.

Wiza's documentation calls its check "Verifying Webhook Signatures", but nothing is computed over the body. Every request carries an `x-auth-key` header "containing a SHA256 hash of your API key". Wiza's samples produce it as lowercase hex. The value is the same on every request until your API key changes, so it works like a fixed shared credential: your receiver computes the hash once and compares the header with it.

## Wiza webhook features

| Feature | Details |
| --- | --- |
| Default URL | **Settings > API**, applies to every request made with that key |
| Per-request URL | `callback_url` on the Start Individual Reveal request |
| All activity | **Send All Callbacks** covers enrichment activity outside the API too |
| Method and format | `POST` with `Content-Type: application/json` |
| Event identifier | `type`, documented as `individual_reveal`, with `status.code` |
| Authentication | `x-auth-key`: the lowercase hex SHA-256 of your API key. No body signature |
| Acknowledgement | Return `200` |
| Retries | Up to 3 for non-2xx responses. Consistent `404` or `403` stops retries for that request |
| Source IPs | Not published |

## Common events

Wiza documents one webhook type, `individual_reveal`, sent when a reveal finishes or fails. `status.code` tells you which:

| `status.code` | `status.message` | Meaning |
| --- | --- | --- |
| `200` | `Single reveal finished` | The reveal finished. `data` holds the result |
| `400` | `Single reveal failed` | The reveal failed. `data.fail_error` gives the reason |

Here is Wiza's example of a finished reveal, trimmed:

```json
{
  "status": { "code": 200, "message": "Single reveal finished" },
  "type": "individual_reveal",
  "data": {
    "id": 123456,
    "status": "finished",
    "is_complete": true,
    "name": "Stephen Hakami",
    "company": "Wiza",
    "enrichment_level": "partial",
    "linkedin_profile_url": "https://www.linkedin.com/in/stephen-hakami-5babb21b0/",
    "title": "Founder, Chief Executive Officer",
    "email": "stephen@wiza.co",
    "email_type": "work",
    "email_status": "valid",
    "emails": [{ "email": "stephen@wiza.co", "email_type": "work", "email_status": "valid" }],
    "credits": { "email_credits": 1, "phone_credits": 0, "export_credits": 0 }
  }
}

```

`data` contains the same fields as the Individual Reveal GET endpoint, and `data.id` is the reveal's ID, the key that ties the webhook back to the request you made. With **Send All Callbacks** on, Wiza sends webhooks for all enrichment activity, but only the `individual_reveal` payload is documented, so log and acknowledge any other `type` you receive.

## Setting up Wiza webhooks

1. In Wiza, go to **Settings > API** and enter your HTTPS endpoint as the default webhook URL. Turn on **Send All Callbacks** if you want activity from outside the API too.
2. Compute the lowercase hex SHA-256 of your API key, which is what `x-auth-key` will contain:
  
  
  ```bash
  printf %s "$WIZA_API_KEY" | sha256sum | cut -d' ' -f1
  
  ```
  
  
3. To send a single request elsewhere, pass `callback_url` when you start the reveal:
  
  
  ```json
  {
    "individual_reveal": {
      "profile_url": "https://www.linkedin.com/in/stephen-hakami-5babb21b0/"
    },
    "enrichment_level": "partial",
    "callback_url": "https://example.com/webhooks/wiza"
  }
  
  ```
  
  

**In Hookdeck.** Create a source with the Wiza type and use its URL as the webhook URL or `callback_url`. In **Hashed API Key**, enter the lowercase hex SHA-256 hash of your Wiza API key, as sent in `x-auth-key`, not the API key itself. Hookdeck compares `x-auth-key` with that value exactly, so an uppercase hash or the raw key won't match. The source accepts `POST`. If you rotate your Wiza API key, compute the new hash and update the source at the same time.

For local development, `hookdeck listen 3000 wiza --path /webhooks/wiza` with the [Hookdeck CLI](/docs/cli) gives you a public HTTPS URL, with no account required.

## Securing Wiza webhooks

Hash your API key once at startup, then compare `x-auth-key` with it in constant time on each request. Wiza's own Node sample passes the buffers straight to `crypto.timingSafeEqual`, which throws when the lengths differ, so check lengths first.

```javascript
const crypto = require('crypto');
const express = require('express');

const app = express();

const apiKey = process.env.WIZA_API_KEY;
const expectedHash = apiKey
  ? crypto.createHash('sha256').update(apiKey, 'utf8').digest('hex')
  : null;

function safeEqual(received, expected) {
  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  // timingSafeEqual throws on buffers of different lengths
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/wiza', express.raw({ type: '*/*' }), (req, res) => {
  if (!expectedHash) return res.sendStatus(500); // fail closed when unconfigured

  const received = req.get('x-auth-key');
  if (typeof received !== 'string' || !safeEqual(received, expectedHash)) {
    return res.sendStatus(401);
  }

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  // Queue the result keyed on payload.data.id. status.code 200 = finished, 400 = failed.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac
import os

def verify_wiza_request(headers) -> bool:
    api_key = os.environ.get("WIZA_API_KEY")
    if not api_key:
        return False  # fail closed
    received = headers.get("x-auth-key")
    if not received:
        return False
    expected = hashlib.sha256(api_key.encode("utf-8")).hexdigest()
    return hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8"))

```

Return `401`, not `403` or `404`, when the header doesn't match. Wiza stops retrying requests that consistently get `404` or `403`, which you don't want for a misconfiguration you're about to fix.

A matching header shows the sender knows a value derived from your API key. The hash doesn't reveal the key, but it's the same on every request and covers nothing in the body, so a captured request can be replayed or have its body changed and still pass. Use HTTPS and keep the hash out of logs. Rotating the API key is the only way to change it.

> **Make Wiza webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks `x-auth-key` on every Wiza webhook, answers with a `200` straight away, and keeps every reveal result for replay.

## Wiza webhook limitations and pain points

### Three retries, then the result is gone

**The Problem:** If your endpoint is down for longer than Wiza's retries take, reveal results never arrive.

**Why It Happens:** "If your webhook endpoint returns a non-2xx response, Wiza will retry up to 3 times." Wiza doesn't publish the interval, and a consistent `404` or `403` stops retries early.

**Workarounds:**

* Acknowledge with `200` immediately and process in the background, as Wiza recommends.
* Poll the Individual Reveal GET endpoint for reveals you started but never heard back about.

**How Hookdeck Can Help:** Hookdeck accepts each Wiza webhook as it arrives, so the three retries only have to reach Hookdeck. Hookdeck then [retries](/docs/retries) delivery to your service on your own schedule and lets you replay any result.

### A key-derived credential, not a signature

**The Problem:** Anyone who sees one request's `x-auth-key` can send convincing fake results, and you can't change the value without rotating your API key.

**Why It Happens:** The header is a hash of the API key and nothing else. It's identical on every request.

**Workarounds:**

* Serve the endpoint over HTTPS only and keep headers out of logs.
* Rotate the API key if the hash may have leaked, and update the expected hash at the same time.
* For results that drive important actions, fetch the reveal from the API by `data.id` before acting.

**How Hookdeck Can Help:** Hookdeck's Wiza source compares `x-auth-key` with the hash you enter. Hookdeck answers Wiza with a `200` either way, then records a request without the right header as failed verification, so it never reaches your destination.

### Failed reveals arrive as webhooks too

**The Problem:** A handler that assumes every webhook carries an email address breaks on failures.

**Why It Happens:** Failed reveals are delivered with `status.code` `400` and a much smaller `data` object with `fail_error`.

**Workarounds:**

* Branch on `status.code` before reading `data`.
* Record failures against the reveal ID so you don't retry them blindly.

**How Hookdeck Can Help:** Hookdeck [filters](/docs/filters) can route finished and failed reveals to different destinations based on `status.code`.

## Best practices

### Compute the hash once

Hash the API key at startup, compare in constant time, and fail closed if the key isn't configured.

### Deduplicate on `data.id`

Retries can deliver the same result more than once. Store the reveal ID with its outcome and skip repeats. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Use `callback_url` to route by workflow

Different pipelines can pass different callback URLs, or query parameters on one URL, so each result lands in the right place without inspecting the body.

### Acknowledge fast and process asynchronously

Wiza asks for a quick `200`. Verify, store, respond, then write to your CRM. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Wiza sends a JSON `POST` when a reveal finishes or fails, with `x-auth-key`, the lowercase hex SHA-256 of your API key, as the only credential. Hash the key once and compare in constant time over HTTPS, branch on `status.code`, deduplicate on `data.id`, and acknowledge quickly, because Wiza gives up after three retries.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks `x-auth-key`, accepts each result straight away, and keeps it until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Wiza webhooks reliably in minutes.