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:
{
"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
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.
Compute the lowercase hex SHA-256 of your API key, which is what
x-auth-keywill contain:printf %s "$WIZA_API_KEY" | sha256sum | cut -d' ' -f1To send a single request elsewhere, pass
callback_urlwhen you start the reveal:{ "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 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.
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:
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 checks
x-auth-keyon every Wiza webhook, answers with a200straight 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
200immediately 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 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.idbefore 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.codebefore readingdata. - Record failures against the reveal ID so you don't retry them blindly.
How Hookdeck Can Help: Hookdeck 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.
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.
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 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 for free and handle Wiza webhooks reliably in minutes.