---
title: "Register a custom domain"
method: POST
path: "/domains"
tags: ["domains"]
---

# Register a custom domain

`POST /domains`

## Request

- Base URL: `https://dash.mailsocket.app/api/v1`
- URL: `https://dash.mailsocket.app/api/v1/domains`
- Auth: HTTP bearer

Creates a `pending_dns` Domain for the caller. Returns the
verification TXT host + token and the MX target to publish.
Custom domains are a Scale and Business feature (3 and 10 domains by
default): a plan that cannot have custom domains at all (Free/Pro;
`custom_domains` cap 0) gets `403 domain_entitlement_required`,
checked BEFORE the request body is validated. A Scale or Business
account that has used its own cap gets `403 domain_limit_reached`. `409 domain_taken` when the
name is already registered by any account, in any status.

## Headers

- `Idempotency-Key` string

## Request body

- DomainWrite
  - `name` string, required: The fully-qualified domain name to register (e.g. `otp.example.com`). Lower-cased before validation. Rejected (`422 validation_error`) if it is not a valid FQDN, or if it is a subdomain of `inboxpipe.net`/`mailsocket.app`. Rejected with `409 domain_taken` if already registered by any account.

## Response `201`

Domain created.

- object
  - `data` Domain, required
    - `id` string, required: The domain public ID.
    - `name` string, required: The FQDN, stored lowercase. Immutable after create.
    - `status` 'pending_dns' | 'verified' | 'failed' | 'disabled', required
    - `verification_txt_host` string, required: The TXT record host to publish (`_mailsocket-verify.<name>`).
    - `verification_token` string, required: The TXT record value to publish at `verification_txt_host`.
    - `mx_target` string, required: The MX target that must be present once DNS is live (`mx.mailsocket.app`).
    - `last_checked_at` string, date-time, nullable, required
    - `last_error` string, required
    - `verified_at` string, date-time, nullable, required
    - `catch_all` boolean: True when unmatched local parts on this (verified) domain route to catch_all_inbox_id. Toggled via `PATCH /domains/{id}/catch_all`.
    - `catch_all_inbox_id` string, nullable: The catch-all inbox's public ID, or null when catch-all has never been configured.
    - `created_at` string, date-time, required
    - `updated_at` string, date-time, required

## Other responses

- `400`: The `Idempotency-Key` header is empty, longer than 255 characters, or not printable ASCII, or the request body is too large to read with an `Idempotency-Key`.
- `401`: Missing or invalid API key.
- `403`: Account forbidden, BYOD disabled (`byod_disabled`), the plan does not include custom domains (`domain_entitlement_required`), or the plan's custom-domain limit is reached (`domain_limit_reached`).
- `409`: The domain name is already registered (`domain_taken`), or a request with the same `Idempotency-Key` is still running (`idempotency_in_progress`).
- `422`: The `Idempotency-Key` was already used with a different body or endpoint (`idempotency_key_reused`), or the request body failed validation.
- `429`: Rate limit exceeded.

---

[API](https://skmtc.dev/mailsocket/apis/mailsocket-api.md) · [All operations](https://skmtc.dev/mailsocket/apis/mailsocket-api/llms.txt) · [OpenAPI document](https://skmtc.dev/mailsocket/apis/mailsocket-api/revisions/e579500dfb7e?raw)
