# Bulk-Adding Google Workspace Domain Aliases Without Service-Account Keys

Managing one domain in Google Workspace is straightforward. Managing ten, twenty, or more domains across several brands is where the process starts to hurt.

For every domain, an administrator has to prove ownership, publish a DNS record, wait for propagation, return to Google Workspace, add the domain, configure mail routing, and verify the result. None of those steps is especially difficult. The problem is repetition — and repetition is where small mistakes become outages.

I recently needed a better workflow for attaching a collection of brand domains to one Google Workspace account. The domains used GoDaddy DNS, and the intended behavior was simple: every existing Workspace user should receive the same username on every brand domain.

That made the domains good candidates for Google Workspace user alias domains. It also made the process a good candidate for automation. The result is a CSV-driven Python CLI with three commands — `plan`, `apply`, `verify` — that connects the Google Workspace Admin SDK, the Site Verification API, and the GoDaddy DNS API, authenticates with OAuth instead of a service-account key, and treats every write as something to check for before doing. Code's open source at [github.com/sirius93/google-workspace-domain-alias-provisioner](https://github.com/sirius93/google-workspace-domain-alias-provisioner), link at the bottom too.

## The problem with doing it manually

The manual workflow usually looks like this:

1.  Open the Google Admin console.
    
2.  Add a domain alias.
    
3.  Copy Google's verification token.
    
4.  Open the DNS provider.
    
5.  Create the TXT record.
    
6.  Wait for DNS propagation.
    
7.  Ask Google to verify the domain.
    
8.  Configure MX records.
    
9.  Repeat for the next domain. Do this once and it feels harmless. Do it twenty times and several risks appear:
    

*   A domain is skipped.
    
*   A TXT value is copied incorrectly.
    
*   The wrong parent domain is selected.
    
*   Existing MX records are replaced accidentally.
    
*   The spreadsheet and the real configuration drift apart.
    
*   Nobody has a reliable record of what succeeded. The goal was not merely to make the process faster. The goal was to make it repeatable, inspectable, and safe to rerun.
    

## Domain alias or secondary domain?

Before automating anything, it is important to choose the correct Google Workspace domain model.

A user alias domain gives existing users and groups equivalent addresses on another domain. If `brand.example` is an alias of `company.example`, then:

```plaintext
alex@company.example  -> alex@brand.example
sales@company.example -> sales@brand.example
```

Both addresses point to the existing account or group. Users continue signing in with their primary address, and there is no separate mailbox.

A secondary domain is different. It is intended for a separate collection of users whose primary accounts live on that domain. Secondary-domain users normally require their own Workspace licences.

The automation described here is specifically for user alias domains. That keeps the mapping predictable: one parent Workspace domain, many brand domains, and matching usernames across all of them.

Google documents the distinction and current domain limits in its multiple-domain guidance.

## Turning the domain list into a source of truth

The first useful improvement is surprisingly mundane: put the desired state in a CSV file.

```csv
domain,type,parent_domain,dns_provider
brand-one.example,domain_alias,company.example,godaddy
brand-two.example,domain_alias,company.example,godaddy
brand-three.example,domain_alias,company.example,godaddy
```

This file becomes the input to every operation. It is easy to review, version, split into pilot batches, and compare with the actual environment.

The script validates that:

*   Every domain is unique.
    
*   Every row has a parent domain.
    
*   The requested type is `domain_alias`.
    
*   The DNS provider is supported. That validation catches boring mistakes before an API call turns them into an operational problem.
    

## The automation flow

The workflow connects three APIs:

*   **Google Site Verification API** generates a DNS verification token and confirms ownership.
    
*   **GoDaddy Domains API v3** reads the zone and creates missing TXT or MX records.
    
*   **Google Workspace Admin SDK Directory API** creates and inspects domain aliases. For each CSV row, the tool performs this sequence:
    

1.  Check whether the domain is already verified.
    
2.  Check whether the Workspace alias already exists.
    
3.  Read the current GoDaddy DNS records.
    
4.  Request a Google verification token when needed.
    
5.  Add the token as an apex TXT record if it is missing.
    
6.  Poll public DNS until the token is visible.
    
7.  Ask Google to verify ownership.
    
8.  Create the Workspace domain alias under the configured parent.
    
9.  Optionally add Google MX when — and only when — the domain has no MX records. The read-before-write behavior is essential. A rerun should continue an incomplete job, not create duplicates.
    

## Why the CLI has plan, apply, and verify

Infrastructure tools are much easier to trust when inspection and mutation are separate operations.

The CLI exposes three commands:

