Architecture & Capabilities

DynIP is a private, multi-tenant authoritative DNS edge platform. It allows hardware firewalls, routers, and edge compute nodes to securely report IP changes using standard HTTP REST protocols or native DNS updates.

Dual-Stack Native

Automatically detects IPv4 and IPv6 traffic, intelligently routing updates to the correct A or AAAA records side-by-side.

Enterprise Security

DNS record updates are authenticated by RFC 2136 TSIG (Transaction Signature) keys over both HTTP /update and native DNS UPDATE, using HMAC-SHA256 by default.

Authentication

DynIP utilizes two distinct authentication mechanisms depending on the endpoint you are accessing:

  • TSIG Keys: Used for DNS record updates — over HTTP /update and native RFC 2136 DNS UPDATE (nsupdate, external-dns, cert-manager) — and to gate AXFR-out. These are 44-character Base64 encoded strings generated per-zone (and per BYOD base, for apex updates). HMAC-SHA256 by default.
  • JWT Bearer Tokens: Used for account management and programmatic zone registration (/register). Generated upon user login.

TSIG Algorithms

DynIP supports two TSIG algorithms for signing dynamic DNS updates: HMAC-SHA256 (the default) and HMAC-MD5 (FortiGate compatibility).

HMAC-SHA256 is the modern standard and the default for new zones. It is required by external-dns, cert-manager, and Proxmox VE's ACME plugin, and is supported by every modern RFC 2136 client we've tested — BIND nsupdate, MikroTik, OPNsense, pfSense, Caddy, Traefik, and most others.

HMAC-MD5 exists for compatibility with FortiGate's genericDDNS implementation, which is fixed to MD5 and cannot use SHA-256. If you're using a FortiGate, switch your zone's algorithm to MD5 in the dashboard before configuring the device.

Switching algorithms. Each zone has its own algorithm setting, configurable in the dashboard via the zone's Options modal. Changing the algorithm does not regenerate the TSIG secret — your existing secret continues to work under either algorithm; only the algorithm value changes.

Standards and Transport

DynIP runs a set of geographically distributed authoritative nameservers, speaking standard DNS over UDP and TCP on port 53. All secondaries answer queries on both IPv4 and IPv6, with AAAA glue published in the parent .dev zone for end-to-end IPv6 resolution including from IPv6-only networks.

TSIG-signed DNS UPDATEs (RFC 2136) arrive at update.dynip.dev, which resolves to A and AAAA records on all secondaries. The secondaries verify the TSIG signature locally before the update is applied. DNSSEC is signed and auto-published on ddns.dynip.dev and ddns.thelost.tech, enabled per zone from the dashboard. BYOD namespaces can be signed too — when the parent zone is hosted elsewhere, you publish the returned DS record at your own provider.

Customer zones can publish A records, AAAA records, or both — dual-stack and IPv6-only deployments are handled as a first-class case, not an afterthought.

DynIP accepts updates over three protocols: the dyndns2 HTTP convention (inadyn, ddclient, routers, UniFi), RFC 2136 DNS UPDATE over TSIG (external-dns, cert-manager, Proxmox VE, lego, certbot, nsupdate), and the native JSON API. See Protocols DynIP speaks for which clients use each and how the platform chooses a response format.

DNSSEC Security (Chain of Trust)

DynIP provides fully automated, zero-touch DNSSEC to protect your domains from DNS spoofing and man-in-the-middle attacks. When you enable DNSSEC via the dashboard, the platform establishes a seamless cryptographic "Chain of Trust" from the global internet root down to your dynamic edge device.

How Automated DNSSEC Works

  1. Key Generation: The platform generates unique Key Signing Keys (KSK) and Zone Signing Keys (ZSK) for your specific subdomain.
  2. Cryptographic Signing: PowerDNS instantly begins injecting RRSIG signatures over your A and AAAA records on-the-fly.
  3. Automated DS Injection: The backend extracts your Delegation Signer (DS) record and seamlessly injects it into our parent base zone, officially completing the global Chain of Trust.

Validation: Once enabled, you can verify your domain's authenticity using standard resolver tools like dig +dnssec your-domain.ddns.dynip.dev @1.1.1.1. Look for the ad (Authentic Data) flag in the header.

BYOD zones with an external parent

Automated DS injection only works when DynIP serves the parent zone (community bases, or a BYOD base whose own parent you also host with us). When you delegate a subdomain like ddns.yourcompany.com to DynIP from a parent that lives elsewhere (Cloudflare, Route53, registrar DNS), we can't publish the DS for you — there's no parent zone here to write it into.

In that case, enabling DNSSEC from your namespace card signs the zone and returns the DS record for you to publish at your provider on the parent zone (yourcompany.com). The chain of trust activates once that DS propagates. Disabling DNSSEC removes the signing keys cleanly; remember to also delete the DS record at your parent.

Available Base Zones

All registered users have immediate access to provision subdomains under our globally distributed, authoritative base zones. These domains are provided free of charge for community use:

🌐

ddns.dynip.dev

Flagship brand zone. General-purpose default.

DNSSEC
⚡

dyn.amic.name

Short, memorable alternate.

🛡️

ddns.thelost.tech

General-purpose, DNSSEC-signed.

DNSSEC
📞

phone-home.org

Alternate zone.

DNSSEC is signed and auto-published on ddns.dynip.dev and ddns.thelost.tech. You can also enable DNSSEC on a verified BYOD namespace — if its parent zone lives off-platform, you publish the returned DS record at your own provider. Let's Encrypt DNS-01 certificate issuance is available for all community zones.

Custom Domains (CNAME)

You do not need to migrate your entire enterprise DNS zone to DynIP to use our platform. You can easily route your own custom domains (e.g., vpn.yourcompany.com) to your dynamic edge devices using a simple CNAME record.

How to configure:

  1. Register a DynIP zone for your device (e.g., branch-office.ddns.dynip.dev).
  2. Set up your router or firewall to update that DynIP zone using our API or native TSIG integration.
  3. In your own registrar (Cloudflare, AWS Route53, GoDaddy), create a CNAME record pointing to your DynIP zone:
vpn.yourcompany.com. IN CNAME branch-office.ddns.dynip.dev.
🛡️

DNSSEC Compatibility: This architecture is 100% DNSSEC compatible. Global resolvers will independently authenticate the cryptographic signature of your custom domain's CNAME record, and then seamlessly authenticate the dynamic A/AAAA target records provided by the DynIP platform.

Custom Namespaces (BYOD)

Organizations can bring their own custom base domains (e.g., ddns.yourcompany.com) directly to the DynIP platform. Unlike a CNAME, which maps a single device, a BYOD namespace lets you dynamically provision unlimited subdomains under your corporate branding directly from the DynIP dashboard.

Proving ownership and making records resolve publicly are two separate steps. You can prove ownership in one of two ways — NS delegation or a TXT record — and a domain can be validated but not delegated: usable on the platform (TSIG key issued, RFC 2136 / API updates accepted, snippet generator enabled) even though records don't resolve on the public internet until you delegate nameservers.

