Mailboxes

Order Google mailboxes on a domain in one call, and sign in with OAuth or an app password.

ResellerHQ sets up whole Google mailboxes for your customers: the Google account, the domain's mail DNS (MX, SPF, DKIM and DMARC) and two-step verification. You get each mailbox back with its sign-in details once it is ready.

It works in two steps:

  1. Order mailboxes on a domain, up to ten per domain. Name the domain and the mailboxes in one call. There is nothing to connect first.
  2. Read each mailbox's sign-in details once it is active, and connect to it in one of two ways.

A domain bought here needs nothing else. For a domain registered elsewhere, you also set its nameservers at its registrar.

Mailboxes can't be ordered by a sub-reseller yet: those calls answer 422 not_available.

Sandbox

In the sandbox, nothing is created at Google. Domains connect and mailboxes come up within seconds, and their sign-in details are realistic but don't work anywhere. A domain starting with taken- behaves like one already used with Google Workspace. On a domain starting with fails-, every mailbox fails during setup.

Price

curl https://resellerhq.com/api/v1/mailboxes/price \
  -H "Authorization: Bearer $RHQ_API_KEY"

price is what you pay per mailbox per month, in US cents, and perDomain is how many mailboxes a domain takes. A domain keeps the price it was connected at, for every mailbox on it, for as long as you keep it.

To fix what a customer pays per mailbox on one domain, send customerPrice to PATCH /v1/mailbox-domains/{id}. Otherwise their price list sets it, under the mailbox product. See Customer pricing.

Order mailboxes

POST /v1/mailboxes takes the domain and the mailboxes you want. Every mailbox is a sale to the domain's customer: its first month once it is active, then each renewal.

A domain bought here: one call, nothing to do

Send the domain's domainId (or its name) and the mailboxes:

curl -X POST https://resellerhq.com/api/v1/mailboxes \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "domainId": "dom_01K6...",
        "mailboxes": [
          { "username": "jo", "firstName": "Jo", "lastName": "Bloggs" },
          { "username": "sam", "firstName": "Sam", "lastName": "Lee" }
        ] }'

The answer has the mailboxes in data, and the domain they are on in mailboxDomain. We do the rest:

  • Nameservers. We point the domain at its mail nameservers ourselves. mailboxDomain.nameserversManaged is true and its status is propagating. It is never awaiting_nameservers.
  • Existing records. Its website, verification and other records are copied over, so nothing it serves stops. Its old MX, SPF, DKIM and DMARC records are not copied, because mail sets its own.
  • Customer. customerId defaults to the customer the domain was bought for.
  • Slow nameservers. If the domain is slow to move, we ask for fresh nameservers and point it again ourselves. We do this up to three times before the domain is failed.

If you pointed the domain's DNS somewhere else yourself (your own nameservers), we can't copy its records from there, so the order is refused with domain_dns_elsewhere. Send "moveNameservers": true to move it anyway, then set its records again under DNS.

A domain registered elsewhere: set its nameservers

A domain you don't hold here is checked with the registrar first. If it is already registered, whether to you at another registrar or to someone else, send its name and a customerId:

curl -X POST https://resellerhq.com/api/v1/mailboxes \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "northwind-mail.com", "customerId": "cus_01K6...",
        "mailboxes": [{ "username": "jo", "firstName": "Jo", "lastName": "Bloggs" }] }'

mailboxDomain.status is awaiting_nameservers and nameserversManaged is false. Set mailboxDomain.nameservers at the domain's registrar. The mailboxes wait as queued, paid for, and are set up once the domain points there. That usually takes minutes and can take up to a day. When it does, the domain becomes active and we send mailbox_domain.active.

If it never points there in time, it becomes failed. Call POST /v1/mailbox-domains/{id}/nameservers/regenerate, set the new nameservers, and we start checking again.

To get the nameservers before buying anything, connect the domain on its own first with POST /v1/mailbox-domains (free), then order on it later the same way.

A domain nobody has registered

