Developers → Webhooks

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.

Add an endpoint

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
$ 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.

What you receive

POST https://example.com/hooks/nameocean
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.

Verify the signature

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.

X-NameOcean-Signature
t=1794300000,v1=hex(HMAC_SHA256(secret, "1794300000." + raw_body))
Python
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", ""))
Node.js
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);
}
Go
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))
}

Retries and turning off

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.

Events

domain.registeredA domain was registered in your account.
domain.renewedA domain's expiry date moved forward (renewal).
domain.expiringA domain expires in 30, 7 or 1 days.
domain.expiredA domain expired yesterday.
domain.nameservers.updatedA domain's nameservers changed.
domain.transfer.startedA transfer in or out was requested at the registry.
domain.transfer.completedA transfer finished.
domain.transfer.failedA transfer was rejected or cancelled.
dns.zone.createdA DNS zone was created.
dns.zone.deletedA DNS zone was deleted.
dns.record.createdA DNS record was added.
dns.record.updatedA DNS record changed.
dns.record.deletedA DNS record was deleted.
ssl.issuedA certificate was issued for the first time.
ssl.renewedA certificate was renewed.
ssl.expiringA certificate expires in 7 days and has not been renewed yet.
email.mailbox.createdA mailbox was added to one of your mail domains.
email.mailbox.deletedA mailbox and its mail were deleted.
email.forwarding.enabledEmail forwarding was turned on for a domain.
email.forwarding.disabledEmail forwarding was turned off for a domain, by you or because the domain stopped qualifying (the reason is in the event).
webhook.testSent 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.

Good to know

  • NameOcean only posts to public addresses, checked again on every delivery, and never follows redirects.
  • Rotate the secret in the dashboard if it leaks; events are signed with the new one at once.
  • Send test posts a webhook.test event right away, whatever the endpoint's event filter.