Option A — Delegate nameservers (recommended):

Proves ownership and makes records resolve publicly in one step. Choose this whenever your registrar lets you set NS records on a subdomain.

  1. In the DynIP dashboard, open Custom Namespaces (BYOD) and register your desired base domain (e.g., ddns.yourcompany.com).
  2. Pick Delegate nameservers.
  3. Log into the registrar where yourcompany.com is managed and create an NS (Name Server) record delegating the ddns subdomain to the DynIP authoritative servers:
ddns.yourcompany.com. IN NS ns1.dynip.dev.
ddns.yourcompany.com. IN NS ns2.dynip.dev.

Both NS records are required. Single-NS delegation is rejected during verification. Click Verify Setup in the dashboard once propagated.

Option B — Verify with a TXT record:

Some registrars (e.g. Hover, Namecheap, GoDaddy) refuse ns1/ns2.dynip.dev as a delegation target because they require registry-level glue that doesn't exist for our nameservers. If your registrar rejects the NS records, prove ownership with a TXT record instead:

  1. Register the base domain in Custom Namespaces (BYOD) and pick Verify with a TXT record.
  2. Click Generate verification token. The dashboard shows a token of the form dynip-verify=… (valid for 7 days).
  3. At your registrar, add a TXT record:
_dynip-verify.ddns.yourcompany.com. IN TXT "dynip-verify=<your-token>"

Then click Validate now. On success the zone is provisioned and becomes fully usable on the platform. If the token expires before you validate, click Regenerate token for a fresh one.

Validated, not delegated: TXT validation proves ownership only. Your records will not resolve on the public internet, and SSL certificate issuance is unavailable (Let's Encrypt's DNS-01 challenge needs the public DNS chain to reach our nameservers), until you also delegate ns1/ns2.dynip.dev at your registrar (Option A). Until then you can still issue TSIG keys and push updates via RFC 2136 / the API — public resolution and SSL are the only things blocked, and that's outside our control until delegation lands. Use Check delegation status in the dashboard to flip the zone to Active once you've delegated.

Manual Validation

You can verify your NS delegation has propagated globally by querying Cloudflare's public DNS-over-HTTPS (DoH) API. Run this in your terminal:

curl -s -H "Accept: application/dns-json" "https://cloudflare-dns.com/dns-query?name=ddns.yourcompany.com&type=NS"

A successful delegation will return a JSON object with "Status": 0 and an "Answer" block containing both ns1.dynip.dev. and ns2.dynip.dev. If only one appears, verification will fail.

Apex DDNS & base-zone controls

A verified namespace gets its own per-base TSIG key (key-<base>), separate from the per-zone keys of the subdomains under it. Open the base's Options modal in the dashboard to reveal the key, switch its TSIG algorithm (HMAC-SHA256 ⇄ HMAC-MD5), and copy a ready-made nsupdate example. This key authorizes RFC 2136 updates to the apex (the base name itself) and any record in the zone, and also grants external AXFR-out.

nsupdate -y hmac-sha256:key-ddns.yourcompany.com:<secret> << EOF server update.dynip.dev zone ddns.yourcompany.com update add ddns.yourcompany.com 60 A 192.0.2.1 send EOF

You can also sign the base zone with DNSSEC from its namespace card — see DNSSEC Security. If the base's own parent lives off-platform, you publish the returned DS record yourself.

Automated SSL/TLS Certificates Let's Encrypt

DynIP features native integration with Let's Encrypt, allowing you to generate valid, trusted SSL/TLS certificates for any of your registered DDNS subdomains directly from the dashboard. Including BYOD.

SSL requires delegation, not just validation. The DNS-01 challenge can only succeed if the domain is actually delegated to ns1/ns2.dynip.dev so Let's Encrypt can find the challenge record in the public DNS chain. A BYOD zone that's validated via TXT but not delegated can use the platform's API / RFC 2136 update flow, but its SSL controls stay disabled until you delegate. TXT validation can still be your activation path — delegating afterward additionally unlocks SSL.

How It Works (The DNS-01 Challenge)

Traditional ACME clients require you to temporarily open port 80 (HTTP-01 challenge) or change your DNS records to point to a validation server, which can disrupt edge traffic. Because DynIP acts as your authoritative nameserver, we utilize the highly robust DNS-01 Challenge.

When you request a certificate, DynIP temporarily injects an _acme-challenge TXT record directly into your DNS zone. Let's Encrypt queries this record to verify domain ownership. Once verified, the certificate is issued and the TXT record is safely deleted—without ever touching your actual IP routing.

Usage Potential

This feature is ideal for securing services that sit behind carrier-grade NAT, or for administrators who refuse to expose port 80 to the public internet. Common uses include securing reverse proxies (Traefik, Nginx Proxy Manager), hardware firewall admin panels (pfSense, FortiGate), or internal self-hosted applications (Nextcloud, Plex) that require trusted HTTPS access.

How to Use the Bundled `.pem` File

When you click "Download" in the dashboard, you receive a unified .pem file. For convenience, this single file bundles both your Private Key and your Full Certificate Chain.

Splitting the File (If Required by Your Hardware):

While modern load balancers like HAProxy accept a unified bundle, some appliances (like pfSense or standard Apache setups) require you to upload the Private Key and the Certificate Chain separately. You can easily split the file using any basic text editor (like Notepad or nano).

1. The Private Key (Save as privkey.pem)

Copy everything from the top of the file, including these exact headers:

-----BEGIN PRIVATE KEY-----
(your encoded key data here)
-----END PRIVATE KEY-----
2. The Certificate Chain (Save as fullchain.pem)

