Skip to content

Set up MX, SPF, DKIM and DMARC for a domain

8 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.

Mailboxes on Hostup web hosting? Connect a domain’s email to Hostup web hosting writes all these records in one call. For any other mail provider, add the records it gives you to the domain’s DNS zone at Hostup, one record per request. Then remove the old MX records.

  • The requests use your key from $HOSTUP_API_KEY (Quickstart). The key needs the scope write:dns. It covers read:dns as well. Keys and scopes
  • The values from your mail provider: the MX hosts with their priorities, the SPF include:, the DKIM name with its key and the DMARC policy you want.
  • The domain uses Hostup’s nameservers, for example primary.ns.hostup.se and secondary.ns.hostup.se. Other mail servers see the records only once it does. Change a domain’s nameservers shows how.

The examples use a provider whose MX hosts are mx1.mail.example.net and mx2.mail.example.net. Put your provider’s values in their place.

Ask for the zone by the domain name. Send the domain itself, not a subdomain.

GET /api/v2/dns-zones
curl "https://cloud.hostup.se/api/v2/dns-zones?name=example.com" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"data": [
{
"id": "zone_01hxa3b4c5d6e7f8g9h0j1k2m3",
"name": "example.com",
"status": "active",
"isDnsOnly": false,
"kind": "linked"
}
],
"hasMore": false,
"nextCursor": null
}

Keep the zone ID. Every record call below uses it.

export ZONE_ID="zone_…" # data[0].id above

The account has no DNS zone with that name. The same empty list comes back when the key is limited to other DNS zones. For a domain registered with Hostup that uses Hostup’s nameservers, reading the domain’s DNS creates the zone. Point a domain at your server shows the two requests.

List DNS zone records returns every record in the zone.

GET /api/v2/dns-zones/{id}/records
curl "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"zone": {
"id": "zone_01hxa3b4c5d6e7f8g9h0j1k2m3",
"name": "example.com",
"status": "active",
"recordCount": 3,
"…": "…"
},
"records": [
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2m4",
"type": "MX",
"name": "example.com",
"value": "mail.example.com",
"ttl": 3600,
"priority": 5
},
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2m5",
"type": "TXT",
"name": "example.com",
"value": "v=spf1 ip4:192.0.2.10 ~all",
"ttl": 3600
},
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2ma",
"type": "TXT",
"name": "_dmarc.example.com",
"value": "v=DMARC1; p=none",
"ttl": 3600
}
]
}

Note the IDs of three kinds of record. Later steps use them:

  • the MX records, which step 4 removes
  • the TXT record whose value starts with v=spf1, which step 5 changes
  • a TXT record named _dmarc.example.com, which step 7 changes

Send one request per MX host to Create a DNS record. A name of @ means the domain itself. priority is required for MX records. Lower numbers are tried first.

POST /api/v2/dns-zones/{id}/records
curl -X POST "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "MX",
"name": "@",
"value": "mx1.mail.example.net",
"priority": 10
}'
Response 201
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2m6",
"type": "MX",
"name": "example.com",
"value": "mx1.mail.example.net",
"ttl": 3600,
"priority": 10,
"warnings": [
{
"code": "same_name_mx_records",
"records": [
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2m4",
"type": "MX",
"name": "example.com",
"value": "mail.example.com",
"priority": 5
}
],
"…": "…"
}
]
}

Send the second host the same way with its own priority (mx2.mail.example.net with 20).

value is a hostname that resolves, never an IP address. A host in your own zone, such as mail.example.com, needs its A or AAAA record in the zone first.

warnings with same_name_mx_records lists the MX records that were there before. Mail can still go to them until step 4 removes them.

The MX request answers 400 mx_record_blocked_email_forwarding. Email forwarding receives the domain’s mail itself. While it is on, the API refuses MX records at @. To use your provider, turn forwarding off with Turn domain email forwarding on or off. That needs the domain ID and the scopes read:domains, write:email and write:domains.

