legitdns.
FeaturesUse casesDocsSign inGet started

Update API

One endpoint updates a hostname:

GET or POST https://legitdns.com/api/v1/update

Authentication

Send the hostname's update token (see Tokens):

  • Preferred: an Authorization: Bearer <token> header.
  • Or HTTP Basic auth: username is the full hostname, password is the token.

Parameters

NameRequiredMeaning
hostnameyesThe full hostname, e.g. mypi.pihome.example
ipv4noAdvanced: an IPv4 address to publish instead of the connection's
ipv6noAdvanced: an IPv6 address to publish instead of the connection's

The two families are independent: send one and the other stays; send both and both update together. Send neither - the normal case - and we publish the address the request came from, in whichever family it arrived in. ipv4 and ipv6 exist for publishing an address different from the one you connect from.

With GET, parameters go in the query string. POST takes a JSON body with the same names:

curl -X POST https://legitdns.com/api/v1/update \
  -H "Authorization: Bearer YOUR-TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"hostname": "mypi.pihome.example", "ipv4": "192.0.2.44", "ipv6": "2001:db8::44"}'

Response

The answer is JSON:

{"status": "good", "reason": "ok", "hostname": "mypi.pihome.example", "changed": true, "records": {"A": "192.0.2.44", "AAAA": "2001:db8::44"}, "previous": {"A": null, "AAAA": null}}
  • status - good (records changed), nochg (nothing changed), or error.
  • reason - machine-readable detail; the same values your history page shows.
  • changed - true only when DNS actually changed.
  • records - the A and AAAA values now published (null where a family has no record).
  • previous - the values before this update.

HTTP status codes and reasons

HTTPreasonMeaning
200okUpdated and published
200no_changeThe addresses already matched; nothing rewritten
200offline_heldThe hostname is switched offline in the panel; the addresses were saved as its last-known values, not published
200redirect_heldThe hostname is in redirect mode; the addresses were saved as last-known, not published
200maintenance_heldThe service is in maintenance; the update was recorded and held, not published
200read_only_heldThe service is in read-only mode; the update was recorded and held, not published
400invalid_ipAn address did not parse, an IPv4 address was sent as ipv6 (or the other way around), or the address is not a public address
400notfqdn / invalid_hostnameThe hostname is malformed
400invalid_requestThe request body could not be read as JSON
401badauthThe token is wrong, revoked, expired, or the update came from a source the token does not allow
404nohostNo such hostname
429rate_limited_ipToo many attempts from your source address this hour
429rate_limited_accountToo many authenticated updates on the account this hour
502zone_error / zone_unavailableA problem on our side publishing the change; retry later
503service_downThe service is down for maintenance; the attempt was recorded, retry later

Private and loopback addresses are refused - they can never work from the internet.

On badauth: if the token looks right, check its rules in the panel (maximum age, allowed sources). The refusal never says which rule fired; your history page shows the exact reason.

Rate limits

  • 300 attempts per hour per source address, failed ones included.
  • 120 updates per hour per account. Only attempts with a valid token count, so wrong-token spraying at your hostname cannot spend your budget.
  • After repeated wrong-token attempts for one hostname from one address, further wrong answers are deliberately delayed, growing with each failure. Correct updates are never delayed.

Normal setups never reach these numbers; a client retrying in a tight loop will.

Certificate challenges (Let's Encrypt)

To issue a certificate with a DNS check, a certificate authority asks you to publish one value as a TXT record at _acme-challenge. in front of your hostname. That one record is the only TXT record legitdns sets, and these two calls manage it - with the hostname's own update token, so a token can only ever set its own hostname's challenge:

POST https://legitdns.com/api/v1/acme-challenge
POST https://legitdns.com/api/v1/acme-challenge/clear

Set takes a JSON body with hostname and value (the value your certificate tool shows you). The answer names the record, its value, and when it expires:

{"status": "good", "reason": "acme_set",
 "hostname": "mypi.pihome.example",
 "challenge": {"name": "_acme-challenge.mypi.pihome.example",
               "value": "the value you sent",
               "expires_at": "2026-01-01T01:00:00+00:00"}}

The record stays up for about an hour and is then removed on its own; clearing early is what the second call is for. Setting a new value replaces the old one. Refusals use the update reasons where they fit: badauth (401), nohost (404), invalid_acme_value (400 - not the token shape a challenge value has), offline_held (409 - the hostname is offline, so a check cannot pass), and the rate-limit reasons above (429). The same rate limits apply. In maintenance or read-only mode, or while the service is down, a set is refused (503) rather than held - the value would be stale by the time it could publish.

Hook scripts for certbot

There is no legitdns plugin for certbot or acme.sh. With certbot's manual mode, two small scripts of yours call the API at check time instead. The auth hook (save it, make it executable, and put your hostname's token in it):

#!/bin/sh
curl -s -X POST "https://legitdns.com/api/v1/acme-challenge" \
  -H "Authorization: Bearer <redacted>" \
  -H "Content-Type: application/json" \
  -d "{\"hostname\": \"$CERTBOT_DOMAIN\", \"value\": \"$CERTBOT_VALIDATION\"}"
sleep 10

The cleanup hook:

#!/bin/sh
curl -s -X POST "https://legitdns.com/api/v1/acme-challenge/clear" \
  -H "Authorization: Bearer <redacted>" \
  -H "Content-Type: application/json" \
  -d "{\"hostname\": \"$CERTBOT_DOMAIN\"}"

And the command that uses them:

certbot certonly --manual --preferred-challenges dns \
  --manual-auth-hook /path/to/auth-hook.sh \
  --manual-cleanup-hook /path/to/cleanup-hook.sh \
  -d mypi.pihome.example

acme.sh's manual DNS mode prints the value to publish instead of calling a script - paste it into the Certificates section of the hostname page (see the panel guide).

curl examples

The normal update sends no address - we publish the address the request comes from:

curl -H "Authorization: Bearer YOUR-TOKEN" \
  "https://legitdns.com/api/v1/update?hostname=mypi.pihome.example"

Advanced: publish explicit addresses instead, one family or both:

curl -H "Authorization: Bearer YOUR-TOKEN" \
  "https://legitdns.com/api/v1/update?hostname=mypi.pihome.example&ipv4=192.0.2.44&ipv6=2001:db8::44"

checkip

GET https://legitdns.com/checkip

Answers with your address as plain text: the address the service sees for the request - the same one an update with no address publishes. No account or token needed. Call it over IPv4 or IPv6 to learn that family's address.

curl https://legitdns.com/checkip
curl -6 https://legitdns.com/checkip

Nothing is stored for a checkip call. Limit: 60 requests per minute per address.

See also: DynDNS2 protocol - the form of update that routers speak; Tokens; Troubleshooting.

legitdns - free dynamic DNS for home serversEULAPrivacyStatusReport abuseFeedback