How it works
- You add an endpoint: an
https://address that you control. - An event happens, for example a newly listed domain matches one of your alert rules.
- We send a
POSTwith a JSON body and a signature header, usually within a minute. - Your endpoint answers with any
2xxstatus. Anything else, or no answer within 8 seconds, counts as a failure and is retried.
Webhooks are part of Pro. You can have up to 3 endpoints.
Set up
Add the endpoint on the API page of your account, or through the API:
curl -X POST "https://hunter.domains/api/v1/webhooks" \
-H "Authorization: Bearer hd_…" -H "Content-Type: application/json" \
-d '{"url": "https://hooks.example.com/hunter", "events": ["alert.matched", "domain.available"]}'
The response contains the signing secret (whsec_…). It is shown once; store it next to your other secrets. Then press "Test" on the account page, or call POST /webhooks/{id}/test, and a ping event arrives at your endpoint.
The address must be https:// on a public host name (no IP addresses, no user name or password in the URL), on port 443 or a port above 1023. Redirects are not followed.
To receive alert.matched you also need at least one active alert rule.
Events
type | When it is sent |
|---|---|
alert.matched | A newly listed domain matched one of your alert rules. Sent at once, whatever the rule’s email frequency. |
domain.available | A domain you watch, or one that matches a rule, dropped and nobody registered it. |
domain.ending_soon | A watched or matched domain is about to end. Fires at the reminder hours chosen in your notification settings. |
check.completed | A domain check you ordered finished: status is done or failed. |
ping | A test event, sent when you press “Test” or call the test endpoint. |
Payload
Every event has the same envelope: an id that is unique to the event, the type, the time and the data.
{
"id": "evt_01k6x2f0q8m3r7t9v4b5n6c8dz",
"type": "alert.matched",
"created_at": "2026-10-06T16:44:02Z",
"data": {
"alert": {
"id": 12,
"name": "Health, DR 30+"
},
"domain": {
"domain": "example.org",
"tld": "org",
"status": "listed",
"score": 78,
"verdict": "ok",
"flags": [],
"topic": "Home",
"language": "en",
"title": "Seasonal planting guides for small gardens",
"metrics": {
"domain_rating": 46,
"referring_domains": 412,
"archive_years": 17,
"archive_first_year": 2006,
"archive_last_year": 2025,
"trust_flow": 28,
"citation_flow": 31
},
"listing": {
"source": "godaddy",
"type": "bid",
"price_usd": 25,
"bids": 3,
"ends_at": "2026-10-09T17:30:00Z",
"url": "https://auctions.godaddy.com/trpItemListing.aspx?domain=example.org"
},
"found_at": "2026-10-06T16:42:10Z",
"open": true,
"detail": "full",
"report_url": "https://hunter.domains/domain/example.org"
}
}
}
The domain object is the same as in the API. Fetch GET /domains/{domain} when you need the full report.
A domain that dropped unregistered:
{
"id": "evt_01k6x2f0q8m3r7t9v4b5n6c8dz",
"type": "domain.available",
"created_at": "2026-10-06T16:44:02Z",
"data": {
"domain": {
"domain": "example.org",
"tld": "org",
"status": "available",
"score": 78,
"verdict": "ok",
"flags": [],
"topic": "Home",
"language": "en",
"title": "Seasonal planting guides for small gardens",
"metrics": {
"domain_rating": 46,
"referring_domains": 412,
"archive_years": 17,
"archive_first_year": 2006,
"archive_last_year": 2025,
"trust_flow": 28,
"citation_flow": 31
},
"listing": {
"source": "godaddy",
"type": "bid",
"price_usd": 25,
"bids": 3,
"ends_at": "2026-10-09T17:30:00Z",
"url": "https://auctions.godaddy.com/trpItemListing.aspx?domain=example.org"
},
"found_at": "2026-10-06T16:42:10Z",
"open": true,
"detail": "full",
"report_url": "https://hunter.domains/domain/example.org"
},
"registration": {
"availability": "available",
"checked_at": "2026-10-06T16:40:00Z",
"available_since": "2026-10-06T16:40:00Z",
"register_url": "https://www.spaceship.com/domain-search/?query=example.org",
"registrar": null,
"registered_at": null,
"expires_at": null,
"caught_after_drop": false
}
}
}
A finished check carries the summary; the report itself is at GET /checks/{id}:
{
"id": "evt_01k6x2f0q8m3r7t9v4b5n6c8dz",
"type": "check.completed",
"created_at": "2026-10-06T16:44:02Z",
"data": {
"check": {
"id": "01k6x2f0q8m3r7t9v4b5n6c8dz",
"domain": "example.org",
"status": "done",
"created_at": "2026-10-06T17:02:11Z",
"finished_at": "2026-10-06T17:03:24Z",
"score": 78,
"verdict": "ok",
"report_url": "https://hunter.domains/domain-checker/report/01k6x2f0q8m3r7t9v4b5n6c8dz"
}
}
}
Each request also carries these headers:
| Header | Value |
|---|---|
Hunter-Event |
The event type, for example alert.matched. |
Hunter-Delivery |
The event id. The same id is sent again on a retry. |
Hunter-Signature |
t=<unix time>,v1=<signature>. |
User-Agent |
hunter.domains-webhooks/1. |
Verifying the signature
Anyone who learns your URL can post to it, so check the signature before you trust a request.
The signature is the HMAC-SHA256 of <t>.<raw body> with your signing secret as the key, written in hex. To verify:
- Read
tandv1from theHunter-Signatureheader. - Compute HMAC-SHA256 over the string
t, a dot, and the raw request body exactly as received (before any JSON parsing). - Compare your result with
v1using a constant-time comparison. - Reject the request if
tis more than five minutes old; that stops someone replaying a captured request.
Node.js:
import crypto from 'node:crypto';
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds;
const given = Buffer.from(parts.v1 ?? '');
return fresh && given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected));
}
Python:
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
signed = parts.get("t", "").encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts.get("t", 0))) <= tolerance
return fresh and hmac.compare_digest(expected, parts.get("v1", ""))
PHP:
function verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', ($parts['t'] ?? '').'.'.$rawBody, $secret);
return abs(time() - (int) ($parts['t'] ?? 0)) <= $tolerance
&& hash_equals($expected, (string) ($parts['v1'] ?? ''));
}
Retries and failures
- A delivery is tried up to 6 times: at once, then after about 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours.
- Answer quickly with a
2xxand do the slow work afterwards. A request that takes longer than 8 seconds is treated as failed, even if your code finishes later. - The same event can arrive more than once, for example when your
2xxwas lost on the way. Use the eventidto ignore repeats. - Events can arrive out of order. Use
created_at, not the order of arrival. - When 15 deliveries in a row have failed after all their retries, we switch the endpoint off and email you. Switch it back on from the account page or with
PATCH /webhooks/{id}and{"active": true}. - The last 50 deliveries of an endpoint, with status and response code, are on the account page and at
GET /webhooks/{id}/deliveries. They are kept for 14 days.
Sending events to other tools
- n8n, Make, Zapier, Pipedream: create a webhook trigger in the tool, paste its URL as your endpoint, and build the rest of the flow there: a Slack message, a row in a sheet, a task.
- Slack and Discord directly: their incoming webhooks expect their own body format, so our JSON will be rejected. Put one of the tools above, or a few lines of your own code, in between.
- Your own server: verify the signature, answer
200, then process the event from a queue.
Frequently asked questions
Can I get expired domain alerts by webhook?
Yes. Create an alert rule with the filters you want (niche, language, Domain Rating, price, extension), add a webhook endpoint, and every newly listed domain that matches the rule is posted to your endpoint as an alert.matched event.
How fast are webhooks?
Usually within a minute of the event. A domain is matched against alert rules as soon as its review finishes during the daily scan.
Are webhooks available on the free plan?
No. Webhooks and alert rules are part of Pro. On the free plan you can poll the API instead.
What happens if my server is down?
The delivery is retried for about nine hours. If your server comes back in that time, nothing is lost. After that the delivery is marked as failed; you can still read what you missed from GET /alerts/matches.
Why did my endpoint get switched off?
15 deliveries in a row failed after all their retries. Look at the response codes in the delivery list, fix the endpoint, and switch it back on.
Updated: 6 October 2026