When the registrar says the domain isn't registered anywhere, send "buy": true to register it (one year, to the customer's registrant or your account default) before the mailboxes are ordered:

curl -X POST https://resellerhq.com/api/v1/mailboxes \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "northwind-mail.com", "customerId": "cus_01K6...", "buy": true,
        "mailboxes": [{ "username": "jo", "firstName": "Jo", "lastName": "Bloggs" }] }'

registeredDomainId in the response is the domain just bought. It waits as awaiting_domain while the registration finishes, its mailboxes paid for, then connects itself (nameserversManaged: true) exactly like a domain you already held.

Without buy: true, that same request is refused with 422 domain_not_registered: nothing is connected, held or charged. A premium name is never bought here, with or without buy (422 premium_domain). Already registered somewhere, by the time buy: true tries it, answers 409 domain_taken. An ending we don't sell can't be asked whether it's registered either, so it connects as above, and buy: true on it answers 422 ending_not_sold. If the registrar can't be reached, this answers 502 check_failed and nothing is held; retry. If the domain was bought but the mailbox order then fails, the error's own message says so, and registeredDomainId names it in details too: it stays the customer's, nothing is charged twice.

POST /v1/mailboxes/bulk and the dashboard already worked this way; this section just brings the single-order route in line with them.

Every order

  • Adding more. Send POST /v1/mailboxes again with the same domain, or POST /v1/mailbox-domains/{id}/mailboxes. A domain takes 10 mailboxes in all; more is refused with mailbox_limit.
  • The first mailbox on a domain administers its Google account. Keep it: OAuth client IDs are granted through it.
  • Setup. Each mailbox is provisioning while it is set up, usually within 10 to 20 minutes and sometimes an hour or more. setupStage says how far it has got. When it is ready it becomes active and we send mailbox.active.
  • Payment. Your wallet is checked for every mailbox first, so if it is short the order is refused with insufficient_funds and nothing changes. Each mailbox's first month is held when you order, but only taken once that mailbox is active. A mailbox that fails (mailbox.failed, with a failure reason) costs nothing, and its money goes back to your wallet.

A domain already used with Google Workspace in another account is refused with google_workspace_unavailable. Remove it from that account first.

Several domains at once

POST /v1/mailbox-domains/check takes up to 50 domain names and says, for each, whether it is already connected (and its room for more mailboxes), held here, available to buy (with its price), registered elsewhere, or unusable, with a reason.

POST /v1/mailboxes/bulk then orders on up to 50 domains in one call, for one customerId. Send "buy": true on a domain the check called available to register it for a year first. Your wallet is checked for everything up front. After that each domain succeeds or fails on its own, and results says which. A domain still being registered comes back awaiting_domain, with its mailboxes paid for and set up once the registration finishes.

DNS once mailboxes are on it

Once mailboxes are on a domain, its DNS is hosted with them. Change records under /v1/mailbox-domains/{id}/dns/records, not under /v1/domains: those routes answer dns_managed_by_mailboxes for this domain. Its nameservers follow the same rule from the other side: PATCH /v1/domains/{id} refuses to move them away from the mail provider's own with 409 nameservers_used_by_mailboxes, naming the domain and its billable mailbox count, unless the same call also sends acknowledgeMailboxes: true - setting them back to what the mail provider already uses needs no flag. DNSSEC is refused outright, with 409 dnssec_not_available: there is nothing to sign while the zone is theirs.

Sign-in details

curl -X POST https://resellerhq.com/api/v1/mailboxes/mbx_01K6.../credentials \
  -H "Authorization: Bearer $RHQ_API_KEY"

This answers with:

  • password, the Google account password;
  • appPassword, for IMAP and SMTP;
  • totpSecret, the seed of the account's two-step verification;
  • the Gmail IMAP and SMTP settings.

Sign-in details are stored encrypted and appear on this route only. They are never in a list, a webhook or a log. Each time they are revealed, it is recorded in your activity log. Reading them needs the mailbox.credentials permission, and an API key needs at least the manage level.

