Integration
Two halves. The key stays on your server, and the page only gets a token.
One call from your server creates a setup. One script on your page lets your customer finish it. A signed webhook tells you when the domain is live.
// Your server. The API key is a secret and stays here.// Your dashboard's Integration page fills in API_URL.const res = await fetch(API_URL + "/v1/dns-sessions", { method: "POST", headers: { "X-Api-Key": process.env.RELAYDNS_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ domain: "app.customer.com", records: [ { type: "CNAME", name: "@", value: "origin.yourapp.com" }, ], }),}); const { widgetToken } = await res.json();// Send widgetToken to the browser. Never the API key.The API
Three endpoints. One key.
Send your project's API key as X-Api-Key or as Authorization: Bearer. Keys begin rdns_, are shown once, and belong to one project, so every setup you create lands there. Errors come back as { "error": "…" } with a plain sentence.
POST/v1/dns-sessions
Create a setup. Returns 202 with the widgetToken.
GET/v1/dns-sessions/:id
Status, attempts and each record's verification.
DELETE/v1/dns-sessions/:id
Remove a setup. Returns 204.
Create a setup
POST /v1/dns-sessions. Returns before DNS is touched.
- domainrequiredstringThe full domain being set up, e.g. app.customer.com.
- recordsrequiredarray, 1 to 50What the domain needs. Names are relative to domain; "@" is the domain itself.
- records[].typerequiredstringA, AAAA, CNAME, TXT, MX, NS, SRV or CAA.
- records[].valuerequiredstringWhere it points, or what it holds.
- records[].ttlinteger60 to 604800 seconds. Defaults to 3600.
- records[].priorityintegerMX and SRV only. Defaults to 10.
- maxVerificationAttemptsinteger1 to 100. Defaults to 12.
- widgetTokenTtlMinutesinteger5 to 1440. Defaults to 60.
- widgetAllowedOriginURLPins the widget token to your app's origin.
// 202 Accepted
{
"id": "…",
"domain": "app.customer.com",
"status": "created",
"expiresAt": "…",
"widgetToken": "rdws_…",
"widgetTokenExpiresAt": "…"
}The widget
One script. It works out the rest.
Load API_URL/widget/v1/relaydns.js and call RelayDNS.open(options). It renders inside a shadow root, so it cannot pick up or disturb your styles, and returns close and refresh.
- tokenrequiredstringThe widgetToken your server received.
- productNamestringYour product's name, used in the widget's heading.
- brandingbooleanfalse hides the Secured by RelayDNS line.
- onSuccess(state)functionThe records answer. The domain is live.
- onStatusChange(status, state)functionEvery status change, as it happens.
- onClose()functionYour customer closed the widget.
- onError(error)functionThe setup could not be loaded, for example once the token has expired.
Webhooks
Told, not polled. Every delivery is signed.
dns/setup.requested
A setup was created.
dns/verification.completed
Every record answers. The domain is live.
dns/verification.failed
Attempts ran out, or the setup expired first.
Signed with HMAC-SHA256 over timestamp.body, sent as RelayDNS-Signature: t=…,v1=…. Reject anything older than five minutes.
A delivery that does not get a 2xx within ten seconds is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Redirects are not followed.
{
"event": "dns/verification.completed",
"sentAt": "…",
"data": {
"dnsSessionId": "…",
"domain": "app.customer.com",
"status": "verified",
"verifiedAt": "…",
"expiresAt": "…"
}
}Verifying a delivery
A few lines. The shape you know from Stripe.
The secret begins whsec_ and is shown once, when you add the endpoint. Verify against the raw body: parsing it first changes the bytes.
import { createHmac, timingSafeEqual } from "node:crypto";
// header: the RelayDNS-Signature value, "t=<seconds>,v1=<hex>"
// body: the raw request body, before any JSON parsing
export function verify(body, header, secret) {
const parts = Object.fromEntries(
header.split(",").map((p) => p.trim().split("=")),
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${t}.${body}`, "utf8")
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}Get started
Try it on one domain. Nothing is charged until one connects in one click.
- 1Create an accountAn email and a password
- 2Make a project and an API keyThe key is shown once
- 3Connect your first domainOne call from your server
- The account and the key take about a minute.