Skip to main content

Domains

You can only send from a domain you have added to Ha Tang and verified. Each domain gets its own DKIM key and its own return-path subdomain, so receiving mailboxes can check that the mail really comes from you. This page covers adding a domain, the DNS records, verification, one-click setup with Domain Connect and what to do when a record does not verify.

Before you start​

  • Access to the DNS settings of the domain (Cloudflare, your registrar or your DNS host).
  • For the API: a key with the email:write scope. Domain endpoints refuse keys without it.

Pick the domain to send from​

The domain part of your From address must match a verified domain exactly. To send from [email protected], add updates.example.com; adding example.com is not enough, and the reverse is also true.

Use a subdomain for marketing

A subdomain such as updates.example.com or mail.example.com keeps its sending reputation apart from your root domain. If a campaign goes badly, the mail your staff send from example.com is not affected. Many teams use one subdomain for transactional mail and another for broadcasts.

Add a domain​

curl -X POST https://api.hatang.dev/domains \
-H "Authorization: Bearer $HATANG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "example.com" }'

In the dashboard, open Email → Domains, type the domain in Domain and click Add domain. Advanced options holds the same settings as the API fields below.

FieldDefaultDescription
namerequiredThe domain or subdomain, for example example.com or updates.example.com. Stored in lower case.
custom_return_pathsendSubdomain label for the bounce address and SPF, so send gives send.example.com. Letters, digits and hyphens, up to 63 characters. Dashboard: Custom Return-Path.
tracking_subdomainnoneOptional label for branded tracking links, for example links gives links.example.com. Must differ from the return-path label. Dashboard: Tracking Subdomain. See Open and click tracking.
open_trackingfalseAdd an open-tracking pixel to HTML emails from this domain.
click_trackingfalseRewrite links in HTML emails from this domain to record clicks.
regionplatform regionAccepted for Resend compatibility and stored. Sending always uses the platform's Amazon SES region.

The response (201) is the domain with the records to publish:

{
"object": "domain",
"id": "dom_0199e6c17a2b7f009c1e3f6a2b1d9e10",
"name": "example.com",
"status": "not_started",
"created_at": "2026-10-14T09:12:03.000Z",
"region": "us-west-2",
"dkim_selector": "rs7hq2kx9p",
"open_tracking": false,
"click_tracking": false,
"tracking_subdomain": null,
"tracking_domain_verified": false,
"custom_return_path": "send",
"ses_status": "pending",
"tls": "opportunistic",
"capabilities": { "sending": "enabled", "receiving": "disabled" },
"records": [
{ "record": "MX", "name": "send.example.com", "type": "MX", "ttl": "Auto", "status": "not_started", "value": "feedback-smtp.us-west-2.amazonses.com", "priority": 10, "required": true },
{ "record": "SPF", "name": "send.example.com", "type": "TXT", "ttl": "Auto", "status": "not_started", "value": "v=spf1 include:amazonses.com ~all", "required": true },
{ "record": "DKIM", "name": "rs7hq2kx9p._domainkey.example.com", "type": "TXT", "ttl": "Auto", "status": "not_started", "value": "v=DKIM1; k=rsa; p=MIIBIjANBgkqh…", "required": true },
{ "record": "DMARC", "name": "_dmarc.example.com", "type": "TXT", "ttl": "Auto", "status": "not_started", "value": "v=DMARC1; p=none;", "required": false }
]
}

Adding the same domain twice returns 409.

The DNS records​

Always copy names and values from the dashboard or from the records array. The values below are examples.

RecordTypeNameValueRequired
DKIMTXT<selector>._domainkey.example.comv=DKIM1; k=rsa; p=<public key>Yes
Return-path MXMX, priority 10send.example.comfeedback-smtp.<region>.amazonses.comYes
Return-path SPFTXTsend.example.comv=spf1 include:amazonses.com ~allYes
DMARCTXT_dmarc.example.comv=DMARC1; p=none;No, recommended
TrackingCNAMElinks.example.comThe tracking host shown in the dashboardOnly with a tracking subdomain
ReceivingMX, priority 10example.comThe inbound host shown in the dashboardOnly with receiving turned on