When Google asks for a code at sign-in, POST /v1/mailboxes/{id}/totp gives you the current one. You can also generate it yourself from totpSecret in any authenticator app.

To change a mailbox's name, username (and so its address), picture or password, use PATCH /v1/mailboxes/{id}. A new password is 12 to 100 characters. To upload a profile photo instead of linking one, use POST /v1/mailboxes/{id}/picture.

Two ways to connect

Pick the one that fits your tool. Both work on every mailbox.

1. OAuth with a client ID

This is best for apps that already sign in with Google: CRMs, sequencers, your own app. Grant your app's Google OAuth client on the domain, and its mailboxes can then sign in to it without Google blocking an unknown app.

curl -X POST https://resellerhq.com/api/v1/mailbox-domains/mdm_01K6.../client-ids \
  -H "Authorization: Bearer $RHQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "clientId": "102938475610293847561" }'
  • Trusted app (the default). Each mailbox still signs in through your app's OAuth screen, once, with its password and code.
  • Domain-wide delegation ("delegated": true, with scopes). Your app (a Google service account) can act as any mailbox on the domain, with no sign-in per mailbox. Scopes are full URLs, such as https://mail.google.com/.

The request takes a few minutes. We send client_id.completed or client_id.failed, and GET /v1/mailbox-domains/{id}/client-ids lists every request with its status. The domain's first (admin) mailbox must be active first.

2. App password over IMAP and SMTP

Use this for any mail client or tool that takes a server, a username and a password. Use the app password, not the account password: Google refuses the account password over IMAP and SMTP.

HostPortSecurity
IMAP (receive)imap.gmail.com993SSL/TLS
SMTP (send)smtp.gmail.com587STARTTLS

The username is the full email address.

DNS

The domain's DNS is hosted with its mailboxes:

  • GET /v1/mailbox-domains/{id}/dns/records lists the records.
  • POST adds records, and PATCH and DELETE on /dns/records/{recordId} change or remove one.

Mail depends on some records: MX, the domain's SPF, its DKIM keys, its DMARC policy and its nameservers. These are marked protected, and changing them is refused with record_protected. Set DMARC through its own route instead.

POST /v1/mailbox-domains/{id}/dns/check says whether every mail record is in place. POST /v1/mailbox-domains/{id}/dns/repair puts back any that are missing or wrong.

Website forwarding and DMARC

  • PUT /v1/mailbox-domains/{id}/forwarding with { "url": "https://northwind.com" } redirects the domain's website. { "url": null } removes the redirect.
  • PUT /v1/mailbox-domains/{id}/dmarc with { "reportingEmail": "...", "policy": "quarantine" } sets the DMARC policy. DELETE removes it.

Renewal and deleting

Renewal. Every active mailbox on a domain renews together, monthly, on the date the domain's first mailbox was ordered (periodEndsAt). A mailbox added mid-month is charged a full first month and then renews with the rest.

Unpaid renewals. If your wallet cannot pay a renewal, we send mailbox_domain.renewal_failed with deletesAt and try again. If it is still unpaid three days later, the domain is deleted with all its mailboxes.

Deleting. A single mailbox cannot be deleted. Delete the whole domain:

curl -X DELETE https://resellerhq.com/api/v1/mailbox-domains/mdm_01K6... \
  -H "Authorization: Bearer $RHQ_API_KEY"

Every mailbox on it is cancelled at Google and stops renewing, and we send mailbox_domain.deleted. Nothing already paid is refunded. Money held for mailboxes not yet set up goes back to your wallet. A domain you hold here keeps its nameservers; change them on the domain if you want its DNS back here.

Webhooks

Every mailbox_domain.*, mailbox.* and client_id.* event is listed in the webhooks guide's Events table, with an example payload per family and the order a multi-mailbox order sends them in. mailbox.setup_progress is frequent and purely informational - see its setupStage values there.

No event ever carries a password or any other sign-in detail.