GET /api/v2/domains
curl "https://cloud.hostup.se/api/v2/domains?name=example.com" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"data": [
{
"id": "dom_01hxa3b4c5d6e7f8g9h0j1k2m3",
"name": "example.com",
"…": "…"
}
],
"hasMore": false,
"nextCursor": null
}
export DOMAIN_ID="dom_…" # data[0].id above

Then turn forwarding off. From then on, mail to the domain is no longer forwarded.

PATCH /api/v2/domains/{id}/email-forwarding
curl -X PATCH "https://cloud.hostup.se/api/v2/domains/$DOMAIN_ID/email-forwarding" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
Response 200
{
"enabled": false,
"…": "…"
}

List the records again (step 2). Then send the MX requests again.

To keep forwarding, leave it on and skip steps 3 and 4. SPF, DKIM and DMARC records can be added while forwarding is on.

Delete each old MX record with Delete a DNS record. Put its id from step 2 in RECORD_ID:

export RECORD_ID="drr_…" # an old MX record's id from step 2
DELETE /api/v2/dns-zones/{id}/records/{recordId}
curl -X DELETE "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records/$RECORD_ID" \
-H "Authorization: Bearer $HOSTUP_API_KEY"

The answer is 204 with no body. A record that is already gone also answers 204. Repeat this for every old MX record.

An old MX such as mail.example.com, as in the example, often means the mailboxes were on a Hostup web hosting account. That account may still accept mail for the domain itself. Set its email routing to remote. The hosting then never accepts mail for the domain. These calls need the scopes read:services, write:hosting and console:services.

Find the hosting account. primaryDomain is each account’s main domain. For an addon domain, List hosting domains shows which account holds it.

GET /api/v2/shared-hosting
curl -s "https://cloud.hostup.se/api/v2/shared-hosting" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
| jq -c '.data[] | {id, primaryDomain}'
export ACCOUNT_ID="acct_…" # id of the account that holds the domain
PUT /api/v2/shared-hosting/{accountId}/email-routing
curl -X PUT "https://cloud.hostup.se/api/v2/shared-hosting/$ACCOUNT_ID/email-routing" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "example.com", "mode": "remote"}'
Response 200
{
"id": "eroute_01hxa3b4c5d6e7f8g9h0j1k2m3",
"domain": "example.com",
"mode": "remote",
"detectedMode": null
}

Get email routing for a domain shows the mode before or after. auto follows the MX records. local delivers the domain’s mail on the hosting server.

SPF is one TXT record on the domain itself. Its value starts with v=spf1. Keep a single SPF record per domain.

Change the record from step 2 with Update a DNS record. Keep the senders that still send mail for the domain, such as a web server. Add your provider’s include:.

export RECORD_ID="drr_…" # the SPF record's id from step 2
PUT /api/v2/dns-zones/{id}/records/{recordId}
curl -X PUT "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records/$RECORD_ID" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "TXT",
"name": "@",
"value": "v=spf1 ip4:192.0.2.10 include:_spf.mail.example.net ~all",
"ttl": 3600
}'
Response 200
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2m5",
"type": "TXT",
"name": "example.com",
"value": "v=spf1 ip4:192.0.2.10 include:_spf.mail.example.net ~all",
"ttl": 3600
}

PUT replaces the whole record. Without ttl the TTL goes back to 3600 seconds. Use the id in the answer from now on. Update a DNS record says when it changes.

Create it. Send the value without quotes. The API checks the SPF syntax and refuses a broken value with 400.

POST /api/v2/dns-zones/{id}/records
curl -X POST "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "TXT",
"name": "@",
"value": "v=spf1 include:_spf.mail.example.net ~all"
}'

Your provider gives a selector and a public key. The selector is a short name such as s1. The record name is the selector followed by ._domainkey.