Copy the remaining sections (there will likely be two of these blocks back-to-back—one for your domain, and one for the Let's Encrypt Root Authority):

-----BEGIN CERTIFICATE-----
(your encoded cert data here)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
(root authority data here)
-----END CERTIFICATE-----

DNS-PERSIST-01 Preview

Preview feature — the record format is not settled. DNS-PERSIST-01 is a new ACME challenge type still in IETF draft. Let's Encrypt runs it in staging only, and has not announced a production date. DynIP can publish the persistent authorization record today, but end-to-end issuance against LE production is not yet possible.

An open spec issue (#64) proposes adding a client-computed value to the record to close a meddler-in-the-middle weakness. If it lands as written, the TXT record contents change and anything you publish now will need to be republished. Treat this section as a way to experiment, not as a head start.

What it is

DNS-PERSIST-01 replaces the per-issuance DNS-01 challenge with a one-shot TXT record at _validation-persist.<your-zone> that names an ACME account and CA. Once published, the CA can re-validate the record on every cert request and renewal without the client needing to write any new DNS records. Let's Encrypt's announcement post explains the design.

For DynIP users this means: configure the persistent record once via the dashboard's Snippets → DNS-PERSIST-01 generator, then your ACME client never needs DynIP credentials for renewals — only its own ACME account key.

Current rollout status

DynIP's authoritative DNS layer accepts and serves _validation-persist records today via the standard RFC 2136 TSIG path. The dashboard's snippet generator produces the right nsupdate command for your zone.

Upstream status

  • Let's Encrypt — staging accepts the challenge type. Production has no announced date: the February 2026 announcement targeted Q2 2026, and LE has since said it will not deploy until spec issue #64 is resolved (community thread, June 2026).
  • IETF draft-ietf-acme-dns-persist-01 — the protocol specification, published 23 March 2026 and unchanged since. Still a working-group document; it has not advanced toward IESG review.
  • Issue #64 — the blocker. A record built only from CA-supplied values lets an attacker replay it; the fix adds a value computed from the ACME client's own public key, changing what goes in the TXT record.
  • ACME clients: lego v5+ supports it via --dns-persist. certbot and cert-manager don't have it yet.

Finding or creating your ACME account URI

The snippet asks for your ACME account URI — the unique identifier ACME CAs assign to your account. It looks like https://acme-v02.api.letsencrypt.org/acme/acct/12345678. The URI is just an identifier; the security of the account lives in the local key file that signs cert requests.

If you have an existing Let's Encrypt account (via certbot, lego, acme.sh, or any other client), the URI is recorded alongside your account key:

  • certbot: certbot show_account prints the URI under Account URL.
  • lego: ~/.lego/accounts/<ca-host>/<email>/account.json holds it under registration.accountURL.
  • acme.sh: ~/.acme.sh/ca/<ca-host>/directory/ca.conf contains it as ACCOUNT_URL.
  • cert-manager: kubectl get clusterissuer <name> -o jsonpath='{.status.acme.uri}'.

A lego account.json looks like this — the value you want is registration.accountURL:

{ "email": "[email protected]", "keyType": "EC256", "server": "https://acme-staging-v02.api.letsencrypt.org/directory", "registration": { "status": "valid", "accountURL": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/12345678" } }

If you don't have an account yet, register one without issuing a certificate, then read the URL straight out of the file lego writes:

lego accounts register \ --email [email protected] \ --server letsencrypt-staging \ --accept-tos # Then read the account URL from the account.json lego created: cat ~/.lego/accounts/acme-staging-v02.api.letsencrypt.org/[email protected]/account.json | jq -r '.registration.accountURL'

Staging and production are separate ACMEs; their accounts are distinct. Use the production server URL (letsencrypt as the shortcode, or the full URL https://acme-v02.api.letsencrypt.org/directory) if and when production enables the challenge and you're ready to switch.

Publishing the record

Open the dashboard's Snippets → DNS-PERSIST-01 generator, paste your account URI, and run the rendered nsupdate command. The record is a single TXT entry at _validation-persist.<your-zone>:

_validation-persist.your-zone.ddns.dynip.dev. 3600 IN TXT "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/12345678"

TTL is 3600 (1 hour) by default — the record is meant to persist, so caching it is fine. You can publish multiple records (different accounts, different CAs) by repeating the same command with different values; the spec supports a many-to-one mapping of accounts to zones.

This is the draft-01 record shape: a CA identifier plus an account URI, both supplied by the CA. Spec issue #64 proposes adding a value derived from your ACME client's public key, which would change this record. Expect to republish.

End-to-end with lego (preview)

Once the record is in place, request a certificate using lego v5+. Lego's --dns-persist flag tells it to use the DNS-PERSIST-01 challenge instead of the regular DNS-01 challenge (so it doesn't need a DNS provider configured at all):

lego run \ --server letsencrypt-staging \ --email [email protected] \ --accept-tos \ --domains your-zone.ddns.dynip.dev \ --dns-persist

If LE production enables the challenge, swap --server letsencrypt-staging for --server letsencrypt (or the full production URL). Your account URI differs between staging and production, so the TXT record has to be republished with the production URI either way.

Removing the authorization

To revoke a persistent authorization, delete the TXT record. Future cert requests from that account will fall back to the regular DNS-01 challenge (or fail, depending on your ACME client's configuration):

nsupdate -y 'hmac-sha256:key-your-zone.ddns.dynip.dev:YOUR_TSIG_KEY' <<EOF server update.dynip.dev zone your-zone.ddns.dynip.dev update delete _validation-persist.your-zone.ddns.dynip.dev. TXT send EOF

IP Change Notifications

Per-zone email alerts that fire whenever a zone's A or AAAA record actually changes. Useful for catching unexpected DDNS updates from a compromised router, monitoring whether a residential connection is stable, or keeping a lightweight audit trail of who changed what.

Enabling notifications

Each zone row in the dashboard has a NOTIFY button next to the snippets and certificate controls. The button is gray when notifications are off (the default) and green when on. Click it to toggle — the change applies immediately, with no save step.

What's in the email

Notifications are delivered to the email address registered on your account. Each message contains:

  • The hostname of the zone that changed
  • The previous IP and the new IP
  • A UTC timestamp for when the change was applied
  • The update method — HTTP /update, native DNS UPDATE (RFC 2136), or the admin dashboard
  • The source IP that submitted the request

Rate limiting

A maximum of one notification is sent per zone per 5 minutes. If your IP genuinely flaps faster than that, intermediate changes are silently dropped — only the most recent IP, the one you'd actually see in DNS, triggers an email once the cooldown elapses.

DDNS Update API

Per-device runtime endpoints — what a router or edge device calls to discover its public IP and keep its DNS record fresh. Authenticated by per-zone TSIG keys.

GET /ip

A simple discovery endpoint that returns the public IP address of the requesting client as seen by the server. Useful for any client — whether behind NAT, on a dual-stack connection, or directly addressable — that needs to know which IP a remote service sees it from. Returns whichever family the request arrived on; use /ip4 or /ip6 to require a specific family.

curl https://api.dynip.dev/ip

# Response (Plain Text): 203.0.113.42

GET /ip4

Returns the client's IPv4 address as plain text. Requires the request to arrive over IPv4 — if the connection was IPv6, the endpoint returns HTTP 400 pointing at /ip6 instead. v4-mapped-v6 sources (::ffff:1.2.3.4) are unwrapped and returned as the underlying IPv4. Use this when you want to publish an A record specifically, or when discovering both families with two separate calls.

curl https://api.dynip.dev/ip4

# Response (Plain Text): 203.0.113.42

# If the request arrives over IPv6:

curl --ipv6 https://api.dynip.dev/ip4

# HTTP 400: This request arrived over IPv6. Use /ip6 instead, or retry over IPv4 transport.

GET /ip6

Returns the client's IPv6 address as plain text. Requires the request to arrive over IPv6 — if the connection was IPv4 (including v4-mapped-v6), the endpoint returns HTTP 400 pointing at /ip4 instead. Use this when you want to publish an AAAA record specifically, or to confirm a host has working public v6 connectivity.

curl https://api.dynip.dev/ip6

# Response (Plain Text): 2001:db8::1234

# If the request arrives over IPv4:

curl --ipv4 https://api.dynip.dev/ip6

# HTTP 400: This request arrived over IPv4. Use /ip4 instead, or retry over IPv6 transport.

GET POST /update

Updates the A or AAAA record for a specific zone. The record type is automatically determined based on the format of the IP address provided. The same three fields are accepted two ways: as URL query parameters on GET, or as a JSON body on POST.

ParameterRequiredDescription
domainYesFull FQDN (e.g. router.ddns.dynip.dev)
ipYesValid IPv4 or IPv6 string
keyYesBase64 TSIG Key

# GET — query parameters (URL-encode the key):

curl "https://api.dynip.dev/update?domain=router.ddns.dynip.dev&ip=203.0.113.42&key=YOUR_URL_ENCODED_KEY"

# POST — JSON body (no URL encoding needed):

curl -X POST https://api.dynip.dev/update \

-H "Content-Type: application/json" \

-d '{"domain": "router.ddns.dynip.dev", "ip": "203.0.113.42", "key": "YOUR_BASE64_KEY"}'

Rate limit: 60 requests per minute per source IP (both forms share the limit). Devices polling more aggressively than once per second will be throttled with HTTP 429.

Zone Management API

Getting started

All endpoints in this section operate on resources tied to your account, so they require a JWT bearer token in the Authorization header (the same token used for /register). The token is issued at login via /auth/login and is also embedded in the dashboard session.

Authorization: Bearer YOUR_JWT_TOKEN

Ownership is enforced server-side: any attempt to read, modify, or delete a zone or BYOD namespace that does not belong to the authenticated user returns HTTP 403. The single unauthenticated endpoint in this group is GET /base-domains, which lists publicly available base zones.

GET /base-domains

Public, unauthenticated. Returns the list of community base zones available to all accounts. Useful as a discovery endpoint for CLI tooling that wants to populate a dropdown of valid base_domain values for /register without requiring a login.

curl https://api.dynip.dev/base-domains

# Response (JSON):

[

{"domain": "ddns.dynip.dev", "deprecated": false},

{"domain": "ddns.thelost.tech","deprecated": false},

{"domain": "dyn.amic.name", "deprecated": false},

{"domain": "phone-home.org", "deprecated": false}

]

If you want a combined list including your own verified BYOD namespaces, use the authenticated GET /user/base-domains endpoint below.

Zone lifecycle

POST /register

Programmatically registers a new zone to your account. This requires an active JWT session token passed in the Authorization header.

ParameterTypeRequiredDescription
AuthorizationHeaderYesBearer {token}
subdomainQueryYesThe desired prefix (e.g. 'my-router')
base_domainQueryYesA valid base zone (e.g. 'ddns.dynip.dev')

curl -X POST "https://api.dynip.dev/register?subdomain=my-router&base_domain=ddns.dynip.dev" \

-H "Authorization: Bearer YOUR_JWT_TOKEN"

# Response (JSON):

{

"status": "success",

"domain": "my-router.ddns.dynip.dev",

"key": "GENERATED_BASE64_TSIG_KEY"

}

Zone quota: Free accounts can register up to 5 zones. Exceeding the quota returns HTTP 403 with "Domain limit reached". See /pricing for higher tiers.

GET /list

Returns all zones owned by the authenticated user, including current A/AAAA values, the TSIG secret, certificate status, and the last sync timestamp. last_sync reflects both REST /update calls and native RFC 2136 updates.

curl https://api.dynip.dev/list \

-H "Authorization: Bearer YOUR_JWT_TOKEN"

# Response (JSON):

[

{

"name": "my-router.ddns.dynip.dev.",

"ip": "203.0.113.42 / 2001:db8::1",

"key": "BASE64_TSIG_KEY",

"cert_status": "active",

"last_sync": "2026-05-02T10:14:33Z",

"deprecated_base": false

}

]

cert_status is one of none, active, or expired; certificate rows also carry cert_expires (ISO timestamp) and cert_days_remaining (negative once expired). deprecated_base is true if the parent base zone has been retired and the zone should be migrated. Each row additionally includes the zone's tsig_algorithm and TSIG propagation state (tsig_ready_at, is_tsig_ready).

DELETE /delete

Permanently removes a zone, its TSIG key, and any issued Let's Encrypt certificate material. The action cascades through PowerDNS, the user_domains table, and the certbot live/archive tree. There is no soft-delete or undo.

ParameterTypeRequiredDescription
AuthorizationHeaderYesBearer {token}
domainQueryYesFull FQDN to delete (e.g. my-router.ddns.dynip.dev)

curl -X DELETE "https://api.dynip.dev/delete?domain=my-router.ddns.dynip.dev" \

-H "Authorization: Bearer YOUR_JWT_TOKEN"

# Response: {"status": "deleted"}

Zone configuration

DNSSEC Endpoints

Programmatic control over the per-zone DNSSEC state. DNSSEC can only be enabled on zones whose base supports it (ddns.dynip.dev, ddns.thelost.tech, and any verified BYOD namespace you've signed). All three verbs accept the same ?domain= query parameter.

GET /dnssec?domain=...

Returns {"enabled": true|false}.

POST /dnssec?domain=...

Generates KSK + ZSK, begins RRSIG signing, and pushes the DS record into the parent base zone. Idempotent.

DELETE /dnssec?domain=...

Removes the DS from the parent, then strips signing keys from the zone. Resolvers may serve cached signed responses for up to the parent DS TTL.

BYOD base-zone DNSSEC

Sign a verified BYOD namespace itself (rather than a child subdomain). When its parent zone is hosted off-platform, the response carries the DS record(s) for you to publish at your provider. These act on the base by id, not a ?domain=.

GET /user/base-domains/{id}/dnssec

Returns {"enabled", "ds_records", "key_tag", "parent_zone", "external_parent", "ds_published_externally"}.

POST /user/base-domains/{id}/dnssec

Signs the base zone. If the parent is on-platform and signed, the DS is auto-published; otherwise external_parent: true and ds_records are returned for self-publication. 409 if the namespace isn't validated yet.

DELETE /user/base-domains/{id}/dnssec

Strips signing keys and clears the external-publish marker. Delete the DS at your parent separately.

POST /user/base-domains/{id}/dnssec/mark-published

Display-only breadcrumb that you've published the DS at your external parent. Gates nothing functionally.

SSL Certificate API Let's Encrypt

Programmatic equivalent of the dashboard's certificate workflow. Issuance runs certbot with the DNS-01 challenge against your zone — there is a per-server lock on cert generation, so concurrent calls for different zones are serialized rather than run in parallel.

GET /ssl/preflight?domain=...

Reports whether the zone is ready for cert issuance. If the zone is a child of a DNSSEC-signed parent but is itself unsigned, returns needs_dnssec_enable: true — issuance would fail validation in that state.

POST /ssl/generate?domain=...

Triggers a fresh certbot run with --force-renewal. On success the encrypted private key + full chain are stored in the database and cert_expires_at is returned.

If preflight detects an unsigned zone under a signed parent, the call returns HTTP 412 with {"status": "needs_dnssec", ...} instead of attempting issuance.

GET /ssl/download?domain=...

Returns the bundled .pem (private key + full chain) as text/plain with a Content-Disposition: attachment header. Designed for header-auth clients (curl, scripts). The dashboard uses a separate one-shot URL flow so the JWT never appears in a browser URL.

curl -X POST "https://api.dynip.dev/ssl/generate?domain=my-router.ddns.dynip.dev" \

-H "Authorization: Bearer YOUR_JWT_TOKEN"

curl -o my-router.pem "https://api.dynip.dev/ssl/download?domain=my-router.ddns.dynip.dev" \

-H "Authorization: Bearer YOUR_JWT_TOKEN"

BYOD (Bring Your Own Domain)

BYOD Namespace Lifecycle

Programmatic equivalent of the Custom Namespaces (BYOD) dashboard flow. Reservations are two-phase: registering a BYOD domain creates a holding row, but the PowerDNS zone is not provisioned until ownership is proven. Ownership can be proven two ways — NS delegation (/verify) or a TXT record (/txt-token then /validate-txt). The is_verified flag means "ownership proven" regardless of method; delegation_status separately tracks whether the domain actually points at ns1/ns2.dynip.dev. A TXT-validated zone is fully usable (TSIG, RFC 2136 / API updates, snippets) but its records won't resolve publicly while delegation_status is not delegated.

GET /user/base-domains

Lists all base zones visible to the user — community zones plus the user's own BYOD namespaces. Each row includes id, domain, user_id, is_verified, validation_method (ns | txt), delegation_status (unknown | delegated | not_delegated), and deprecated. For your own rows on the TXT path it also returns txt_token and txt_record_name. For your own verified rows it carries the per-base apex key (tsig_secret, tsig_algorithm, tsig_ready_at); these fields are stripped from community and other users' bases.

POST /user/base-domains

Reserves a BYOD domain for 24 hours. JSON body: {"domain": "ddns.yourcompany.com"}.

Returns the id needed to call /verify, plus an expires_at timestamp. After expiry the reservation is garbage-collected and must be re-created.

GET /user/base-domains/{id}/verify

Queries the parent zone's authoritative nameservers directly (non-recursive) and confirms that both ns1.dynip.dev and ns2.dynip.dev appear in the delegation. On success, the PowerDNS zone is provisioned, is_verified flips to true, and delegation_status is set to delegated. Returns {"status": "success" | "failed", "message": "..."}.

POST /user/base-domains/{id}/txt-token

Switches the domain onto the TXT validation path and issues (or rotates) an ownership token of the form dynip-verify=<random>, valid for 7 days. Returns txt_token, txt_record_name (_dynip-verify.<domain>), and expiry timestamps. Publish the token as a TXT record at txt_record_name, then call /validate-txt.

POST /user/base-domains/{id}/validate-txt

Does a fresh lookup of _dynip-verify.<domain> TXT via public resolvers (bypassing caches) and requires a record matching the stored token. On success the zone is provisioned and is_verified flips to true (delegation_status stays as-is — typically unknown until you run a delegation check). Rate-limited to one attempt per 10s per zone (429 otherwise); an expired token returns 410. Failures return an actionable message naming exactly what was looked up and found.

POST /user/base-domains/{id}/check-delegation

On-demand check of whether the domain's NS records point at ns1/ns2.dynip.dev, independent of ownership proof. Updates delegation_status to delegated or not_delegated; a transient lookup failure returns unknown and never demotes a previously-delegated zone. Returns {"status": "...", "delegation_status": "...", "message": "..."}.

POST /user/base-domains/{id}/tsig-algorithm

Switches the per-base apex key between hmac-sha256 and hmac-md5. JSON body: {"algorithm": "hmac-md5"}. The secret is unchanged — only the algorithm flips. Returns {"status", "algorithm", "ready_at", "seconds_until_ready"}; updates are accepted under the new algorithm once it propagates to the secondaries. 409 if the base isn't validated yet. Mirrors the per-zone POST /user/zones/{id}/tsig-algorithm switch.

DELETE /user/base-domains/{id}

Removes the BYOD namespace and (if it was verified) the underlying PowerDNS zone. Child zones already provisioned under it are not auto-deleted — call /delete for each first.

Hardware & Integration Guides

⚡ Automated Snippet Generator

You do not need to construct these requests manually! Once you register a domain in the DynIP Dashboard, simply click the "Snippets" button next to your active zone. The dashboard dynamically generates ready-to-paste configurations for all supported hardware, perfectly pre-filling your exact domain, your secure TSIG key, and handling all URL encoding formats required by different vendors.

Below are the structural templates for how DynIP integrates with various hardware and software platforms. You can configure native DNS updates (RFC 2136) or utilize our custom universal REST endpoint.

AI agents (Claude Code, opencode, Cursor, Open WebUI, Hermes)

The snippet generator emits a per-zone skill or tool definition that lets a coding agent or chat assistant manage the zone on its own. It is instructions plus credentials in the format your agent already reads — not a wrapper CLI and not an MCP server. The agent drives the same public endpoints documented on this page using the tools it already has: curl, nsupdate and dig.

HostArtifactInstall path
Claude CodeAgent Skill~/.claude/skills/dynip-<zone>/SKILL.md
opencodeAgent Skill~/.config/opencode/skills/dynip-<zone>/SKILL.md
CursorProject Rule.cursor/rules/dynip-<zone>.mdc
Open WebUITool (Python)Workspace → Tools
Hermes / OpenAI-shapedFunction signaturesSystem prompt <tools> block

opencode also reads ~/.claude/skills/*/SKILL.md, so the Claude Code install covers both. The Hermes variant is signatures only — your host implements the functions; the Open WebUI tool is a working reference implementation of the same set.

The artifact tells the agent which transport to use for which job, because picking wrong is the common failure. POST /update writes the zone's own A or AAAA record and nothing else; every other name, every other record type, and all deletions go over RFC 2136 with the same per-zone TSIG key; reads go to ns1.dynip.dev via dig. It also carries the operating rules — the 60 requests/minute limit on /update, the TSIG propagation delay that makes nsupdate fail on a brand-new zone, what BADKEY and NOTAUTH mean, and not to overwrite records the user did not ask about.

Ticking Include zone create & delete adds the Zone Management endpoints (/list, /user/base-domains, /register, /delete), so the agent can provision hostnames rather than only edit one. Those authenticate with a Personal Access Token (Pro and above) rather than the zone's TSIG key. The token is shown once at creation and is never embedded in the snippet — the emitted instructions read it from $DYNIP_API_TOKEN. A read-scoped token limits the agent to /list and /user/base-domains; /register and /delete need full.

The generated file contains the zone's TSIG secret in plain text. For Claude Code and opencode it lands under your home directory; Cursor rules live in the repository and are normally committed, so gitignore that file or replace the secret with $DYNIP_TSIG_KEY. Full walkthrough: AI agents guide.

Docker container (recommended for Docker / Kubernetes hosts)

For docker-compose, Kubernetes, or any container runtime, DynIP publishes a small updater image at ghcr.io/33k-org/dynip-updater:latest. The container detects your current IPv4 and IPv6, reconciles against the currently-published DNS record each cycle, and pushes an update to /update when anything diverges. It runs as a non-root user, has no persistent state, exposes a healthcheck, and exits with specific codes on auth/config errors so an orchestrator's restart loop doesn't mask the problem.

Minimal docker-compose.yml snippet:

services:
  dynip-updater:
    image: ghcr.io/33k-org/dynip-updater:latest
    container_name: dynip-updater
    restart: unless-stopped
    environment:
      - DYNIP_DOMAIN=user.ddns.dynip.dev
      - DYNIP_KEY=YOUR_44_CHAR_BASE64_TSIG_SECRET

Environment variables:

Variable Default Description
DYNIP_DOMAINrequiredFull FQDN of the zone to update (e.g. user.ddns.dynip.dev).
DYNIP_KEYrequiredTSIG secret for that zone, from the dashboard.
DYNIP_INTERVAL300Seconds between detection cycles.
DYNIP_ENABLE_V4trueWhether to publish an A record.
DYNIP_ENABLE_V6trueWhether to publish an AAAA record.
DYNIP_HEARTBEAT_INTERVAL86400Seconds. Forces an update even when the IP hasn't moved.
DYNIP_LOG_LEVELINFODEBUG, INFO, WARN, or ERROR.
DYNIP_HEALTHCHECK_PORT9090TCP port for the in-container /healthz endpoint.

Healthcheck: GET :9090/healthz returns 200 ok when the last cycle succeeded within 2 × DYNIP_INTERVAL, or 503 unhealthy otherwise. The image's HEALTHCHECK probes this automatically.

Image tags available on GHCR: :latest, :v1, :v1.1, :v1.1.0. Pin to a major (:v1) for automatic patch/minor updates, or to a full version for reproducible deploys. Worked examples: Netbird, Traefik, Caddy.

external-dns (Kubernetes RFC 2136)

external-dns is a Kubernetes controller that publishes DNS records based on Service and Ingress hostname annotations. DynIP supports it via RFC 2136 (TSIG-authenticated dynamic DNS updates).

The snippet generator in the dashboard produces a complete manifest (ServiceAccount, ClusterRole, ClusterRoleBinding, Deployment) parameterized for your zone — choose Snippets → external-dns (Kubernetes), enter a Cluster Name (used by external-dns to track which records it owns; must be unique per cluster pointing at the same zone), and apply the generated YAML.

Minimal Deployment args (the snippet fills these in for you):

- --provider=rfc2136
- --rfc2136-host=update.dynip.dev
- --rfc2136-port=53
- --rfc2136-zone=user.ddns.dynip.dev
- --rfc2136-tsig-keyname=key-user.ddns.dynip.dev
- --rfc2136-tsig-secret=YOUR_44_CHAR_BASE64_TSIG_SECRET
- --rfc2136-tsig-secret-alg=hmac-sha256
- --domain-filter=user.ddns.dynip.dev
- --registry=txt
- --txt-prefix=_externaldns.
- --txt-owner-id=your-cluster-name
- --policy=upsert-only

Argument reference:

Argument Description
--provider=rfc2136Use the RFC 2136 dynamic update provider.
--rfc2136-hostDynIP's update endpoint. Always update.dynip.dev.
--rfc2136-zoneThe zone external-dns manages.
--rfc2136-tsig-keynameTSIG key name. DynIP convention: key-<your-zone>.
--rfc2136-tsig-secretTSIG secret from the dashboard.
--rfc2136-tsig-secret-algMust be hmac-sha256.
--domain-filterRestricts external-dns to this zone only.
--registry=txtUse TXT records to track ownership.
--txt-owner-idPer-cluster identifier. Must be unique if multiple clusters share a zone.
--policy=upsert-onlyDefault. Creates and updates records; never deletes.

Limitations: per-zone record count and update rate are currently unbounded.

For step-by-step setup, including verifying your zone and deploying to a cluster, see the external-dns guide.

cert-manager (Kubernetes RFC 2136)

cert-manager issues and renews TLS certificates inside Kubernetes. With DynIP it uses the ACME DNS-01 challenge over RFC 2136 to write _acme-challenge TXT records, then Let's Encrypt validates them and signs the cert.

The snippet generator in the dashboard produces three resources (Secret with the TSIG key, ClusterIssuer with the rfc2136 solver, Certificate for your zone) parameterized for you — choose Snippets → cert-manager (Kubernetes), enter a contact email and target namespace, and apply the generated YAML.

Solver config (the snippet fills these in for you):

solvers:
- dns01:
    rfc2136:
      nameserver: update.dynip.dev:53
      tsigKeyName: key-user.ddns.dynip.dev.
      tsigAlgorithm: HMACSHA256
      tsigSecretSecretRef:
        name: dynip-tsig-key-user-ddns-dynip-dev
        key: tsig-secret-key

Configuration reference:

Field Description
nameserverDynIP's update endpoint with explicit port. Always update.dynip.dev:53.
tsigKeyNameTSIG key name with trailing dot. DynIP convention: key-<your-zone>.
tsigAlgorithmMust be HMACSHA256 (cert-manager's spelling, no dashes).
tsigSecretSecretRefReference to a Secret holding the base64 TSIG secret under the tsig-secret-key entry.
Prerequisites: the cert-manager controller must be started with the --dns01-recursive-nameservers-only flag pointing at public resolvers (e.g. 1.1.1.1:53,8.8.8.8:53), otherwise cluster DNS may SERVFAIL during the SOA discovery and propagation polling. The zone must also have DNSSEC enabled if its parent is signed (the platform's ddns.dynip.dev base is signed; child zones must be too, or validating resolvers will SERVFAIL on the challenge name). The cert-manager guide walks through both.

For step-by-step setup, including the controller flag, the DNSSEC requirement, and a working end-to-end example, see the cert-manager guide.

Proxmox VE (ACME via RFC 2136)

Proxmox VE has a built-in ACME client that can issue and renew the Let's Encrypt certificate for its web UI (port 8006) using the DNS-01 challenge. With DynIP it uses the nsupdate DNS plugin to write _acme-challenge TXT records over TSIG-authenticated RFC 2136 UPDATE — no port 80 exposure, works behind CGNAT.

The snippet generator in the dashboard produces a complete, non-interactive setup script parameterized for your zone — choose Snippets → Proxmox VE ACME (RFC 2136), enter a contact email, and paste the script into a root shell on the node. It registers the ACME account, writes the TSIG key file, registers the plugin, and orders the certificate.

Plugin data (the snippet fills these in for you):

pvenode acme plugin add dns dynip-user-ddns-dynip-dev \
    --api nsupdate \
    --data "$DATA_FILE" \
    --validation-delay 60

# where $DATA_FILE contains:
NSUPDATE_KEY=/etc/dynip-user-ddns-dynip-dev.tsig
NSUPDATE_SERVER=update.dynip.dev
NSUPDATE_ZONE=user.ddns.dynip.dev
Prerequisites: the zone must have DNSSEC enabled (the platform's ddns.dynip.dev base is signed; an unsigned child zone makes Let's Encrypt's validating resolvers SERVFAIL on the challenge name) and its TSIG algorithm set to HMAC-SHA256. Keep the TSIG key file in /etc/, not /etc/pve/ — the pmxcfs fuse mount's ownership prevents the ACME nobody user from reading it there.

For step-by-step setup, renewals, multi-node clusters, and the PBS/PMG variants, see the Proxmox VE guide.

Universal cURL API (Manual Setup)

Our custom /update REST endpoint acts as a universal adapter. It's built to handle nearly any edge-case. You can trigger this manually via standard Linux shells, embed it in custom cron scripts, or use it with any generic third-party DDNS client that accepts custom HTTP Webhooks.

curl -X POST "https://api.dynip.dev/update" \
    -H "Content-Type: application/json" \
    -d '{"domain": "YOUR_DOMAIN", "ip": "YOUR_IP", "key": "YOUR_KEY"}'

POST keeps the TSIG key in the request body, out of proxy and CDN access logs, and needs no URL encoding. The GET form with query parameters still works for clients that can only send credentials in the URL — see the /update reference.

nsupdate (RFC 2136)

BIND's nsupdate is the reference RFC 2136 client, and reaches everything /update doesn't: any record type, at any name inside your zone. The snippet generator takes a record type (A, AAAA, TXT, CNAME, MX, SRV or CAA), a name relative to the zone and the record content, then renders one TSIG-signed update with your key and algorithm filled in. Leave an A or AAAA record's content blank to publish the address of the machine running the command.

nsupdate -y 'hmac-sha256:key-YOUR_DOMAIN:YOUR_KEY' <<'EOF'
server update.dynip.dev
zone YOUR_DOMAIN
update delete www.YOUR_DOMAIN. A
update add www.YOUR_DOMAIN. 60 A 203.0.113.42
send
EOF

The delete + add pair replaces the whole record set at that name and type in one atomic update. Like every RFC 2136 client, it fails on a brand-new zone until the TSIG key has reached the secondaries, so the generator disables the option until then. Per-type syntax is in the protocols guide.

Windows PowerShell

Use the native `Invoke-RestMethod` to trigger updates from Windows Server environments. The key travels in the JSON body, so it never lands in a proxy access log and needs no URL encoding.

$body = @{ domain = "YOUR_DOMAIN"; ip = "YOUR_IP"; key = "YOUR_KEY" } | ConvertTo-Json
Invoke-RestMethod -Uri "https://api.dynip.dev/update" -Method Post -ContentType "application/json" -Body $body

Fortinet FortiOS (Native DNS)

FortiGate firewalls natively support enterprise RFC 2136 DNS updates. By executing this via the CLI, the firewall binds directly to the authoritative name server without HTTP APIs. Note: Using edit 0 ensures a new entry is appended without overwriting existing DDNS settings.

config system ddns
    edit 0
        set monitor-interface "wan1"
        set ddns-server genericDDNS
        set ddns-server-addr "update.dynip.dev"
        set ddns-domain "YOUR_DOMAIN"
        set ddns-zone "YOUR_DOMAIN"
        set ddns-auth tsig
        set ddns-keyname "key-YOUR_DOMAIN"
        set ddns-key "YOUR_BASE64_KEY"
    next
end

Palo Alto PAN-OS

Configure via Network > Interfaces > DDNS in the GUI. Ensure your external interface has DDNS enabled and is bound to this profile.

Vendor / Provider: Custom

Server: api.dynip.dev

Protocol: HTTPS

URL Path: /update?domain=YOUR_DOMAIN&ip=<ipaddress>&key=YOUR_URL_ENCODED_KEY

Cisco IOS Router

Configure the DDNS update method globally, then apply it to your WAN interface.

! Cisco IOS Router Configuration
ip ddns update method DYNIP
 HTTP
  add https://api.dynip.dev/update?domain=<h>&ip=<a>&key=YOUR_URL_ENCODED_KEY
 interval maximum 0 0 10 0
!
interface GigabitEthernet0/0
 ip ddns update hostname YOUR_DOMAIN
 ip ddns update DYNIP

Cisco ASA Firewall

Similar to IOS, define the update method and apply it to the outside interface.

! Cisco ASA Firewall Configuration
ddns update method DYNIP
 http
  add https://api.dynip.dev/update?domain=<h>&ip=<a>&key=YOUR_URL_ENCODED_KEY
 interval 00:10:00
!
interface GigabitEthernet1/1
 ddns update hostname YOUR_DOMAIN
 ddns update DYNIP

MikroTik RouterOS

Native integration via /tool fetch — the built-in HTTP client discovers the current IP from /ip and posts it to /update. See the MikroTik guide for the full walkthrough, IPv6 patterns, and private-APN scenarios.

/system script add name="dynip-update" source="\n  :local currentIP ([/tool fetch url=\"https://api.dynip.dev/ip\" output=user as-value]->\"data\")\n  :local updateUrl \"https://api.dynip.dev/update?domain=YOUR_DOMAIN&ip=$currentIP&key=YOUR_URL_ENCODED_KEY\"\n  /tool fetch url=$updateUrl keep-result=no\n"\n/system scheduler add name="dynip-schedule" interval=10m on-event="dynip-update"

pfSense / OPNsense

Navigate to Services > Dynamic DNS and create a new custom entry.

Service Type: Custom

Update URL: https://api.dynip.dev/update?domain=%H&ip=%I&key=YOUR_URL_ENCODED_KEY

Result Match: success

Hostname: YOUR_DOMAIN

UniFi (UDM / UDR / UXG)

UniFi OS ships a Dynamic DNS client (a wrapped inadyn) that can keep your zone's A record pointed at the gateway's WAN IPv4 with no agent anywhere else on the network. Configure it under Settings → Internet → Dynamic DNS → Create New. The snippet generator in the dashboard produces the exact field values for your zone — choose Snippets → UniFi (UDM / UDR / UXG); the output is a list of values to transcribe into the form, not a script to run.

Set Service to Custom, Hostname to your zone, Username to any non-empty value (DynIP ignores it, but the form requires one), Password to your key, and Server to api.dynip.dev/update?domain=%h&ip=%i&key=%p. That Server value is the same for every user — %h, %i and %p are inadyn substitutions filled in at request time from the Hostname, detected WAN IPv4, and Password fields. Validated end-to-end on 2026-08-27 against a UDM-SE running UniFi OS v5.1.19.

On first-generation UDM hardware running UniFi OS 1.x we previously hit a stale CA bundle breaking TLS to the update endpoint and URL-parsing quirks in the bundled inadyn, and recommended a LAN-host container instead. Modern UniFi OS resolves those. If you are on UniFi OS 1.x, or you want IPv6 today, the DynIP Docker container on any always-on LAN host remains a dependable alternative and covers both address families.

For the field-by-field walkthrough, verification, and troubleshooting, see the UniFi guide.

Teltonika RutOS

Teltonika devices support secure BIND TSIG updates. Paste this into the CLI or apply equivalents in the RutOS WebUI.

uci -q delete ddns.myddns
uci set ddns.myddns=service
uci set ddns.myddns.enabled='1'
uci set ddns.myddns.domain='YOUR_DOMAIN'
uci set ddns.myddns.lookup_host='YOUR_DOMAIN'
uci set ddns.myddns.password='YOUR_BASE64_KEY'
uci set ddns.myddns.ip_source='web'
uci set ddns.myddns.ip_url='https://api.dynip.dev/ip'
uci set ddns.myddns.use_https='1'
uci set ddns.myddns.cacert='IGNORE'
uci set ddns.myddns.update_url='https://api.dynip.dev/update?domain=[DOMAIN]&ip=[IP]&key=[PASSWORD]'
uci commit ddns
/etc/init.d/ddns restart

OpenWrt (ddns-scripts)

Leverages the standard ddns-scripts package. Ensure curl is installed via opkg install curl ddns-scripts.

uci -q delete ddns.myddns
uci set ddns.myddns=service
uci set ddns.myddns.enabled='1'
uci set ddns.myddns.domain='YOUR_DOMAIN'
uci set ddns.myddns.lookup_host='YOUR_DOMAIN'
uci set ddns.myddns.password='YOUR_BASE64_KEY'
uci set ddns.myddns.ip_source='web'
uci set ddns.myddns.ip_url='https://api.dynip.dev/ip'
uci set ddns.myddns.use_https='1'
uci set ddns.myddns.cacert='IGNORE'
uci set ddns.myddns.update_url='https://api.dynip.dev/update?domain=[DOMAIN]&ip=[IP]&key=[PASSWORD]'
uci commit ddns
/etc/init.d/ddns restart

DD-WRT / Netgear / Linksys

Navigate to Setup > DDNS to configure DD-WRT flashed consumer routers.

DDNS Service: Custom

DYNDNS Server: api.dynip.dev

Username: dynip

Password: YOUR_URL_ENCODED_KEY

Hostname: YOUR_DOMAIN

URL: /update?domain=YOUR_DOMAIN&ip=[IP]&key=YOUR_URL_ENCODED_KEY

Synology DSM NAS

Go to Control Panel > External Access > DDNS > Customize to add DynIP as a custom provider, then create your entry.

Service Provider: DynIP

Query URL: https://api.dynip.dev/update?domain=__HOSTNAME__&ip=__MYIP__&key=YOUR_URL_ENCODED_KEY


Save the provider, then create a new DDNS entry:

Hostname: YOUR_DOMAIN

Username/Password: Leave blank or enter dummy data

Fritz!Box Web GUI

Navigate to Internet > Permit Access > Dynamic DNS in your Fritz!Box menu and enter the following settings:

Provider: Custom / Benutzerdefiniert

Update-URL: https://api.dynip.dev/update?domain=<domain>&ip=<ipaddr>&key=<pass>

Domain Name: YOUR_DOMAIN

Username: dynip-user (Can be any value)

Password: YOUR_BASE64_KEY

Python Automation

A clean template for integrating DynIP into custom Python orchestration scripts or Docker containers.

import requests

BASE_URL = "https://api.dynip.dev"
DOMAIN = "YOUR_DOMAIN"
TSIG_KEY = "YOUR_BASE64_KEY"

def update_ddns():
    try:
        # Step 1: Discover external IP
        current_ip = requests.get(f"{BASE_URL}/ip").text.strip()
        print(f"Detected IP: {current_ip}")
        
        # Step 2: Trigger Update (POST keeps the key out of the URL)
        payload = {"domain": DOMAIN, "ip": current_ip, "key": TSIG_KEY}
        response = requests.post(f"{BASE_URL}/update", json=payload)
        
        if response.status_code in (200, 204):
            print("Successfully updated DNS records.")
        else:
            print(f"Update failed: {response.status_code}")
            
    except requests.exceptions.RequestException as e:
        print(f"Network error occurred: {e}")

if __name__ == "__main__":
    update_ddns()

Arduino / ESP32 (IoT)

Perfect for remote sensors and headless microcontrollers. Uses the standard HTTPClient library to discover its external IP and update the DynIP API once a network connection is established.

#include <WiFi.h>
#include <HTTPClient.h>

const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const String baseUrl = "https://api.dynip.dev";
const String domain = "YOUR_DOMAIN";
const String key = "YOUR_URL_ENCODED_KEY";

void setup() {
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);

  HTTPClient http;
  
  // 1. Discover IP
  http.begin(baseUrl + "/ip");
  int httpCode = http.GET();
  String currentIp = "";
  if (httpCode > 0) currentIp = http.getString();
  http.end();

  // 2. Update DDNS
  if (currentIp != "") {
    String updateUrl = baseUrl + "/update?domain=" + domain + "&ip=" + currentIp + "&key=" + key;
    http.begin(updateUrl);
    http.GET();
    http.end();
  }
}

void loop() {
  // Add deep sleep or delay logic here
}

Minimal C Script (libcurl)

For environments where you just want a compiled, lightweight binary without needing Python, Node, or Bash. It uses a write callback to store the IP in memory before constructing the update URL. Compile with: gcc ddns.c -lcurl -o ddns_update

#include <stdio.h>
#include <string.h>
#include <curl/curl.h>

// Callback to store the fetched IP
size_t write_cb(void *ptr, size_t size, size_t nmemb, void *data) {
    strncat((char *)data, (char *)ptr, size * nmemb);
    return size * nmemb;
}

int main(void) {
    CURL *curl = curl_easy_init();
    if(!curl) return 1;

    char ip[64] = {0};
    char url[512];

    // 1. Discover IP
    curl_easy_setopt(curl, CURLOPT_URL, "https://api.dynip.dev/ip");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, ip);
    curl_easy_perform(curl);

    // Strip trailing newline from IP (safety measure)
    ip[strcspn(ip, "\r\n")] = 0;

    // 2. Update DDNS
    if(strlen(ip) > 0) {
        snprintf(url, sizeof(url), "https://api.dynip.dev/update?domain=YOUR_DOMAIN&ip=%s&key=%s", ip, "YOUR_URL_ENCODED_KEY");
        curl_easy_setopt(curl, CURLOPT_URL, url);
        
        // Fix: Reset libcurl to properly print to standard output
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, stdout);
        
        curl_easy_perform(curl);
        printf("\n");
    }

    curl_easy_cleanup(curl);
    return 0;
}