```bash
python workspace_domain_aliases.py plan domains.csv
python workspace_domain_aliases.py apply domains.csv
python workspace_domain_aliases.py verify domains.csv
```

### plan

`plan` is read-only. It checks Google and GoDaddy, then prints a compact summary:

```plaintext
DOMAIN                           DNS      VERIFIED  WORKSPACE  MX
brand-one.example                ok       no        missing    0
brand-two.example                ok       yes       missing    5
brand-three.example              ok       no        present    1
```

This answers the questions that matter before a change: Can the tool access the DNS zone? Does Google already recognize the owner? Does the Workspace alias already exist? Is mail already configured?

### apply

`apply` creates missing ownership records, verifies domains, and creates missing Workspace aliases.

```bash
python workspace_domain_aliases.py apply domains.csv
```

DNS propagation is asynchronous, so the tool polls for the verification TXT record. If the record does not become visible before the timeout, the run reports the domain as pending. Nothing needs to be rolled back: wait, then rerun the same command.

### verify

`verify` reconciles the CSV with the live state, checking ownership, alias presence, parent-domain association, and whether the domain has at least one MX record. A non-zero exit code makes it usable in a shell script or a future CI workflow.

## Authentication without service-account keys

My first instinct was to use a service account with domain-wide delegation. That is a common design for unattended Workspace administration, but the organization enforced this policy:

```plaintext
iam.managed.disableServiceAccountKeyCreation
```

That was not a problem to work around. It was a useful security boundary.

Long-lived service-account JSON keys are portable credentials. If one leaks into a repository, backup, support ticket, or developer machine, it can be abused until revoked. Google recommends avoiding service-account keys when a safer authentication method fits the workload.

For a locally operated administrative tool, interactive OAuth is a good match. The application uses an OAuth 2.0 Desktop app client and asks the administrator to authorize two scopes:

```plaintext
https://www.googleapis.com/auth/admin.directory.domain
https://www.googleapis.com/auth/siteverification
```

The Python flow is conceptually simple:

```python
from google_auth_oauthlib.flow import InstalledAppFlow
 
scopes = [
    "https://www.googleapis.com/auth/admin.directory.domain",
    "https://www.googleapis.com/auth/siteverification",
]
 
flow = InstalledAppFlow.from_client_secrets_file(
    "oauth-client.json",
    scopes,
)
credentials = flow.run_local_server(port=0)
```

The first run opens a browser. The administrator signs in and approves access. The resulting refresh token is stored locally and reused on later runs.

That token must still be treated like a password. It should never be committed, pasted into an issue, or stored beside public example files.

## Connecting to Google Workspace

Once OAuth credentials are available, the Python client can construct both Google services:

```python
from googleapiclient.discovery import build
 
directory = build(
    "admin",
    "directory_v1",
    credentials=credentials,
    cache_discovery=False,
)
 
verification = build(
    "siteVerification",
    "v1",
    credentials=credentials,
    cache_discovery=False,
)
```

Creating a domain alias uses the Directory API:

```python
directory.domainAliases().insert(
    customer="my_customer",
    body={
        "domainAliasName": "brand.example",
        "parentDomainName": "company.example",
    },
).execute()
```

Before inserting, the tool lists existing aliases and compares the live parent domain with the CSV. If an alias already exists under a different parent, the script stops instead of guessing how to repair it.

## Automating domain verification

Google ownership verification has two distinct operations: obtain a token and verify the resource.

The request describes an internet domain and asks for DNS verification:

```python
result = verification.webResource().getToken(
    body={
        "site": {
            "identifier": "brand.example",
            "type": "INET_DOMAIN",
        },
        "verificationMethod": "DNS",
    }
).execute()
 
token = result["token"]
```

The token is published as an apex TXT record. Once it is visible in public DNS, the tool asks Google to verify the resource:

```python
verification.webResource().insert(
    verificationMethod="DNS",
    body={
        "site": {
            "identifier": "brand.example",
            "type": "INET_DOMAIN",
        }
    },
).execute()
```

Keeping these steps separate makes DNS propagation explicit. A successful API response from the DNS provider does not mean every resolver can see the record immediately.

## Adding records through GoDaddy safely

GoDaddy's v3 DNS API supports individual record creation:

```plaintext
POST /v3/domains/zones/{zone}/dns-records
```

A verification record looks like this:

```json
{
  "type": "TXT",
  "name": "@",
  "data": "google-site-verification=...",
  "ttl": 600
}
```

The safe pattern is:

1.  List existing records of the relevant type.
    
2.  Normalize record names and values.
    
3.  Create the record only when an exact match is absent.
    
