Developers → Webhooks
NameOcean posts a signed JSON event to your HTTPS endpoint when something happens on your account, so your code can react instead of polling.
In Dashboard → Webhooks, with the API, or by asking your AI over MCP (create_webhook). Choose event types, groups such as dns.*, or all events. The answer includes the signing secret (whsec_…).
$ curl -X POST https://nameocean.net/api/v1/webhooks \ -H "Authorization: Bearer $NAMEOCEAN_API_KEY" -H "Content-Type: application/json" \ -d '{"url": "https://example.com/hooks/nameocean", "events": ["dns.*", "ssl.issued", "domain.expiring"]}'
The URL must be https on a public server. Answer with any 2xx within 10 seconds; do slow work after answering.
Content-Type: application/json
X-NameOcean-Event: dns.record.created
X-NameOcean-Delivery: whd_7c1e0f9a2b3d4e5f6a7b8c9d
X-NameOcean-Signature: t=1794300000,v1=5f2b…
{
"id": "evt_3a9f1c2e4b5d6a7c8e9f0a1b",
"type": "dns.record.created",
"created": "2026-11-10T09:20:00+00:00",
"data": {
"zone": "example.com",
"record": {"id": 4211, "type": "A", "hostname": "www", "ip": "203.0.113.10", "ttl": "1h"}
}
}
The event id stays the same when a delivery is retried or resent: use it to skip events you have already handled. Events can arrive out of order.
t is the Unix time of the request, v1 the hex HMAC-SHA256 of t, a dot and the raw body, keyed with your secret. Compute it over the raw bytes, before parsing the JSON, compare in constant time, and refuse timestamps older than five minutes.
t=1794300000,v1=hex(HMAC_SHA256(secret, "1794300000." + raw_body))
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
fields = dict(part.split("=", 1) for part in header.split(","))
ts = int(fields["t"])
if abs(time.time() - ts) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, fields.get("v1", ""))
import crypto from "node:crypto";
export function verify(secret, header, rawBody, tolerance = 300) {
const fields = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const ts = Number(fields.t);
if (Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
const expected = crypto.createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("hex");
const got = Buffer.from(fields.v1 || "", "hex");
return got.length === 32 && crypto.timingSafeEqual(Buffer.from(expected, "hex"), got);
}
func Verify(secret, header string, body []byte) bool {
var ts int64
var v1 string
for _, part := range strings.Split(header, ",") {
k, v, _ := strings.Cut(part, "=")
switch k {
case "t":
ts, _ = strconv.ParseInt(v, 10, 64)
case "v1":
v1 = v
}
}
if d := time.Now().Unix() - ts; d > 300 || d < -300 {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
fmt.Fprintf(mac, "%d.", ts)
mac.Write(body)
return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(v1))
}
A delivery that gets no answer, a timeout, a 5xx, 408 or 429 is retried after 1m, 5m, 30m, 2h, 5h, 10h, 10h: 8 attempts over about a day. Any other 4xx fails at once. After 5 deliveries in a row fail for good, the endpoint is turned off and you get an email; turn it back on in the dashboard or with PATCH active: true. Every delivery, with the status code and the start of your answer, is listed in the dashboard and at GET /api/v1/webhooks/{id}/deliveries, where you can resend one.
| domain.registered | A domain was registered in your account. |
| domain.renewed | A domain's expiry date moved forward (renewal). |
| domain.expiring | A domain expires in 30, 7 or 1 days. |
| domain.expired | A domain expired yesterday. |
| domain.nameservers.updated | A domain's nameservers changed. |
| domain.transfer.started | A transfer in or out was requested at the registry. |
| domain.transfer.completed | A transfer finished. |
| domain.transfer.failed | A transfer was rejected or cancelled. |
| dns.zone.created | A DNS zone was created. |
| dns.zone.deleted | A DNS zone was deleted. |
| dns.record.created | A DNS record was added. |
| dns.record.updated | A DNS record changed. |
| dns.record.deleted | A DNS record was deleted. |
| ssl.issued | A certificate was issued for the first time. |
| ssl.renewed | A certificate was renewed. |
| ssl.expiring | A certificate expires in 7 days and has not been renewed yet. |
| email.mailbox.created | A mailbox was added to one of your mail domains. |
| email.mailbox.deleted | A mailbox and its mail were deleted. |
| email.forwarding.enabled | Email forwarding was turned on for a domain. |
| email.forwarding.disabled | Email forwarding was turned off for a domain, by you or because the domain stopped qualifying (the reason is in the event). |
| webhook.test | Sent when you press Send test; ignores the endpoint's event filter. |
data carries the domain or zone and the object: the record (with previous on updates), the expiry date, the nameservers (with previous). The exact fields of each object are those of the REST API.
Incoming email can be posted to a URL too: set it per address in Email Forwarding. Those posts use the same headers and signature, with X-NameOcean-Event: email.received.