POST /api/v2/dns-zones/{id}/records
curl -X POST "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "TXT",
"name": "s1._domainkey",
"value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAexamplekey"
}'
Response 201
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2m9",
"type": "TXT",
"name": "s1._domainkey.example.com",
"value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAexamplekey",
"ttl": 3600
}
  • Send the key as one value without quotes, even when it is longer than 255 characters. The API splits long values into DNS strings itself. Quoted pieces copied from a zone file are joined into one value.
  • Some providers give DKIM as a CNAME record. Send "type": "CNAME" with the same name and the provider’s hostname as value. A CNAME cannot share its name with any other record. The API answers 409 when one is already there.

DMARC is one TXT record at _dmarc. Its value must start with v=DMARC1. The API refuses anything else. The examples use p=none, which only asks receivers for reports. Move to p=quarantine or p=reject once the reports show that your mail passes.

Keep a single DMARC record per domain. The API accepts a second _dmarc record with a different value. Receivers then apply no DMARC policy at all.

Change the _dmarc.example.com record from step 2 with Update a DNS record, as in step 5.

export RECORD_ID="drr_…" # the DMARC record's id from step 2
PUT /api/v2/dns-zones/{id}/records/{recordId}
curl -X PUT "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records/$RECORD_ID" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "TXT",
"name": "_dmarc",
"value": "v=DMARC1; p=none; rua=mailto:[email protected]",
"ttl": 3600
}'
Response 200
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2ma",
"type": "TXT",
"name": "_dmarc.example.com",
"value": "v=DMARC1; p=none; rua=mailto:[email protected]",
"ttl": 3600
}

Create it.

POST /api/v2/dns-zones/{id}/records
curl -X POST "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "TXT",
"name": "_dmarc",
"value": "v=DMARC1; p=none; rua=mailto:[email protected]"
}'

Read the MX records again with a type filter.

GET /api/v2/dns-zones/{id}/records
curl "https://cloud.hostup.se/api/v2/dns-zones/$ZONE_ID/records?type=MX" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"zone": {
"id": "zone_01hxa3b4c5d6e7f8g9h0j1k2m3",
"name": "example.com",
"status": "active",
"recordCount": 2
},
"records": [
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2m6",
"type": "MX",
"name": "example.com",
"value": "mx1.mail.example.net",
"ttl": 3600,
"priority": 10
},
{
"id": "drr_01hxa3b4c5d6e7f8g9h0j1k2m7",
"type": "MX",
"name": "example.com",
"value": "mx2.mail.example.net",
"ttl": 3600,
"priority": 20
}
]
}

Only your provider’s hosts should be left. Send ?type=TXT to see the SPF, DKIM and DMARC records.

A record you create, change or delete is in the zone when the API answers. There is no job to follow. None of these calls sends an e-mail or costs money.

Mail servers that looked up the old MX records can keep using them until the old TTL has passed. Keep the old mailboxes until then.

Before each record change the API saves a copy of the zone. List DNS snapshots and List DNS history for a domain show what changed.

Next: Change DNS records on many domains at once

  • 400 mx_record_blocked_email_forwarding: email forwarding is on for the domain. Turn it off as in step 3. Then add the MX records again.
  • 400 invalid_request: a field is wrong. errors points at it when the API can tell, for example a missing priority on an MX record or a TTL outside 60 to 86400. Other refusals have no errors and do not say which check failed. Then check:
    • The MX host resolves.
    • No other MX record at that name uses the same priority.
    • The SPF and DMARC values are valid.
    • The zone holds fewer than 200 records besides SOA and NS.
  • 409 dns_conflict: a CNAME and another record share a name. This happens most often at the DKIM name. Delete the record that is in the way. Use the record type your provider names.
  • 503 upstream_failed: the API could not look up the MX host at that moment. Nothing was saved. Try again a little later.
  • 403 forbidden: the key lacks a scope or is limited to other DNS zones or records. When a key is refused