DKIM​

Ha Tang creates a 2048-bit RSA key for each domain. The public half goes in a TXT record under a selector that starts with rs followed by eight random characters (the dkim_selector field). Every message is signed with the private half for your domain, which is what lets DMARC pass on DKIM.

A 2048-bit key is longer than 255 characters. Most DNS hosts split it into several strings for you; paste the whole value as one record. Ha Tang ignores whitespace when it compares the key.

Return path: MX and SPF on send.​

The bounce address (the envelope sender, or Return-Path) of every message is on your return-path subdomain, for example bounce+…@send.example.com. Two records on that subdomain make it work:

  • The MX record sends bounces and complaints back to Amazon SES, which reports them to Ha Tang.
  • The SPF TXT record authorizes Amazon SES to send for that subdomain. Because the return path is a subdomain of your domain, SPF aligns with your From domain under DMARC's relaxed alignment.

The records go on the subdomain, not on the root, so they never collide with the SPF or MX records your root domain already has. If the subdomain already has an SPF record, Ha Tang accepts any v=spf1 record that contains include:amazonses.com.

DMARC​

DMARC tells receiving mailboxes what to do with mail that fails SPF and DKIM alignment. Ha Tang proposes v=DMARC1; p=none;, which only asks for monitoring. Any record that starts with v=DMARC1 counts as present, so keep your existing policy if you have one. DMARC does not block verification. Gmail and Yahoo expect bulk senders to publish one, so add it. See Deliverability.

Tracking and receiving​

These two records appear only when you use the feature: the tracking CNAME when the domain has a tracking subdomain, and the receiving MX when you turn on receiving. Neither blocks sending. See Open and click tracking and Receive email.

Names at your DNS host

Many DNS hosts add the zone to the name you type. If you type send.example.com in the Name field for the zone example.com, you may end up with send.example.com.example.com. Type only the part before your domain (send, rs7hq2kx9p._domainkey, _dmarc) when your host works that way.

Verify the domain​

When the records are published, start verification:

  • Dashboard: open the domain and click Verify DNS records. While the domain is pending, the page checks again and refreshes by itself.
  • API: POST /domains/{id}/verify, or hatang.domains.verify(id) in the Node.js SDK.
curl -X POST https://api.hatang.dev/domains/dom_0199e6c17a2b7f009c1e3f6a2b1d9e10/verify \
-H "Authorization: Bearer $HATANG_API_KEY"
{ "object": "domain", "id": "dom_0199e6c17a2b7f009c1e3f6a2b1d9e10", "status": "pending" }

Ha Tang checks every record right away, then keeps checking in the background until the domain is verified. DNS changes can take up to an hour to reach everyone. A domain is Verified when every required record (DKIM, return-path MX and SPF) matches and Amazon SES has confirmed the DKIM signature. Optional records never hold verification back.

Domain statuses​

StatusDashboardMeaning
not_startedNot startedThe domain was added; verification has not been requested yet.
pendingPendingHa Tang is checking. Some required records do not match yet, or Amazon SES has not confirmed DKIM yet.
verifiedVerifiedYou can send from the domain.
temporary_failureTemporary failureA DNS lookup failed for a reason other than "not found", for example a timeout. Ha Tang tries again.
failedFailedVerification failed, for example because Amazon SES rejected the DKIM setup. Fix the records and verify again.

Each record also has its own status (not_started, pending, verified or failed) and, when it fails, a last_error that says what Ha Tang found instead, such as found: nothing or the value that is there now.

Ha Tang sends the domain.created, domain.updated and domain.deleted webhooks, with the domain as GET /domains/{id} shows it. A status change fires domain.updated.

One-click setup with Domain Connect​

