> ## Documentation Index
> Fetch the complete documentation index at: https://docs.helohq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Automate customer DNS with NS subdomain delegation

> Manage DKIM and Return-Path records for every customer domain programmatically, by delegating a subdomain to nameservers you control in Cloudflare or Route 53

[Domain management for platforms](/platforms/platforms-domain-management) covers the standard flow: your customer adds a domain, you show them the DNS records Helo generates, and they paste those records in at their own DNS provider. That works well, but it means a manual DNS step for every customer, and it ties each customer's DNS directly to Helo's records. If you ever want to stop using Helo or add another email service provider to the mix, you have to go back to that customer and ask them to update their DNS records again.

An arguably better pattern is NS delegation, where your customer delegates a subdomain to your name servers so you can control the related DNS records instead. Doing so allows you to make future changes to their DNS records, which you might need to do if you want to:

* rotate or manage their DKIM keys for them
* add the ability to send to a new email service provider (ESP) or migrate from one ESP to another

That second point is especially powerful. If you're a platform, locking all or most of your customers to a single ESP, including Helo, can represent a significant business risk. We believe platforms should be free to use whichever ESP (or combination of ESPs) that works best for them, given the current circumstances, without arbitrary vendor lock-in.

This guide covers two ways to set that up. [Cloudflare DNS](#option-1-cloudflare-dns-recommended) is the recommended default because it supports custom (vanity) nameservers, so every customer delegates to the same branded set — such as `ns1.yourplatform.com` and `ns2.yourplatform.com` — instead of a Cloudflare-branded pair. That does require a Business or Enterprise plan, though. If you're using AWS and would prefer to use Route 53 instead, the [reusable delegation set approach](#option-2-route-53-if-youre-using-aws) is below.

## The one-time customer step: NS delegation

Instead of asking a customer to add Helo's specific TXT and CNAME records, ask them to add NS records that delegate a subdomain — for example `yourplatform.customerdomain.com` — to nameservers you control. Once that's in place, `yourplatform.customerdomain.com` is a zone you fully manage. You can add, change, or remove any record underneath it without ever going back to the customer, including if you rotate a DKIM key or change providers down the line.

The only difference between the two providers below is how you get those nameservers and where you write records afterward.

## Option 1: Cloudflare DNS (recommended)

Cloudflare is the recommended default because it supports custom (vanity) nameservers: you can serve every customer's zone from a single consistent, on-brand set like `ns1.yourplatform.com` and `ns2.yourplatform.com`. That gives you the same one-time-to-document nameserver set that Route 53 gets from a reusable delegation set, without tying customer DNS to AWS. Custom nameservers require a Business or Enterprise plan.

The mechanics are otherwise the same as any DNS provider: you add each customer's subdomain to your Cloudflare account as its own zone, and Cloudflare assigns that zone a pair of nameservers when you create it. You surface the appropriate nameservers to the customer and then manage all of the zone's records through the Cloudflare API.

<Info>
  [Account custom nameservers](https://developers.cloudflare.com/dns/nameservers/custom-nameservers/account-custom-nameservers/), the single set shared by every zone in your account, are available on Enterprise plans, and on Business plans after you contact Cloudflare Support. If you'd rather not depend on a Cloudflare plan, the [Route 53 approach](#option-2-route-53-if-youre-using-aws) is available on all AWS accounts.
</Info>

<Steps>
  <Step title="Add the customer's subdomain as a Cloudflare zone" titleSize="h3">
    Create the zone with an API token that has `Zone:Edit` and `DNS:Edit` permissions, scoped to **All zones** in your account so the token can create new zones:

    ```bash command-line theme={null}
    curl --location 'https://api.cloudflare.com/client/v4/zones' \
    --header 'Content-Type: application/json' \
    --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
    --data '{
      "name": "yourplatform.customerdomain.com",
      "account": { "id": "<cloudflare-account-id>" },
      "type": "full"
    }'
    ```

    The response returns the zone `id` — which you'll use for every DNS write — and `name_servers`, the pair of nameservers this zone is pinned to (for example `john.ns.cloudflare.com` and `melinda.ns.cloudflare.com`). Save both on the customer's record.

    <Info>
      If you're using Cloudflare's assigned nameservers, this pair differs per zone, so generate the NS records you show each customer from this response rather than hardcoding them. If you've configured [account custom nameservers](https://developers.cloudflare.com/dns/nameservers/custom-nameservers/account-custom-nameservers/), use those (`ns1.yourplatform.com`, `ns2.yourplatform.com`, and so on) for every customer instead — that's the single branded set that makes Cloudflare the recommended default.
    </Info>
  </Step>

  <Step title="Ask your customer to delegate their sending subdomain" titleSize="h3">
    This is the only manual DNS step your customer will ever do. Show them the two `name_servers` from step 1 and ask them to add NS records for the subdomain they want to send from:

    ```
    yourplatform.customerdomain.com.  NS  <nameserver-1-from-cloudflare>.
    yourplatform.customerdomain.com.  NS  <nameserver-2-from-cloudflare>.
    ```

    If you're using account custom nameservers, these are identical for every customer and can be baked straight into your onboarding docs or in-product instructions. If you're using the assigned nameservers, generate them per customer from the zone-create response.

    NS propagation isn't instant. Before moving on, confirm delegation has actually taken effect. `dig +trace` follows the delegation down from the root, so you can see the parent zone handing off to your nameservers:

    ```bash command-line theme={null}
    dig +trace NS yourplatform.customerdomain.com
    ```
  </Step>

  <Step title="Create the domain in Helo" titleSize="h3">
    Use the [Domains API](/api-reference/domains/create-a-domain) to register the subdomain in Helo, scoped to the [Channel](/core/channels) you use for this customer:

    ```bash command-line theme={null}
    curl --location 'https://api.helohq.com/domains' \
    --header 'Content-Type: application/json' \
    --header 'Accept: application/json' \
    --header "Authorization: Bearer $HELO_API_KEY" \
    --data '{
      "name": "yourplatform.customerdomain.com",
      "channelIds": ["<customer-channel-id>"]
    }'
    ```

    Helo's response includes the DKIM TXT record (`dnsRecords.domainKeyActive`) and the two Return-Path CNAME records (`dnsRecords.returnPath`) this domain needs. Each record has a `host` and a `value`. See [Domains](/core/domains) for what those records are and why Helo asks for two Return-Path records.
  </Step>

  <Step title="Write the records into the zone" titleSize="h3">
    Take the records from Helo's response and create them with the Cloudflare DNS records API — no customer involvement required. Create the DKIM TXT record first:

    ```bash command-line theme={null}
    curl --location 'https://api.cloudflare.com/client/v4/zones/<zone-id>/dns_records' \
    --header 'Content-Type: application/json' \
    --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
    --data '{
      "type": "TXT",
      "name": "<dkim-host-from-helo>",
      "content": "<dkim-value-from-helo>",
      "ttl": 300
    }'
    ```

    Then create each of the two Return-Path CNAME records Helo returned:

    ```bash command-line theme={null}
    curl --location 'https://api.cloudflare.com/client/v4/zones/<zone-id>/dns_records' \
    --header 'Content-Type: application/json' \
    --header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
    --data '{
      "type": "CNAME",
      "name": "<return-path-host-1-from-helo>",
      "content": "<return-path-value-1-from-helo>",
      "ttl": 300,
      "proxied": false
    }'
    ```

    Repeat that request for the second Return-Path record, using `<return-path-host-2-from-helo>` and `<return-path-value-2-from-helo>`.

    <Warning>
      Keep `"proxied": false` on the Return-Path CNAMEs. If Cloudflare proxies them, they resolve to Cloudflare's IPs instead of Helo's, which breaks Return-Path verification and bounce handling. These records must stay **DNS only**.
    </Warning>
  </Step>

  <Step title="Verify the domain and listen for the result" titleSize="h3">
    Trigger verification with the [Verify a domain](/api-reference/domains/verify-a-domain) endpoint, or let Helo's periodic checks pick it up:

    ```bash command-line theme={null}
    curl --location --request POST 'https://api.helohq.com/domains/<id>/verify' \
    --header "Authorization: Bearer $HELO_API_KEY"
    ```

    Subscribe a [webhook](/core/webhooks) to the `domain-key-verified`, `domain-key-verification-failed`, `return-path-domain-verified`, and `return-path-domain-verification-failed` events (optionally scoped to this customer's Channel) so your product can reflect real verification status without polling.
  </Step>
</Steps>

## Option 2: Route 53 (if you're using AWS)

If you're using AWS and would prefer to use Route 53 instead, a [reusable delegation set](https://docs.aws.amazon.com/Route53/latest/APIReference/API_CreateReusableDelegationSet.html) pins every customer's hosted zone to one fixed set of 4 nameservers. That means the NS records are identical for every customer, so you can document them once and never look them up per customer.

<Steps>
  <Step title="Create a reusable delegation set" titleSize="h3">
    Normally, every Route 53 hosted zone you create is assigned a random set of 4 nameservers. A [reusable delegation set](https://docs.aws.amazon.com/Route53/latest/APIReference/API_CreateReusableDelegationSet.html) flips that around: you create one fixed set of 4 nameservers up front, then assign every customer's hosted zone to that same set. Do this once, not per customer.

    ```bash command-line theme={null}
    aws route53 create-reusable-delegation-set \
      --caller-reference "helo-platform-delegation-set"
    ```

    The response includes an `Id` (something like `/delegationset/N1PA6795SAMPLE`) and 4 `NameServers`. Save both — you'll reuse the delegation set ID for every hosted zone you create, and the nameserver names are what you'll show every customer.

    <Info>
      Route 53 doesn't support renaming these to something like `ns1.yourplatform.com`. A reusable delegation set gets you *consistent* nameservers across every customer, not *vanity* ones — which is enough to document once and never look up per customer. True vanity nameservers mean moving to a DNS provider built for that specifically, and most platforms don't need it.
    </Info>
  </Step>

  <Step title="Create a hosted zone for the subdomain" titleSize="h3">
    Create a Route 53 hosted zone for the customer's subdomain, pinned to your reusable delegation set:

    ```bash command-line theme={null}
    aws route53 create-hosted-zone \
      --name "yourplatform.customerdomain.com" \
      --caller-reference "customer-1234-mail-domain" \
      --delegation-set-id "N1PA6795SAMPLE"
    ```

    Because the zone is created with your delegation set, its NS records automatically match the nameservers from step 1 — you don't set them manually. The response includes the hosted zone's `Id` (for example `/hostedzone/Z2SAMPLE1234`). Save it on the customer's record, because you'll use it for every DNS write.

    <Info>
      Create the hosted zone before the customer adds their NS records. If the customer delegates to nameservers that don't host a zone for the subdomain yet, lookups fail and the delegation is left dangling.
    </Info>
  </Step>

  <Step title="Ask your customer to delegate their sending subdomain" titleSize="h3">
    This is the only manual DNS step your customer will ever do. Show them the 4 nameservers from step 1, and ask them to add NS records for the subdomain they want to send from:

    ```
    yourplatform.customerdomain.com.  NS  ns-1487.awsdns-57.org.
    yourplatform.customerdomain.com.  NS  ns-406.awsdns-50.com.
    yourplatform.customerdomain.com.  NS  ns-870.awsdns-44.net.
    yourplatform.customerdomain.com.  NS  ns-1972.awsdns-54.co.uk.
    ```

    Because these NS records are identical for every customer, you can bake them straight into your onboarding docs or in-product instructions — no per-customer lookup needed.

    NS propagation isn't instant. Before moving on, confirm delegation has actually taken effect. `dig +trace` follows the delegation down from the root, so you can see the parent zone handing off to your nameservers:

    ```bash command-line theme={null}
    dig +trace NS yourplatform.customerdomain.com
    ```
  </Step>

  <Step title="Create the domain in Helo" titleSize="h3">
    Use the [Domains API](/api-reference/domains/create-a-domain) to register the subdomain in Helo, scoped to the [Channel](/core/channels) you use for this customer:

    ```bash command-line theme={null}
    curl --location 'https://api.helohq.com/domains' \
    --header 'Content-Type: application/json' \
    --header 'Accept: application/json' \
    --header "Authorization: Bearer $HELO_API_KEY" \
    --data '{
      "name": "yourplatform.customerdomain.com",
      "channelIds": ["<customer-channel-id>"]
    }'
    ```

    Helo's response includes the DKIM TXT record (`dnsRecords.domainKeyActive`) and the two Return-Path CNAME records (`dnsRecords.returnPath`) this domain needs. Each record has a `host` and a `value`. See [Domains](/core/domains) for what those records are and why Helo asks for two Return-Path records.
  </Step>

  <Step title="Write the records into the hosted zone" titleSize="h3">
    Take the records from Helo's response and write them into the hosted zone via the Route 53 API, using the hosted zone ID from step 2 — no customer involvement required:

    ```bash command-line theme={null}
    aws route53 change-resource-record-sets \
      --hosted-zone-id Z2SAMPLE1234 \
      --change-batch file://change-batch.json
    ```

    ```json change-batch.json theme={null}
    {
      "Changes": [
        {
          "Action": "UPSERT",
          "ResourceRecordSet": {
            "Name": "<dkim-host-from-helo>",
            "Type": "TXT",
            "TTL": 300,
            "ResourceRecords": [{ "Value": "\"<dkim-value-from-helo>\"" }]
          }
        },
        {
          "Action": "UPSERT",
          "ResourceRecordSet": {
            "Name": "<return-path-host-1-from-helo>",
            "Type": "CNAME",
            "TTL": 300,
            "ResourceRecords": [{ "Value": "<return-path-value-1-from-helo>" }]
          }
        },
        {
          "Action": "UPSERT",
          "ResourceRecordSet": {
            "Name": "<return-path-host-2-from-helo>",
            "Type": "CNAME",
            "TTL": 300,
            "ResourceRecords": [{ "Value": "<return-path-value-2-from-helo>" }]
          }
        }
      ]
    }
    ```

    <Warning>
      Route 53 limits each string in a TXT record to 255 characters, and a 2048-bit DKIM key is longer than that. Split the DKIM value into chunks of 255 characters or fewer, each wrapped in its own quotes and separated by a space, such as `"\"<first-255-chars>\" \"<rest-of-value>\""`. Receivers join the chunks back together when they look up the key.
    </Warning>
  </Step>

  <Step title="Verify the domain and listen for the result" titleSize="h3">
    Trigger verification with the [Verify a domain](/api-reference/domains/verify-a-domain) endpoint, or let Helo's periodic checks pick it up:

    ```bash command-line theme={null}
    curl --location --request POST 'https://api.helohq.com/domains/<id>/verify' \
    --header "Authorization: Bearer $HELO_API_KEY"
    ```

    Subscribe a [webhook](/core/webhooks) to the `domain-key-verified`, `domain-key-verification-failed`, `return-path-domain-verified`, and `return-path-domain-verification-failed` events (optionally scoped to this customer's Channel) so your product can reflect real verification status without polling.
  </Step>
</Steps>

## Good to know

### This assumes your customers are fine sending from a subdomain

Helo requires an exact match between the domain you verify and the domain you send from. If you verify `yourplatform.customerdomain.com`, mail has to come from an address on that exact domain (e.g. `hello@yourplatform.customerdomain.com`) — there's no way to verify a subdomain and send from its parent (`hello@customerdomain.com`).

You might expect DMARC's default relaxed alignment to get around this — under relaxed alignment, a DKIM signature from a subdomain (`d=yourplatform.customerdomain.com`) does align with a From header on the parent domain, since alignment only requires a shared organizational domain, not an exact match. That's true as a general DMARC mechanic (it breaks down only for the small minority of domains that publish `adkim=s`). Helo doesn't currently support it, though: domain verification, DKIM signing, and the From-address check on every send all key off an exact match today, with no subdomain/parent-domain logic in the pipeline. So the constraint holds in practice regardless of what DMARC alignment would technically permit.

It's worth knowing that even if this were supported, you'd probably want to avoid it anyway. Mailbox providers weigh the *visible* From domain heavily when building sender reputation, not just the authenticated identity DMARC checks. If a customer's mail shows `From: someone@customerdomain.com` while actually being signed and delivered through a subdomain, their root domain's reputation is on the hook for whatever gets sent through your platform — and vice versa, any reputation the root domain already has (good or bad) bleeds onto your platform's sending. That's exactly the mixing a dedicated sending subdomain is normally used to prevent (it's why Google's bulk sender guidelines recommend separating bulk mail onto its own subdomain in the first place). Exact-match domain verification, whatever its origin, ends up enforcing the isolation you'd want by default anyway.

So this entire pattern only works if your customers are okay sending from a subdomain rather than their bare domain. A subdomain still clearly belongs to the customer's brand, and most recipients don't scrutinize the From address that closely. Delegating an entire subdomain does require a certain level of trust and should be something your customers are comfortable doing though. It's worth noting that any subdomain can be used: it could be `yourplatform.customerdomain.com` or something else, including one of your customer's choosing.

If a customer specifically wants to send from their root domain, NS delegation of a subdomain won't get you there. You'd need them to add records directly at their root domain instead, which puts you back in the [manual, per-record flow](/platforms/platforms-domain-management) for that customer.

Worth confirming this is an acceptable tradeoff with a few customers before you build this out broadly.

### Delegation controls DNS, not reputation

This pattern solves the DNS bottleneck: once a customer delegates their subdomain, you never need to go back to them for another record change. It does not solve the reputation bottleneck, and it's worth being explicit about that with anyone building this.

Inbox providers build trust in a domain or IP gradually, based on how recipients respond to mail actually sent from it — not based on what DNS says is authorized to send. If you ever move a customer's sending from one ESP to another (including away from Helo), updating the records in the hosted zone is a quick process, but the new infrastructure behind those records still has no track record with inbox providers. Sending a customer's full volume through it on day one will look like a spam run, however clean the DNS is.

Treat a provider switch as a gradual cutover, not a DNS flip:

* Run both providers in parallel for a period rather than switching 100% of a customer's mail in one day.
* Shift volume over gradually, watching bounce and complaint rates as you go.
* Since each customer's sending is already isolated (by Channel, if you're on Helo), migrate customers in staggered cohorts instead of all at once — prove out deliverability on a few before moving the rest.

See [Protect your domain and IP reputation](/core/sending-guidelines#protect-your-domain-and-ip-reputation) and [Migrating to Helo from another ESP](/core/sending-guidelines#migrating-to-helo-from-another-esp) for the warm-up practices themselves — this delegation setup makes those migrations easier to execute (no per-customer DNS step required), but doesn't shorten how long they should take.

### This makes future changes a background job, not a support ticket

If Helo asks you to rotate a domain's DKIM key (via [Rotate a domain key](/api-reference/domains/rotate-a-domain-key)), or if you ever need to change providers, the update happens inside the zone you already control. Nobody emails the customer, and nobody's DNS needs to change on their end — the NS delegation they set up once keeps pointing at your infrastructure regardless of what's underneath it.

### Hosted zones aren't free on Route 53

Route 53 charges a small monthly fee per hosted zone. Factor that into your per-customer costs, and clean up zones for customers who churn. On Cloudflare, check how your plan prices and caps additional zones, and delete zones (for example, `DELETE /zones/<zone-id>`) when customers churn.

### Give propagation a moment before you automate the next step

If you're scripting this end-to-end, don't attempt verification immediately after asking the customer to add NS records. Propagation can take anywhere from a few minutes to a few hours, depending on the customer's DNS provider and how long resolvers have cached earlier answers for the subdomain. Confirm delegation resolves first, then write the records and verify.

## Related reading

* [Domain management for platforms](/platforms/platforms-domain-management) — the simpler, non-delegated approach
* [Domains](/core/domains) — what the DKIM and Return-Path records are and why Helo needs both
* [Channels](/core/channels) and [Separate your customers' sending](/platforms/platforms-channels)
* [Webhooks](/core/webhooks)
