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
| Name | Required | Meaning |
|---|---|---|
hostname | yes | The full hostname, e.g. mypi.pihome.example |
ipv4 | no | Advanced: an IPv4 address to publish instead of the connection's |
ipv6 | no | Advanced: 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), orerror.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
| HTTP | reason | Meaning |
|---|---|---|
| 200 | ok | Updated and published |
| 200 | no_change | The addresses already matched; nothing rewritten |
| 200 | offline_held | The hostname is switched offline in the panel; the addresses were saved as its last-known values, not published |
| 200 | redirect_held | The hostname is in redirect mode; the addresses were saved as last-known, not published |
| 200 | maintenance_held | The service is in maintenance; the update was recorded and held, not published |
| 200 | read_only_held | The service is in read-only mode; the update was recorded and held, not published |
| 400 | invalid_ip | An address did not parse, an IPv4 address was sent as ipv6 (or the other way around), or the address is not a public address |
| 400 | notfqdn / invalid_hostname | The hostname is malformed |
| 400 | invalid_request | The request body could not be read as JSON |
| 401 | badauth | The token is wrong, revoked, expired, or the update came from a source the token does not allow |
| 404 | nohost | No such hostname |
| 429 | rate_limited_ip | Too many attempts from your source address this hour |
| 429 | rate_limited_account | Too many authenticated updates on the account this hour |
| 502 | zone_error / zone_unavailable | A problem on our side publishing the change; retry later |
| 503 | service_down | The 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.