Guides

Recipes

Copy-paste solutions to the things teams build most: hosting a site on your subdomain, calling Itoka from your app, dynamic DNS, preview environments, wildcard certificates and webhooks.

Point your subdomain at Vercel, Netlify or Render

yourteam.itoka.dev is the apex of your zone, and a CNAME can't sit at an apex. Use the host's A record for @ and a CNAME for www. For Vercel there's a preset that does both:

await itoka.presets.apply('yourteam.itoka.dev', 'vercel'); // A @ 76.76.21.21, CNAME www cname.vercel-dns.com

For Netlify, Render or anything else, copy the A record (apex) and CNAME (www) values the host shows when you add the custom domain, then add them as records. Add the domain in the host's dashboard too, so it issues the HTTPS certificate: .dev sites only open over HTTPS.

Call Itoka from your app (not from the browser)

Your API key can change your DNS, so it must stay on a server. The API also refuses requests from other websites' browsers (403 bad_origin). Call it from your backend or a serverless function, and have your frontend call that.

Next.js: app/api/records/route.ts
import { Itoka } from '@skilledcrow/itoka';

const itoka = new Itoka(); // reads ITOKA_API_KEY from the server's environment

export async function POST(req: Request) {
  const { name, ip } = await req.json();
  // Check who's asking before changing DNS for them.
  const { record } = await itoka.records.upsert('yourteam.itoka.dev', { type: 'A', name, value: ip, ttl: 60 });
  return Response.json({ record: record.display });
}

Dynamic DNS for a home server

Keep home.yourteam.itoka.dev pointing at a machine whose public IP changes. Run it every five minutes from cron; it only writes when the IP changed. The key needs Read records and Write records.

// ddns.mjs — node ddns.mjs (cron: */5 * * * *)
import { Itoka } from '@skilledcrow/itoka';

const zone = 'yourteam.itoka.dev', name = 'home';
const itoka = new Itoka();
const ip = (await (await fetch('https://api.ipify.org')).text()).trim();
const current = (await itoka.records.list(zone)).find((r) => r.type === 'A' && r.name === name);
if (current?.value !== ip) {
  await itoka.records.upsert(zone, { type: 'A', name, value: ip, ttl: 60 });
  console.log(`${name}.${zone} -> ${ip}`);
}

A preview name for every pull request

Give each PR pr-123.yourteam.itoka.dev, pointing at its preview deployment, and remove it when the PR closes. Store the key as the repository secret ITOKA_API_KEY.

.github/workflows/preview-dns.yml
on:
  pull_request:
    types: [opened, synchronize, closed]
jobs:
  dns:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm install @skilledcrow/itoka
      - env:
          ITOKA_API_KEY: ${{ secrets.ITOKA_API_KEY }}
          NAME: pr-${{ github.event.number }}
          TARGET: your-preview-host.example.com   # where the preview is deployed
          CLOSED: ${{ github.event.action == 'closed' }}
        run: |
          node --input-type=module -e '
          import { Itoka } from "@skilledcrow/itoka";
          const itoka = new Itoka(), zone = "yourteam.itoka.dev", name = process.env.NAME;
          if (process.env.CLOSED === "true") {
            for (const r of (await itoka.records.list(zone)).filter((r) => r.name === name)) await itoka.records.delete(zone, r.id);
          } else {
            await itoka.records.upsert(zone, { type: "CNAME", name, value: process.env.TARGET, ttl: 60 });
          }'

A wildcard certificate with certbot

Make a key with only DNS-01 challenges: it can add and remove _acme-challenge TXT records and nothing else, so a leaked key can't touch your other records. These hooks need curl and jq.

itoka-auth.sh
#!/bin/sh
# certbot sets CERTBOT_DOMAIN and CERTBOT_VALIDATION
ZONE=yourteam.itoka.dev
NAME="_acme-challenge.$CERTBOT_DOMAIN"   # full names are shortened to the zone for you
curl -sf "https://api.itoka.ng/api/v1/zones/$ZONE/records" \
  -H "Authorization: Bearer $ITOKA_API_KEY" -H "Content-Type: application/json" \
  -d "{\"record\": {\"type\": \"TXT\", \"name\": \"$NAME\", \"value\": \"$CERTBOT_VALIDATION\", \"ttl\": 60}}" \
  | jq -r .record.id > "/tmp/itoka-$CERTBOT_VALIDATION"
itoka-cleanup.sh
#!/bin/sh
ZONE=yourteam.itoka.dev
curl -sf -X DELETE "https://api.itoka.ng/api/v1/zones/$ZONE/records/$(cat "/tmp/itoka-$CERTBOT_VALIDATION")" \
  -H "Authorization: Bearer $ITOKA_API_KEY" > /dev/null
rm -f "/tmp/itoka-$CERTBOT_VALIDATION"
sh
certbot certonly --manual --preferred-challenges dns \
  --manual-auth-hook ./itoka-auth.sh --manual-cleanup-hook ./itoka-cleanup.sh \
  -d 'yourteam.itoka.dev' -d '*.yourteam.itoka.dev'

Receive webhooks in Next.js

Verify the signature on the raw body before trusting anything in it. See Webhooks for the event list and the check in other languages.

app/api/itoka/route.ts
import { verifyWebhook, WebhookVerificationError } from '@skilledcrow/itoka';

export async function POST(req: Request) {
  try {
    const event = await verifyWebhook({
      payload: await req.text(), // the raw body, not parsed JSON
      signature: req.headers.get('Itoka-Signature'),
      secret: process.env.ITOKA_WEBHOOK_SECRET!,
    });
    if (event.type === 'zone.records_changed') console.log(event.data);
    return new Response(null, { status: 204 });
  } catch (e) {
    if (e instanceof WebhookVerificationError) return new Response(e.message, { status: 400 });
    throw e;
  }
}