Webhook payload format
Corrections: support@domainvane.com
Webhooks depend on your plan. Solo includes one generic JSON webhook. Agency and Agency+ include the generic JSON webhook and also a Slack-compatible webhook. Free has no webhook. Domainvane posts each alert to the URLs you save, alongside the alert email.
There is no public API and there are no API keys in this version. Webhooks are outbound only. Domainvane calls your URL, and you never call Domainvane.
Set up a webhook
- Sign in and open Webhooks in the dashboard menu (
/settings/webhooks). - Paste your endpoint URL and choose Save. Agency and Agency+ accounts get a second form for the Slack-compatible URL.
- Choose Send test. Domainvane posts the sample body shown below and tells you whether your endpoint accepted it or why it failed.
- Copy the signing secret from the same page into your receiver so it can verify each request.
Remove stops posts to that URL and cancels any retry still waiting for it. Regenerate secret creates a new secret that is used from the next post on, so update your receiver first. The page shows only the start of a saved URL, because webhook URLs often contain a token.
Delivery rules
- Each alert sends one HTTPS
POSTto each saved webhook, withContent-Type: application/json,User-Agent: Domainvane-Webhook/1.0, and anX-Domainvane-Signatureheader. - The URL must use
httpson port 443 and must not contain a username or password. The host is checked against private and reserved addresses when you save the URL and again on every post. The URL is stored encrypted. - Your endpoint must present a valid, publicly trusted TLS certificate for its hostname.
- Any 2xx response counts as delivered.
- Redirects are not followed. A 3xx response counts as a failed delivery and is not retried. Save the final URL.
- A 4xx response is a failure and is not retried.
- A 5xx response, a timeout, a DNS failure, or a connection failure gets one retry about 60 seconds later. The pending retry is stored, so a Domainvane restart delays it, usually by about a minute, instead of dropping it. It sends the same body with a new signature timestamp. It goes to the URL saved at retry time. If you remove the webhook, turn it off, or move to a plan without it, the retry is not sent. There is no second retry, with one exception: if Domainvane stops in the middle of sending a retry, that retry can be sent again after the restart. Make your receiver tolerate duplicates, for example by ignoring a repeat of the same
type,hostname,incident_id, andrecovery. - By default Domainvane waits 5 seconds for the connection and 8 seconds for the response. A response body over 1 MB also counts as a failure.
- Every webhook post counts toward a service-wide hourly limit on outbound webhook requests. That includes alerts, retries, and tests. If the limit is reached, the post is recorded as failed and is not retried. Send test is also limited to 5 tests per account every 10 minutes.
- Domainvane's logs and delivery records keep the webhook's host name, not the full URL.
- A channel your plan does not include is not contacted. After a downgrade, the saved URL is kept encrypted but not used.
Verify the signature
Every post, including the Slack-compatible one and Send test, carries:
X-Domainvane-Signature: t=<unix seconds>,v1=<64 hex characters>
t is the send time in Unix seconds. v1 is the lowercase hex HMAC-SHA256 of t, a period, and the raw request body, keyed with your signing secret (the whole string, including the dvwh_ prefix). To verify a request:
- Read the raw body before you parse it as JSON.
- Compute the HMAC as described above and compare it to
v1in constant time. - Reject the request if
tis more than a few minutes from your clock. This blocks replays.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyDomainvane(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(String(header).split(',').map((p) => p.split('=')));
const expected = createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds;
return fresh && typeof parts.v1 === 'string' && parts.v1.length === expected.length
&& timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}
When a webhook is sent
The same alerts that are emailed are posted:
- Expiry thresholds at 30, 14, 7, and 1 days before the certificate's
notAfteror the RDAP expiration date, usingfloor((expiry − now) / 86400000)in UTC. An unknown RDAP result has no date, so it sends no domain-expiry alert. - Failures after 3 consecutive failed checks of the same kind (TLS or RDAP).
- One recovery notice when that incident clears.
While a condition stays true, the same alert can repeat after its 24-hour cooldown.
Generic JSON body
One object per alert. Here is a TLS certificate at the 14-day threshold:
{
"type": "tls_expiry",
"hostname": "www.example.com",
"port": 443,
"threshold_days": 14,
"incident_id": 3,
"recovery": false,
"subject": "TLS certificate expiry alert: www.example.com:443",
"body": "www.example.com:443 has reached the 14 day TLS certificate expiry threshold.",
"expires_at": "2026-11-16T15:00:00.000Z",
"days_left": 14,
"status": "ok",
"checked_at": "2026-11-02T09:00:00.000Z",
"dashboard_url": "https://domainvane.com/dashboard"
}
| Field | Meaning |
|---|---|
type | tls_expiry, domain_expiry, tls_failure, or domain_failure. On a recovery notice, the type of the condition that cleared. test for Send test. |
hostname | The monitored hostname, normalized (lowercase, punycode). |
port | 443 in this version. |
threshold_days | 30, 14, 7, or 1 for an expiry alert and for the recovery of one. null for a failure alert and for the recovery of one. |
incident_id | Integer. It advances after the condition clears, so a repeat after recovery is a new incident. |
recovery | true on a recovery notice, otherwise false. |
subject | The same subject line as the alert email. |
body | The same text as the alert email. |
expires_at | ISO 8601 UTC time from the check that raised the alert. For tls_* types it is the certificate's notAfter. For domain_* types it is the RDAP expiration date. null when that check had no date, for example after a TLS timeout or an unknown RDAP result. |
days_left | floor((expires_at − checked_at) / 86400000), or null when expires_at is null. |
status | For tls_* types: ok, or the TLS status code from that check (such as TIMEOUT, EXPIRED, or CHAIN_INCOMPLETE). For domain_* types: ok, unknown, or error. test for Send test. |
checked_at | ISO 8601 UTC time of the check that raised the alert. |
dashboard_url | Link to your Domainvane dashboard. |
The first eight fields are unchanged from the earlier body, and the newer fields are added after them. A receiver written for the eight-field body keeps working. Ignore fields you don't recognize, because new fields may be added.
Failure after three consecutive TLS problems:
{
"type": "tls_failure",
"hostname": "www.example.com",
"port": 443,
"threshold_days": null,
"incident_id": 4,
"recovery": false,
"subject": "TLS check failure: www.example.com:443",
"body": "www.example.com:443 has reached the configured consecutive TLS check failure limit.",
"expires_at": null,
"days_left": null,
"status": "TIMEOUT",
"checked_at": "2026-11-03T03:00:00.000Z",
"dashboard_url": "https://domainvane.com/dashboard"
}
A domain-registration lookup failure uses domain_failure and the words RDAP check failure.
Recovery notice for that TLS incident, after a renewed certificate:
{
"type": "tls_failure",
"hostname": "www.example.com",
"port": 443,
"threshold_days": null,
"incident_id": 4,
"recovery": true,
"subject": "Recovered: www.example.com:443",
"body": "www.example.com:443 recovered from tls_failure.",
"expires_at": "2027-02-01T09:00:00.000Z",
"days_left": 90,
"status": "ok",
"checked_at": "2026-11-03T09:00:00.000Z",
"dashboard_url": "https://domainvane.com/dashboard"
}
Slack-compatible body
Agency and Agency+ only. Slack-compatible incoming webhooks accept a JSON object with a text field. Domainvane sends only that field: the subject, a newline, then the body.
{
"text": "TLS certificate expiry alert: www.example.com:443\nwww.example.com:443 has reached the 14 day TLS certificate expiry threshold."
}
Domainvane does not install a Slack app. You paste an incoming-webhook URL.
Test body
Send test posts this body to the generic webhook. It has the same fields as an alert and uses type and status test, so your receiver can tell it apart from a real alert. The Slack-compatible test sends subject and body as text, the same way an alert does.
{
"type": "test",
"hostname": "example.com",
"port": 443,
"threshold_days": null,
"incident_id": 0,
"recovery": false,
"subject": "Domainvane test notification",
"body": "This is a test from your Domainvane webhook settings. No certificate or domain changed.",
"expires_at": null,
"days_left": null,
"status": "test",
"checked_at": "2026-11-02T09:00:00.000Z",
"dashboard_url": "https://domainvane.com/dashboard"
}