Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7801,6 +7801,7 @@ paths:
tags:
- domains
post:
description: "Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict."
operationId: registerDomain
requestBody:
content:
Expand Down
5 changes: 4 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,10 @@ of `deleted: true`. Domain deletion adds the durable, open-set
Custom sending/receiving domains and their DNS verification.

- `GET /v1/domains`, `POST /v1/domains` — list / register (returns required MX +
TXT records and the DKIM selector/key).
TXT records and the DKIM selector/key). Registering a domain the account
already owns is idempotent: it returns the existing row (still 201) and
does not count against the domain cap. A different account's claim is a
409 `domain_taken` conflict.
- `GET /v1/domains/{domain}`, `DELETE /v1/domains/{domain}?confirm=DELETE` —
fetch / delete (delete deprovisions the sending identity; irreversible).
The deletion receipt's open `sending_teardown` value is the DNS-release
Expand Down
8 changes: 5 additions & 3 deletions docs/design/api-v1-redesign.md
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,8 @@ review diligence — a #206-style omission can't merge.
> exposes an optional `idempotency_key` tool arg so hosted-MCP callers can retry
> safely (the MCP hop is a network boundary the SDK auto-mint can't span). The
> create tools don't qualify: `create_agent` (email) and `register_domain`
> (domain) have natural unique keys (retry → 409, never a duplicate), and creates
> (domain) have natural unique keys (a retry reclaims the existing row with
> 201, never a duplicate), and creates
> are not the frequent/high-harm path — the rule is *mutating + duplicate-harmful
> + no natural dedup*, not call frequency. (`rotate_webhook_secret`, the one
> rare-but-dangerous non-create, is already idempotent from the GA-blockers work.)
Expand Down Expand Up @@ -1258,8 +1259,9 @@ worth making while we're reshaping the contract anyway, roughly in priority:
adding it to `create_agent`/`create_webhook`/`register_domain`; on review,
idempotency stays scoped to the send-family. The create tools don't meet the
bar (*mutating + duplicate-harmful + no natural dedup*): `create_agent` (email)
and `register_domain` (domain) have natural unique keys → retry is a 409, not a
duplicate. The send-family already carries it (and exposes the optional tool
and `register_domain` (domain) have natural unique keys: a retry reclaims
the existing row (201), not a duplicate. The send-family already carries it (and
exposes the optional tool
arg so hosted-MCP callers retry safely). Matches how mature email APIs scope it.
8. **Consistent vocabulary — resolved.** `send_email` mixed "email" with
`reply_to_message`/`forward_message`. Standardize the noun on the API
Expand Down
3 changes: 2 additions & 1 deletion internal/httpapi/domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ func (s *Server) registerDomains() {
registerOp(s.API, huma.Operation{
OperationID: "registerDomain", Method: http.MethodPost, Path: "/v1/domains",
Summary: "Register a domain", Tags: []string{"domains"},
Security: []map[string][]string{{"bearer": {}}}, DefaultStatus: http.StatusCreated,
Description: "Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.",
Security: []map[string][]string{{"bearer": {}}}, DefaultStatus: http.StatusCreated,
Responses: map[string]*huma.Response{
"402": s.limitExceededResponse(),
"409": s.jsonResponse(reflect.TypeOf(ErrorEnvelope{}), "ErrorEnvelope",
Expand Down
3 changes: 3 additions & 0 deletions sdks/python/src/e2a/v1/generated/api/domains_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,7 @@ async def register_domain(
) -> DomainView:
"""Register a domain

Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.

:param register_domain_request: (required)
:type register_domain_request: RegisterDomainRequest
Expand Down Expand Up @@ -966,6 +967,7 @@ async def register_domain_with_http_info(
) -> ApiResponse[DomainView]:
"""Register a domain

Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.

:param register_domain_request: (required)
:type register_domain_request: RegisterDomainRequest
Expand Down Expand Up @@ -1034,6 +1036,7 @@ async def register_domain_without_preload_content(
) -> RESTResponseType:
"""Register a domain

Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.

:param register_domain_request: (required)
:type register_domain_request: RegisterDomainRequest
Expand Down
1 change: 1 addition & 0 deletions sdks/typescript/src/v1/generated/apis/DomainsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export class DomainsApiRequestFactory extends BaseAPIRequestFactory {
}

/**
* Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.
* Register a domain
* @param registerDomainRequest
*/
Expand Down
2 changes: 2 additions & 0 deletions sdks/typescript/src/v1/generated/types/ObjectParamAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1628,6 +1628,7 @@ export class ObjectDomainsApi {
}

/**
* Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.
* Register a domain
* @param param the request object
*/
Expand All @@ -1636,6 +1637,7 @@ export class ObjectDomainsApi {
}

/**
* Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.
* Register a domain
* @param param the request object
*/
Expand Down
2 changes: 2 additions & 0 deletions sdks/typescript/src/v1/generated/types/ObservableAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,7 @@ export class ObservableDomainsApi {
}

/**
* Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.
* Register a domain
* @param registerDomainRequest
*/
Expand All @@ -1634,6 +1635,7 @@ export class ObservableDomainsApi {
}

/**
* Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.
* Register a domain
* @param registerDomainRequest
*/
Expand Down
2 changes: 2 additions & 0 deletions sdks/typescript/src/v1/generated/types/PromiseAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,7 @@ export class PromiseDomainsApi {
}

/**
* Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.
* Register a domain
* @param registerDomainRequest
*/
Expand All @@ -1175,6 +1176,7 @@ export class PromiseDomainsApi {
}

/**
* Registering a domain the account already owns is idempotent: it returns the existing row (still 201) and does not count against the domain cap. Registering a domain owned by a different account is a 409 domain_taken conflict.
* Register a domain
* @param registerDomainRequest
*/
Expand Down
Loading