4.  Never replace the entire DNS zone. This matters because DNS APIs differ. Some "set records" methods replace every record with a matching name and type. Others can replace the whole zone. A provisioning tool should use the narrowest operation available.
    

GoDaddy also warns that record creation is not inherently idempotent: blindly replaying a failed POST can create duplicates. Reading the zone before each write significantly reduces that risk.

## Why MX changes are opt-in

Verification TXT records are additive and relatively low-risk. MX records are different: they control where inbound email is delivered.

The tool therefore does not touch MX during the default `apply` operation. Mail configuration requires an explicit flag:

```bash
python workspace_domain_aliases.py apply domains.csv --add-google-mx
```

Even with the flag, the tool adds `smtp.google.com` only when the domain has zero MX records. If any MX record already exists, it prints a warning and leaves the configuration untouched.

That conservative decision handles several real-world cases: the domain already uses Google's older five-record MX configuration, the domain uses Google's newer single-record configuration, another mail provider is still active, or the domain has intentional routing or migration records.

Automation should not interpret "different from my preferred configuration" as "safe to overwrite."

SPF, DKIM, and DMARC are also intentionally outside the first version. Each requires knowledge of the organization's complete mail-sending environment. For example, adding a second SPF record can make SPF invalid rather than more secure.

## Making reruns safe

Bulk administrative work rarely completes in one flawless pass. DNS might be slow, one domain might belong to another account, an API can rate-limit a request, or an administrator might pause the rollout.

The tool is designed so that rerunning `apply` is normal:

*   Existing TXT token: skip creation.
    
*   Already verified domain: skip verification.
    
*   Existing Workspace alias with correct parent: skip insertion.
    
*   Existing MX records: leave unchanged.
    
*   Pending DNS: retry later. This is the practical meaning of idempotency for the workflow. The entire run may involve APIs whose individual POST operations are not idempotent, but the program checks the current state before deciding whether a write is necessary.
    

## A rollout strategy that minimizes surprises

For a real environment, I would use the following sequence:

1.  Export or back up every DNS zone.
    
2.  Put one non-critical domain in a pilot CSV.
    
3.  Run `plan`.
    
4.  Inspect any existing MX records.
    
5.  Run `apply` without the MX flag.
    
6.  Confirm the alias in the Google Admin console.
    
7.  Test receiving mail after the approved MX change.
    
8.  Confirm outbound authentication separately.
    
9.  Expand to small batches.
    
10.  Run `verify` and keep the output with the change record. The pilot is not ceremony. It confirms that the OAuth project, administrator privileges, Workspace tenant, GoDaddy account, authoritative nameservers, and API behavior all match the assumptions in the code.
     

## What I would add next

The first release solves one narrow problem well. Natural next steps include:

*   DNS-provider adapters for Cloudflare, Route 53, TransIP, and others
    
*   Secondary-domain support
    
*   JSON output for CI and audit systems
    
*   Unit tests with mocked API responses
    
*   Explicit DNS backup and rollback workflows
    
*   SPF, DKIM, and DMARC validation without automatic enforcement
    
*   A keyless workload-identity design for approved unattended execution Any future provider should preserve the same safety contract: read before write, create only what is missing, never replace a zone implicitly, and require explicit intent for destructive actions.
    

## Lessons learned

Three ideas mattered more than the API calls themselves.

First, model the desired state before writing code. The CSV is small, but it turns an informal checklist into something reviewable and repeatable.

Second, treat security controls as design input. A blocked service-account key did not require disabling an organization policy. It led to an authentication flow better suited to a human-operated CLI.

Third, make dangerous behavior inconvenient. The tool can automate MX creation, but only through an explicit flag, and it refuses to modify existing mail routing. A little friction is valuable when a command can affect production email.

That's usually the sweet spot for internal automation. I've open sourced it — if you're wiring up your own multi-brand Workspace setup, the CLI and the client wrappers are there to fork.

## Resources

*   **Source code:** [github.com/sirius93/ggwdap](https://github.com/sirius93/ggwdap) — plan/apply/verify CLI, CSV schema, and the Google + GoDaddy client wrappers described above.
    
*   Google Workspace Admin SDK Directory API
    
*   Google Site Verification API
    
*   Google Directory API Python quickstart
    
*   Google Workspace multiple-domain guidance
    
*   GoDaddy DNS API guide
    
*   Google Cloud authentication guidance
    

* * *

*This project is not affiliated with or endorsed by Google or GoDaddy. DNS and mail-routing changes can disrupt production services; test with a non-critical domain and keep current DNS backups.*