If your DNS host supports Domain Connect, the domain page shows a DNS Records panel with Auto configure with your host's name. Click it, sign in at your DNS host and approve the change. The host adds the records, sends you back to Ha Tang and verification starts right away. Manual setup jumps to the table of records instead.

Domain Connect adds:

  • the return-path MX record and the SPF rule (include:amazonses.com, merged into an existing SPF record on that subdomain),
  • the DKIM record,
  • the DMARC record (v=DMARC1; p=none;),
  • the tracking CNAME, when the domain has a tracking subdomain.
note

Review the changes your DNS host shows before you approve them, especially if the domain already has a DMARC record you want to keep.

When the DNS host does not support Domain Connect but the domain is on Cloudflare, the domain page links straight to the zone's DNS records instead.

From the API:

# Can this domain be set up in one click?
curl https://api.hatang.dev/domains/$DOMAIN_ID/domain-connect -H "Authorization: Bearer $HATANG_API_KEY"
# { "object": "domain_connect", "available": true, "reason": null, "dns_host": "Cloudflare", "zone": "example.com" }

# Get the signed URL to send the user to
curl -X POST https://api.hatang.dev/domains/$DOMAIN_ID/domain-connect \
-H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" -d '{}'
# { "object": "domain_connect", "id": "dom_…", "url": "https://…" }

When available is false, reason is no_domain_connect (the DNS host does not support it), template_not_supported (it supports Domain Connect but not Ha Tang's template yet) or not_configured. POST takes an optional redirect_uri; without it, the user comes back to the domain page in the dashboard.

GET /domains/{id}/dns-provider tells you which DNS host serves the domain:

{ "object": "dns_provider", "zone": "example.com", "provider": "cloudflare", "nameservers": ["ada.ns.cloudflare.com", "bob.ns.cloudflare.com"], "cloudflare_url": "https://dash.cloudflare.com/…" }

Custom return path​

The return-path label is send unless you choose another one when you add the domain. Change it only if send.example.com is already in use for something else, for example by another email provider's bounce records. It cannot be changed afterwards: delete the domain and add it again with the new label.

Change or delete a domain​

PATCH /domains/{id} changes the settings that can change after creation:

curl -X PATCH https://api.hatang.dev/domains/$DOMAIN_ID \
-H "Authorization: Bearer $HATANG_API_KEY" -H "Content-Type: application/json" \
-d '{ "open_tracking": true, "click_tracking": true, "tracking_subdomain": "links" }'
FieldEffect
open_tracking, click_trackingTurn tracking on or off for emails sent from now on.
tracking_subdomainSet, change or remove (null) the branded tracking host. A new CNAME record appears to publish.
capabilities.receiving"enabled" or "disabled". See Receive email.

DELETE /domains/{id} removes the domain and its Amazon SES identity. Emails still queued for that domain bounce with "Sending domain no longer exists".

Troubleshooting​

SymptomCause and fix
A record shows found: nothingThe record is missing or under the wrong name. Check for the zone being added twice (see the caution above), then wait a few minutes for DNS to update.
DKIM fails but the record is thereThe value was cut or changed. Paste the full v=DKIM1; k=rsa; p=… value again. Some hosts need the long value in quotes or split into several strings.
SPF on send. failsThe subdomain has an SPF record without include:amazonses.com, or has two SPF records. Keep one v=spf1 record that includes include:amazonses.com.
All records verify but the status stays pendingAmazon SES has not confirmed DKIM yet. This usually follows the DNS within minutes; click Re-check DNS later.
The return-path MX conflicts with another recordsend.example.com already has a CNAME. A name with a CNAME cannot have other records. Remove the CNAME or add the domain again with another Custom Return-Path.
403 "The example.com domain is not verified" when sendingThe From domain is not added, not verified, or is a different subdomain. See Pick the domain to send from.
401 restricted_api_key when sendingThe API key is limited to another domain. Use a key for this domain or an unrestricted key.

Next steps​

View this page as Markdown