Change DNS records on many domains at once
5 steps · Checked against the API code 3 November 2026
A guide does one task from start to finish. Every endpoint, with all its fields and cases, is in the API reference.
One call adds or deletes the same DNS records in many zones. The API answers with one job, which works through the domains one at a time. When it ends, its result says what happened on each domain.
Before you start
Section titled “Before you start”- The requests use your key from
$HOSTUP_API_KEY(Quickstart). The key needs the scopewrite:dns. It coversread:dnsfor the lists of zones and records. Keys and scopes - Domains registered with Hostup in this account, each with its DNS zone at Hostup. A domain registered elsewhere that only uses Hostup’s DNS is refused. Change its records one at a time, as in Point a domain at your server.
jqfor the commands that filter the answers.
1. Pick the domains
Section titled “1. Pick the domains”The bulk call takes domain names, not IDs. List your DNS zones to see which names it accepts.
curl "https://cloud.hostup.se/api/v2/dns-zones" \ -H "Authorization: Bearer $HOSTUP_API_KEY"{ "data": [ { "id": "zone_01hxa3b4c5d6e7f8g9h0j1k2m3", "name": "example.com", "kind": "linked", "…": "…" }, { "id": "zone_01hxa3b4c5d6e7f8g9h0j1k2m4", "name": "example.net", "kind": "dns_only", "…": "…" }, { "id": "zone_01hxa3b4c5d6e7f8g9h0j1k2m5", "name": "example.nu", "kind": "linked", "…": "…" }, { "id": "zone_01hxa3b4c5d6e7f8g9h0j1k2m6", "name": "example.se", "kind": "linked", "…": "…" } ], "hasMore": false, "nextCursor": null}Use the zones whose kind is linked. The bulk call refuses dns_only zones. Keep the list: step 5 uses the zone IDs.
Up to 1000 zones come back at a time. When hasMore is true, ask again with ?cursor= and the nextCursor value. Pagination
The names as a JSON list, ready for the request body:
curl -s "https://cloud.hostup.se/api/v2/dns-zones" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ | jq -c '[.data[] | select(.kind == "linked") | .name]'If you add SPF, DMARC or a CNAME
Section titled “If you add SPF, DMARC or a CNAME”add never replaces a record. A domain that already has a DMARC record gets a second one. The job still reports success. With two _dmarc records, receivers apply no DMARC policy at all. An SPF record and a CNAME also belong once per name.
Check which zones already have a record at the name first. This prints each linked zone that has a record at _dmarc, with its values:
curl -s "https://cloud.hostup.se/api/v2/dns-zones" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ | jq -r '.data[] | select(.kind == "linked") | .id' \ | while read -r ID; do curl -s "https://cloud.hostup.se/api/v2/dns-zones/$ID/records?type=TXT&name=_dmarc" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ | jq -c 'select(.records | length > 0) | {zone: .zone.name, values: [.records[].value]}' doneFor SPF, ask for type=TXT&name=@ and look for values that start with v=spf1. For a CNAME, ask for the name without a type, such as name=www.
Leave the printed domains out of the add. Change their record one zone at a time with Update a DNS record.
2. Send the change
Section titled “2. Send the change”Send one of the two requests below. Every domain in domainNames gets the same change.
The whole batch is checked before anything is queued. If one domain is not in the account or one record does not validate, the call is refused as a whole. Nothing changes then.
Add records
Section titled “Add records”Here three domains get a DMARC record. The check in step 1 printed none of them.
curl -X POST "https://cloud.hostup.se/api/v2/domains/bulk/dns" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "add", "domainNames": ["example.com", "example.se", "example.nu"], "records": [ {"type": "TXT", "name": "_dmarc", "value": "v=DMARC1; p=none"} ] }'{ "operation": { "status": "pending", "jobId": "dbj_01hxa3b4c5d6e7f8g9h0j1k2m3", "pollUrl": "/api/jobs/dbj_01hxa3b4c5d6e7f8g9h0j1k2m3" }, "action": "add", "domainsQueued": 3}export JOB_ID="dbj_…" # operation.jobId abovedomainsQueued is the number of domains in the job.
Two things differ from creating a single record:
typeis in capitals, such asTXTorMX. Lower case is refused.nameis the short name inside each zone, such as_dmarcorwww.@is the domain itself. The same name is used in every zone. Never send a full name:www.example.combecomeswww.example.com.example.sein the zoneexample.se.
Change DNS records in several zones describes the other record fields.
Delete records
Section titled “Delete records”Here three domains lose every record that points at a server you no longer use. Send "action": "delete" with "deleteType": "by_value" and the old address in recordValue:
curl -X POST "https://cloud.hostup.se/api/v2/domains/bulk/dns" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "delete", "domainNames": ["example.com", "example.se", "example.nu"], "deleteType": "by_value", "recordValue": "198.51.100.7" }'{ "operation": { "status": "pending", "jobId": "dbj_01hxa3b4c5d6e7f8g9h0j1k2m4", "pollUrl": "/api/jobs/dbj_01hxa3b4c5d6e7f8g9h0j1k2m4" }, "action": "delete", "domainsQueued": 3}export JOB_ID="dbj_…" # operation.jobId aboveTo point the domains at a new address instead, add the new records first. Move records to a new address shows the order.
deleteType decides which records are deleted. Send only the field that goes with it:
by_valuewithrecordValue: every record whose value is exactly that, whatever its name and type. It suits A and AAAA records, whose value is the address.by_typewithrecordType, in capitals: every record of that type. Add"recordName": "@"to limit it to the domain itself.by_namewith"recordName": "@": every record at the domain itself, mail and website records included.all: every record in each zone, mail and website records included.
Across many domains, only "recordName": "@" works. A short name such as www matches nothing. A full name matches one zone only.
3. Follow the job
Section titled “3. Follow the job”Poll the job until data.status is completed or failed. One poll looks like this:
curl "https://cloud.hostup.se/api/jobs/$JOB_ID" \ -H "Authorization: Bearer $HOSTUP_API_KEY"{ "data": { "jobId": "dbj_01hxa3b4c5d6e7f8g9h0j1k2m3", "type": "bulk_dns_add", "status": "running", "progressDetails": { "current": 2, "total": 3, "message": "Adding DNS records to example.se" }, "…": "…" }, "…": "…"}progressDetails.current and total count the domains. data.type is bulk_dns_add, bulk_dns_update or bulk_dns_delete.
To wait until it ends, run the polling script from Jobs. It prints data.result when the job has finished.
./wait-for-job.sh "/api/jobs/$JOB_ID"4. Read what happened on each domain
Section titled “4. Read what happened on each domain”Once data.status is completed, the same request returns the result.
curl "https://cloud.hostup.se/api/jobs/$JOB_ID" \ -H "Authorization: Bearer $HOSTUP_API_KEY"{ "data": { "jobId": "dbj_01hxa3b4c5d6e7f8g9h0j1k2m3", "type": "bulk_dns_add", "status": "completed", "progress": 100, "result": { "message": "Bulk DNS add completed", "total": 3, "success": 2, "failed": 1, "results": [ { "domainName": "example.com", "success": true, "recordsAdded": 1, "totalRecords": 1, "records": [{ "name": "_dmarc", "type": "TXT", "success": true }] }, { "domainName": "example.se", "success": true, "recordsAdded": 1, "totalRecords": 1, "records": [{ "name": "_dmarc", "type": "TXT", "success": true }] }, { "domainName": "example.nu", "success": false, "recordsAdded": 0, "totalRecords": 1, "records": [ { "name": "_dmarc", "type": "TXT", "success": false, "error": "Record already exists or could not be added" } ] } ] }, "error": null, "…": "…" }, "…": "…"}completed means the job went through the whole list. It does not mean every domain worked. data.result.success and data.result.failed count the domains. data.result.results has one entry per domain.
What each entry holds depends on the action:
| Action | Fields per domain | The domain counts as failed when |
|---|---|---|
add | recordsAdded, totalRecords, records | any of its records failed |
delete | recordsDeleted, totalRecords (the records that matched) | nothing was deleted. "totalRecords": 0 means nothing matched. Nothing is wrong with that zone. |
A delete where recordsDeleted is lower than totalRecords left some matches in place. That entry still has "success": true.
A failed record has "success": false and the reason in error. A domain the job could not work on at all has only domainName, "success": false and error. One cause is a zone that was removed after the call.
The domains to look at, with the records that failed on each:
curl -s "https://cloud.hostup.se/api/jobs/$JOB_ID" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ | jq '.data.result.results[] | select(.success == false or (.recordsDeleted != null and .recordsDeleted < .totalRecords)) | {domainName, error, failed: [.records[]? | select(.success == false)]}'The error Record already exists or could not be added is also given when the write returned no reason. The record may be in the zone already. Check the zone in step 5 before you send anything again.
If data.status is failed
Section titled “If data.status is failed”The job stopped as a whole. data.result is null and data.error says Job failed. Some zones may have changed before it stopped. Look at their records before you send the change again.
5. Check a zone
Section titled “5. Check a zone”Look at the zone of a domain that failed. Here it is example.nu. When every domain worked, check any one zone the same way.
export ZONE_ID="zone_…" # the domain's zone ID from step 1Ask for the records by type and name.
curl "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records?type=TXT&name=_dmarc" \ -H "Authorization: Bearer $HOSTUP_API_KEY"{ "zone": { "id": "zone_01hxa3b4c5d6e7f8g9h0j1k2m5", "name": "example.nu", "status": "active", "recordCount": 1, "…": "…" }, "records": [ { "id": "drr_01hxa3b4c5d6e7f8g9h0j1k2m3", "type": "TXT", "name": "_dmarc.example.nu", "value": "v=DMARC1; p=none", "ttl": 3600 } ]}Names come back in full. One record with the value you sent means the record is in place. That domain needs nothing more, even though step 4 listed it as failed.
If the record is missing
Section titled “If the record is missing”Send a new request with only the domains whose zone lacks the record. For add, send only the records that failed on them. The records that went through are in place already. Follow the new job as in step 3.
If an older record was there
Section titled “If an older record was there”Two records with different values mean an older record was there already. Delete the one you don’t want with Delete a DNS record. Use its id from this list.
Move records to a new address
Section titled “Move records to a new address”Here the domains point at 198.51.100.7 and should point at 192.0.2.10. Add the records with the new address first. Wait until that job has finished. Then delete the old records on the domains where the add worked.
Bulk jobs can run at the same time. A delete sent before the add has finished can remove the old address from a domain that does not have the new one yet. That domain is then left without an address.
1. Find the names that use the old address
Section titled “1. Find the names that use the old address”This prints each linked zone with the names whose value is the old address:
curl -s "https://cloud.hostup.se/api/v2/dns-zones" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ | jq -r '.data[] | select(.kind == "linked") | .id' \ | while read -r ID; do curl -s "https://cloud.hostup.se/api/v2/dns-zones/$ID/records" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ | jq -c '{zone: .zone.name, names: [.records[] | select(.value == "198.51.100.7") | .name]}' doneThe delete in step 4 removes all of these names. Here example.com prints example.com and www.example.com. In the add, they are @ and www.
One add gives every domain the same names. Put the domains that use the same names in one request.
2. Add the records with the new address
Section titled “2. Add the records with the new address”curl -X POST "https://cloud.hostup.se/api/v2/domains/bulk/dns" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "add", "domainNames": ["example.com", "example.se", "example.nu"], "records": [ {"type": "A", "name": "@", "value": "192.0.2.10"}, {"type": "A", "name": "www", "value": "192.0.2.10"} ] }'{ "operation": { "status": "pending", "jobId": "dbj_01hxa3b4c5d6e7f8g9h0j1k2m5", "pollUrl": "/api/jobs/dbj_01hxa3b4c5d6e7f8g9h0j1k2m5" }, "action": "add", "domainsQueued": 3}export JOB_ID="dbj_…" # operation.jobId aboveadd leaves the records with the old address in place.
3. Wait for the add job
Section titled “3. Wait for the add job”Wait for the job as in Follow the job:
./wait-for-job.sh "/api/jobs/$JOB_ID"Then list the domains where the add worked:
curl -s "https://cloud.hostup.se/api/jobs/$JOB_ID" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ | jq -c '[.data.result.results[] | select(.success == true) | .domainName]'Here it prints ["example.com","example.se"]. The add failed on example.nu. Read what happened on each domain shows how to find out why. A domain whose add failed keeps its old records. Leave it out of the delete until its new records are in place.
4. Delete the old records
Section titled “4. Delete the old records”Send the delete with the domains from step 3 and the old address in recordValue. This delete cannot be undone through the API either.
curl -X POST "https://cloud.hostup.se/api/v2/domains/bulk/dns" \ -H "Authorization: Bearer $HOSTUP_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "delete", "domainNames": ["example.com", "example.se"], "deleteType": "by_value", "recordValue": "198.51.100.7" }'{ "operation": { "status": "pending", "jobId": "dbj_01hxa3b4c5d6e7f8g9h0j1k2m6", "pollUrl": "/api/jobs/dbj_01hxa3b4c5d6e7f8g9h0j1k2m6" }, "action": "delete", "domainsQueued": 2}export JOB_ID="dbj_…" # operation.jobId aboveFollow this job too. Each entry’s recordsDeleted is the number of old records removed on that domain.
Replace a record on many domains
Section titled “Replace a record on many domains”The body also takes "action": "update". Don’t use it to replace records. In each zone it looks for a record of the same type whose stored name equals name. Zones store full names. A short name such as _dmarc or @ finds nothing there. The record is then added next to the old one. update also reports a record as written without checking that the write went through.
To replace an A or AAAA record on many domains, add the new one and delete the old one by its value, as in Move records to a new address. Replace other records zone by zone: find the record’s id as in step 5. Then send the new record to Update a DNS record.
What happens next
Section titled “What happens next”A domain’s records change when the job reaches it. Resolvers that looked a name up before keep the old answer until its TTL runs out.
Nothing is billed and no e-mail goes out. The snapshot taken before the job is listed at List DNS snapshots, as the caution in step 2 says.
A finished job is kept for a limited time. Read the result while you poll. Jobs
Next: Change a domain’s nameservers, for a domain whose records should answer from Hostup’s nameservers.
Errors you may hit
Section titled “Errors you may hit”- 403
forbiddenwitherrors[]: a name indomainNamesis not a domain in this account. Each entry points at one, for example/domainNames/2. Adom_ID gets the same answer: send names. - 403
forbiddenwith the detailAccess denied.and no list: a domain is registered elsewhere. The same detail comes when the key is tied to certain DNS zones or records:- Every zone in the batch must be on the key’s list.
- A key limited to certain records can add and update only those records.
- A bulk delete needs a grant for each whole zone.
- 403
forbiddenwithrequiredScopes: the key lackswrite:dns. When a key is refused - 400
invalid_requestwitherrors[0].pointer: the body does not fit the action. It has a field the action does not take, an empty list or a delete without its filter field. The pointer names the field. - 400
invalid_requestwith the detailThe bulk DNS operation request is invalid.and no pointer: a record did not validate or a domain has no DNS zone at Hostup. Check that each record type is in capitals. Then check the TTL, the priority on MX and SRV records, the value format and the zones from step 1. - 500
upstream_failed: the job could not be queued. Nothing changed. Send the same request again a little later.
Nothing is queued after a 403 or a 400.