Public API
The public API gives you tenant-scoped endpoints. Use them to manage domains, contacts, hosts, OAuth clients, memberships, and related resources. The API also has endpoints that do not need authentication. Use these endpoints to manage OAuth tokens, to accept an invitation, and to verify a contact.
Errors
Each response that is not a 2xx response uses this envelope:
{
"error": "<machine-readable code>",
"detail": { "..optional structured context.." },
"details": { "<field>": ["<message>",..] }
}
- The
errorfield is a stable snake_case identifier. A client can select an action from this value. - The
detailfield is optional. It contains structured data, for example{ "purge_due_at": ".." }. Each endpoint gives the content of its owndetailfield. - The
detailsfield contains a validation message for each field that is not correct. It occurs only with anunprocessable_entityerror from a validation failure.
Status code conventions
| Status | When | Retryable? |
|---|---|---|
| 400 | The request body is not correct, a necessary header is not present, or the Idempotency-Key is not correct |
No |
| 401 / 403 | The authentication failed, or the scope is not sufficient | No, until the credentials change |
| 404 | The resource is not in the scope of the caller | No |
| 409 | The current state does not agree with a precondition, for example wrong_state or idempotency_key_in_use |
Only after the state changes |
| 422 | A validation rule or a business rule failed | No |
| 429 | The rate limit stopped the request | Yes, after the time in Retry-After |
| 500 | The server has a defect | Yes, with a backoff delay |
| 502 / 503 / 504 | The upstream registry failed | Yes. Use an Idempotency-Key |
There are two different classes of 5xx error:
- 500 Internal Server Error — the registrar failed.
- 502 / 503 / 504 — the upstream registry failed. It sent a bad response, or it was not available, or it did not answer in time. The registrar operates correctly.
A client with a general retry policy for 5xx errors can retry both classes.
Idempotency
The POST, PATCH, and DELETE endpoints accept an optional Idempotency-Key header. This header makes it safe to send the same request again.
Idempotency-Key: <1–200 chars of [A-Za-z0-9_\-:.+]>
- The server does not interpret the key. Use a UUIDv4 or a ULID.
- The server holds each key in the scope of one tenant. For an endpoint that does not need authentication, the server holds the key in the scope of the credential or the IP address. Thus a key collision between two tenants is not possible.
- The server runs the first request in the usual manner. The server keeps the response for 24 hours. If a subsequent request has the same key, the server sends the same response again.
| Replay scenario | Result |
|---|---|
| The same key and the same body | The server sends the response that it kept. Idempotent-Replayed: true |
| The same key, but the first request is not complete | 409 idempotency_key_in_use with detail.in_flight: true. Retry after a short delay |
| The same key, but a different body | 409 idempotency_key_in_use. The server found a conflict |
| A key that is not correct | 400 malformed_idempotency_key |
| A 5xx error on the first attempt | The server does not keep the response. It runs the next attempt again |
The response has Idempotent-Replayed: false for the first execution. The response has Idempotent-Replayed: true for a replay.
Health Endpoints
These endpoints do not need authentication. They are not billed and they have no rate limit. Use them for load-balancer probes.
| Endpoint | Purpose |
|---|---|
GET /healthz |
Liveness. The endpoint returns 200 OK when the web process operates. |
GET /health |
Readiness. The endpoint reports the database connection and a summary of the registry circuit-breaker states. It returns 503 if the database ping fails. The status field is "ok", "degraded", or "fail". |
Authentication
OAuth 2.0 Bearer Tokens
Most endpoints require an OAuth 2.0 bearer token in the Authorization header:
Authorization: Bearer <access_token>
The token is a JWT with an HS256 signature. For each request, the service does these steps:
- It gets the token from the
Authorization: Bearer <token>header. - It verifies the JWT signature and the expiration time with the configured secret.
- It finds the
jti(JWT ID) of the token in theoauth_tokenstable. This shows if the token is revoked. - It puts the claims of the token into the request context. The claims include
sub, which is the current user ID, andtenant_id.
If the token is missing, invalid, expired, or revoked, the service returns 401 Unauthorized:
{
"error": "unauthorized"
}
Tenant Isolation
After the service validates the bearer token, all endpoints below /api/v1/tenants/:tenant_id do these checks for tenant isolation:
- The
tenant_idin the URL must agree with thetenant_idclaim in the bearer token. - The tenant must exist. The tenant must not be suspended.
- The authenticated user must have an active membership in the tenant.
If one of these checks fails, the service returns 403 Forbidden with one of these errors:
tenant_mismatch– the token is for a different tenanttenant_not_found– the tenant does not existtenant_inactive– the tenant is suspendednot_a_member– the user is not a member of this tenantmembership_deactivated– the membership of the user is not active
How to Get a Token
Get a token from the OAuth endpoints that follow. These endpoints do not need a bearer token.
OAuth Endpoints
These endpoints do not need authentication. They control the life cycle of a token.
Authorize (Authorization Code flow)
GET /oauth/authorize
This endpoint starts the Authorization Code flow with PKCE.
Query parameters:
| Parameter | Required | Description |
|---|---|---|
response_type |
yes | This value must be "code". |
client_id |
yes | The registered OAuth client ID. |
redirect_uri |
yes | This value must agree exactly with a registered redirect URI. |
code_challenge |
yes | The PKCE code challenge (S256). |
code_challenge_method |
yes | This value must be "S256". |
scope |
no | A list of the requested scopes, with a space between each scope. |
tenant_id |
no | The tenant for the authorization. |
state |
no | A value that the server does not interpret. The server sends it back in the response. |
Response: 200 OK with an HTML page. The service examines the browser session, and it then shows one of these pages:
- The login page, when the browser has no session.
- The TOTP page, when the user has a second factor.
- The consent page, with a tenant picker, when the user has a session.
Each page sends a POST request to a step endpoint that follows. At the end of the flow, the service sends a 302 Found response to redirect_uri?code=…&state=…. If the user denies the request, the service sends a 302 Found response with an error parameter.
You can use the authorization code one time only. The code expires after 10 minutes.
Authorize Steps
POST /oauth/authorize/login
POST /oauth/authorize/totp
POST /oauth/authorize/consent
The pages of the authorize flow use these endpoints. They accept a form body, and they need a CSRF token from the page. Do not use them from an API client: start the flow at GET /oauth/authorize instead.
Each body also contains the initial authorize parameters, thus the flow keeps its data between the steps.
| Endpoint | Body fields | Behavior |
|---|---|---|
POST /oauth/authorize/login |
email, password |
The service authenticates the user. If the credentials are correct, it sends a redirect to GET /oauth/authorize. If the user has a second factor, it shows the TOTP page. If the credentials are not correct, or if the account is locked for a short time, it shows the login page again with a message. |
POST /oauth/authorize/totp |
code |
The service examines the TOTP code. If the code is correct, it sends a redirect to GET /oauth/authorize. If the sign-in is expired, it shows the login page again. |
POST /oauth/authorize/consent |
approve or deny, tenant_id |
If the user approves, the service makes the authorization code and sends a 302 Found response to redirect_uri?code=…&state=…. If the user denies, it sends a 302 Found response with error=access_denied. |
The login endpoint and the TOTP endpoint have a rate limit for each source address.
Log Out
GET /oauth/logout
POST /oauth/logout
The POST endpoint revokes the session that agrees with the browser cookie, and it removes the cookie. The GET endpoint shows a small form that sends the POST request. The GET endpoint is for a user who selects a log-out link.
Query parameter or body field. It is optional:
| Field | Description |
|---|---|
post_logout_redirect_uri |
The URI for the browser after the log-out. The value must be a registered redirect URI of an OAuth client. |
Response:
204 No Contentwhen the request has nopost_logout_redirect_urivalue.302 Foundto that URI when the request has a permitted value.
Errors: 400 invalid_redirect_uri when the URI is not a registered redirect URI.
Exchange Token
POST /oauth/token
This endpoint changes credentials into an access token, and optionally into a refresh token. The result depends on the grant_type parameter.
grant_type: authorization_code
This grant type changes an authorization code into tokens.
Request body:
| Field | Required | Description |
|---|---|---|
grant_type |
yes | "authorization_code" |
code |
yes | The authorization code from the authorize step. |
code_verifier |
yes | The PKCE code verifier. This is the plaintext value whose hash is the initial challenge. |
redirect_uri |
yes | This value must agree with the redirect URI from the authorization step. |
grant_type: client_credentials
This grant type authenticates the client directly with its credentials.
Request body:
| Field | Required | Description |
|---|---|---|
grant_type |
yes | "client_credentials" |
client_id |
yes | The registered OAuth client ID. |
client_secret |
yes | The client secret. Confidential clients only. |
scope |
no | A list of the requested scopes, with a space between each scope. |
grant_type: refresh_token
This grant type changes a refresh token into a new pair of tokens.
Request body:
| Field | Required | Description |
|---|---|---|
grant_type |
yes | "refresh_token" |
refresh_token |
yes | A refresh token that is valid and not revoked. |
Token response
Each grant type that is successful returns this body:
{
"access_token": "jwt-access-token",
"token_type": "Bearer"
}
If the client has a refresh_token_ttl_seconds value, the response also has a refresh token:
{
"access_token": "jwt-access-token",
"refresh_token": "jwt-refresh-token",
"token_type": "Bearer"
}
Errors
400 Bad Requestwith{"error": "unsupported_grant_type"}if the grant type is not known.- Other errors occur if the client is not valid, if the code is expired, or if the PKCE values do not agree.
Revoke Token
POST /oauth/revoke
This endpoint revokes an access token or a refresh token.
Request body:
| Field | Required | Description |
|---|---|---|
token |
yes | The JWT token to revoke. |
Response: 200 OK with an empty body. The request is always successful, also if the token is already revoked or not known. This behavior agrees with RFC 7009.
Introspect Token
POST /oauth/introspect
This endpoint does RFC 7662 token introspection. A resource server, for example the MCP gateway, uses it to validate a bearer token. The resource server does not have to verify the JWT signature locally.
Request body (application/json or application/x-www-form-urlencoded):
| Field | Required | Description |
|---|---|---|
token |
yes | The token value to examine. |
Response for an active token: 200 OK
{
"active": true,
"client_id": "<oauth_client uuid>",
"exp": 1712000000,
"iat": 1711996400,
"scope": "space separated scopes",
"sub": "<user uuid or null>",
"tenant_id": "<tenant uuid>",
"token_type": "Bearer"
}
Response for a token that is not active, not valid, or not known: 200 OK
{
"active": false
}
Register Client (Dynamic Client Registration)
POST /oauth/register
This endpoint does RFC 7591 Dynamic Client Registration. The endpoint is
disabled by default. To enable it, set
config :registrar, Registrar.Auth.DCR, enabled: true. Each client that
this endpoint registers is public and thus has no client_secret.
Such a client can use only the authorization_code grant type with
PKCE. Such a client also has no tenant. The tenant_id query parameter
on GET /oauth/authorize selects the tenant for each token.
Request body:
| Field | Required | Description |
|---|---|---|
redirect_uris |
yes | An array of redirect URIs. |
client_name |
no | A name for a person to read. The default value is “Dynamically registered client”. |
scope |
no | A list of the requested scopes, with a space between each scope. |
Response: 201 Created
{
"client_id": "<new client_id>",
"client_name": "..",
"client_type": "public",
"grant_types": [
"authorization_code",
"refresh_token"
],
"redirect_uris": [
"http://127.0.0.1:51351/callback"
],
"response_types": [
"code"
],
"scope": "",
"token_endpoint_auth_method": "none"
}
Errors:
404 Not Found— DCR is disabled.422 Unprocessable Entity— theredirect_urisfield is not present, or the input is not valid.
Discovery Metadata
Authorization Server Metadata
GET /.well-known/oauth-authorization-server
This endpoint gives RFC 8414 metadata about the OAuth endpoints. The
registration_endpoint field occurs only if DCR is enabled. The
endpoint does not need authentication.
Unauthenticated Endpoints
These endpoints do not need a bearer token.
Preview Invitation
GET /api/v1/invitations/:token
This endpoint gives the public data of an invitation that is pending. Use it to show the correct form to the invitee before the invitee accepts or declines. The endpoint does not need a bearer token, because an invitee on the new-account path has no account. The endpoint has a rate limit for each token and for each source address.
Response: 200 OK
{
"data": {
"account_exists": false,
"email": "invitee@example.com",
"expires_at": "2025-01-08T00:00:00Z",
"role": "member",
"tenant_name": "Example Registrar"
}
}
The account_exists field tells you which acceptance path applies. If the value is false, the invitee must supply a password. If the value is true, the invitee must authenticate first.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
The token is not correct, or it is expired, or the invitation is not pending. |
| 429 | rate_limited |
The caller made too many requests for this token or from this address. |
Accept Invitation
POST /api/v1/invitations/:token/accept
This endpoint accepts an invitation that is pending. Use the invitation token from the email message. In a test environment, get the token from the operator API.
A bearer token is optional. Which path applies comes from the invited address:
- No account exists for the address. Send
{ "password": "<new password>" }. The service makes the account, records the address as verified, and makes the membership. Do not send a bearer token. - An account exists for the address. Authenticate as that user and send a bearer token. Do not send a password.
Request body:
| Field | Required | Description |
|---|---|---|
password |
on the no-account path only | The new password of the account. It must obey the password policy. |
Response: 200 OK
{
"data": {
"message": "invitation_accepted",
"user_id": "uuid"
}
}
Errors:
| Status | Code | Meaning |
|---|---|---|
| 401 | authentication_required |
An account exists for the address, but the request has no bearer token, or the actor is not a user. |
| 403 | invitation_email_mismatch |
The authenticated user has a different address than the invitation. |
| 404 | not_found |
The token is not correct, or it is expired, or the invitation is not pending. |
| 409 | account_exists |
The request has a password, but an account already exists for the address. |
| 422 | password_required |
No account exists for the address, and the request has no password. |
| 429 | rate_limited |
The caller made too many requests for this token or from this address. |
Decline Invitation
POST /api/v1/invitations/:token/decline
This endpoint declines an invitation that is pending. Only an invitee who has an account can decline. Authenticate as that user and send a bearer token. An invitee who has no account lets the invitation expire.
The invitation becomes declined, and the service makes no membership. The tenant can send a new invitation to the same address.
Request body: none.
Response: 200 OK
{
"data": {
"message": "invitation_declined"
}
}
Errors: the same as the errors for Accept Invitation, but account_exists and password_required do not occur.
Verify Contact
POST /api/v1/tenants/:tenant_id/contacts/:handle/verify
This endpoint sends the answer to a verification challenge for a contact. The registrant gets the challenge token in an email message or an SMS message. The registrant then sends the token to this endpoint. A bearer token is not necessary.
Request body:
| Field | Required | Description |
|---|---|---|
channel |
yes | The verification channel, for example "email" or "sms". |
token |
yes | The verification token from that channel. |
Response: 200 OK with the updated contact object.
When the contact was suspended because its verification period ended, it becomes active. After the response, a background job releases the contact_inaccuracy hold of each domain of which the contact is the registrant. The job tries a failed release again later. A hold for another reason stays. A domain with no other hold goes back to registered. When an investigation suspended the contact, the verification records the verified channel, but the contact stays suspended and its domains stay on hold. Only the operator can end that suspension. and § Reinstate Contact.
User Email Verification
Complete verification
POST /api/v1/users/verify
This endpoint does not need authentication. It uses a challenge token from the operator resend endpoint. It then sets the email address of the user to the verified condition.
The registrar also has an HTML form at GET /verify/user?token=<token>. A user who selects a link in an email message gets a confirmation page. That page sends a POST request to this endpoint.
Request body:
{
"token": "<opaque-challenge-token>"
}
Response: 200 OK
{
"data": {
"user": {
"email": "user@example.com",
"email_verified_at": "2026-04-24T12:00:00Z",
"id": "uuid"
}
}
}
Errors:
404 Not Found— the token does not agree with a challenge that is not expired. The token is not present, or it is not valid, or it is expired, or it is already used.422 Unprocessable Entity— thetokenfield is not in the request body.
Password Reset
This flow lets a user who forgot the password set a new one. Each token is a 256-bit random value. You can use a token one time only. A token expires after 24 hours. The server keeps only SHA-256(token). If the flow is successful, the server revokes each active OAuth session of that user in all tenants. The server keeps the TOTP enrollment.
Request reset
POST /api/v1/users/reset_password
This endpoint does not need authentication. The body is { "email": "user@example.com" }. The response is the same if the email address agrees with a user and if it does not. The shape and the approximate time are also the same. Thus a client cannot use this endpoint to find which email addresses are registered.
Response: 200 OK with { "status": "ok" }.
Errors: 400 invalid_email_format if the syntax of the input is not correct.
The registrar also has an HTML form at GET /reset-password?token=<token>. A user who selects a link in an email message gets the form for the new password. That form sends a POST request to the completion endpoint that follows.
Complete reset
POST /api/v1/users/reset_password/complete
This endpoint does not need authentication. The body is { "token": "<opaque>", "password": "<new>" }. The endpoint validates the token. The token must exist, it must not be used, and it must not be expired. The endpoint also validates the password, which must have a minimum of 12 characters. The endpoint then updates the user, uses the token, and revokes each active session.
Response: 200 OK with { "data": { "user": { "id": "uuid" } } }.
Errors:
400 invalid_token— the token is not correct, or it is not known, or it is already used, or it is expired. There is one error code for all these conditions. This is deliberate.422 unprocessable_entitywithdetails.password— the new password does not obey the policy.
Registrar-hosted Pages
The registrar serves these HTML pages itself. They give a person a way to complete an operation without an API client. An email message links to them when the tenant has no page of its own.
A page is a second front end onto the JSON endpoints above. It adds no rule: each precondition, each authorization test, and each error case is the same.
| Page | Purpose |
|---|---|
GET / |
The informational home page of the service. |
GET /docs/api |
This documentation, for a person to read. |
GET / POST /verify/user |
User email verification. The page sends the request to POST /api/v1/users/verify. |
GET / POST /verify/:tenant_id/:handle |
Contact verification, when the tenant has no verification_url_template value. The page sends the request to POST /api/v1/tenants/:tenant_id/contacts/:handle/verify. |
GET / POST /forgot-password |
The form that requests a password reset. |
GET / POST /reset-password |
The form that sets the new password. |
GET /invitations/:token |
The invitation page. It shows a form for a password when the invitee has no account, or the accept and decline actions when the invitee has an account and a session. |
POST /invitations/:token/accept |
The accept action of that page. |
POST /invitations/:token/decline |
The decline action of that page. |
GET /change-of-registrant/:token |
The consent page for a change of registrant. |
POST /change-of-registrant/:token/confirm |
The confirm action of that page. |
POST /change-of-registrant/:token/decline |
The decline action of that page. |
Each page has a CSRF protection and a rate limit for each source address. These paths give HTML, not JSON. Do not use them from an API client: use the JSON endpoints in this document.
Current User Endpoints
These endpoints below /api/v1/user need authentication. They are in
the scope of the user of the bearer token, which is the sub claim.
They are not in the scope of one tenant. The URL has no :tenant_id.
The service does not examine the tenant_id claim of the token.
List my tenants
GET /api/v1/user/tenants
This endpoint returns each tenant in which the authenticated user is an active member. The list does not include a suspended tenant or a membership that is not active.
Response: 200 OK
{
"data": [
{
"id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"membership_state": "active",
"name": "Acme Co",
"role": "owner",
"slug": "acme",
"status": "active",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}
TOTP enrollment and management
All four endpoints need a bearer token. The POST and DELETE verbs need the write scope. The GET verb needs the read scope.
GET /api/v1/user/totp
POST /api/v1/user/totp/enrollments
POST /api/v1/user/totp/enrollments/confirm
DELETE /api/v1/user/totp
GET /api/v1/user/totp returns:
{
"data": {
"status": "not_enrolled" | "pending" | "enrolled",
"enrolled_at": "2026-04-24T19:23:33Z" | null,
"recovery_codes_remaining": 10
}
}
POST /api/v1/user/totp/enrollments is idempotent when the user is pending. If the user is enrolled, the endpoint returns 409 already_enrolled. If the request is successful, the endpoint returns status 201 with { "data": { "secret": "BASE32", "otpauth_uri": "otpauth://totp/.." } }.
POST /api/v1/user/totp/enrollments/confirm has the body { "code": "123456" }. If the code is correct, the endpoint sets the user to enrolled. It then returns { "data": { "recovery_codes": [10 strings] } }. The endpoint shows the recovery codes one time only.
DELETE /api/v1/user/totp has the body { "code": "123456" } or { "recovery_code": ".." }. The endpoint removes the enrollment and returns 204. The caller must show that it has a current TOTP code or a recovery code.
Tenant-Scoped Endpoints
All the endpoints that follow need a valid bearer token. They are in the scope of one tenant at /api/v1/tenants/:tenant_id. The Authentication section above tells you how the service applies tenant isolation.
Tenant self-read
GET /api/v1/tenants/:tenant_id
This endpoint gives the tenant record of the caller. Each active member of the tenant can use it, and no special role is necessary. The necessary scope is registry:read.
Response: 200 OK with the tenant object. The object contains the same fields that the self-update endpoint gives, for example id, name, state, and transfer_out_mode.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 403 | tenant_mismatch |
The token is for a different tenant. |
Registrar information
GET /api/v1/tenants/:tenant_id/registrar_information
This endpoint gives the registrar information of the tenant that the tenant can publish. Each active member of the tenant can use it, and no special role is necessary. The necessary scope is registry:read. Only an operator can write this record. Use this projection on your registrar-services website, to obey [RAA §3.17].
Response: 200 OK
{
"data": {
"correspondence_address": {
"..": ".."
},
"legal_name": "Example Registrar, Inc.",
"primary_contact": {
"address": {
"..": ".."
},
"email": "compliance@example.com",
"name": "Jane Doe",
"phone": "+1.5555550100",
"title": "Compliance Officer"
},
"principal_address": {
"..": ".."
},
"ultimate_parent_entity": "Example Holdings, Ltd."
}
}
The principal_address field and the ultimate_parent_entity field occur only when they have a value. The response never contains the fields that the tenant must not publish.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
The tenant has no registrar-information record. |
Tenant self-update
PATCH /api/v1/tenants/:tenant_id
PUT /api/v1/tenants/:tenant_id
This endpoint updates the tenant attributes that you can change. The caller must be an owner or an admin of the tenant.
Request body. All the fields are optional:
| Field | Description |
|---|---|
name |
The name of the tenant, for a person to read. |
verification_from_email / suspension_from_email / renewal_from_email |
The From address for each type of notification. |
display_name |
The registrar name that registrants see in the From header and the text of each registrant email. |
verification_from_phone / suspension_from_phone |
The From number in E.164 format for SMS notifications. |
verification_url_template |
A URL template with a {{token}} placeholder. The service uses it to make a verification link. |
transfer_out_mode |
"auto", which is the default, or "explicit". This value controls how the service handles an outbound transfer that is pending. |
Response: 200 OK with the updated tenant object. The object contains transfer_out_mode.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 403 | forbidden |
The caller is not an owner or an admin. |
| 422 | unprocessable_entity |
A validation rule failed. For example, transfer_out_mode is not "auto" or "explicit", or a From address is not correct. |
Tenant TLD-association reads
GET /api/v1/tenants/:tenant_id/tld_associations
GET /api/v1/tenants/:tenant_id/tld_associations/:id_or_tld
These endpoints list the TLD associations of the calling tenant, or show one of them. The endpoints are read-only. Each member of the tenant can use them, and no special role is necessary. Only an operator can create, delete, or rebind an association, or change its configuration, credentials, or certificate.
Filters for the list, in the query string. All the filters are optional:
| Field | Description |
|---|---|
state |
Select the associations in this state, for example active. |
accreditation_type |
icann_accredited or non_icann. |
registry_account_id |
Select the associations that use this registry account. |
Identifier: the URL segment can be the association id, which is a
UUID, or the TLD label, for example com or co.uk. The service ignores letter case and a leading or trailing dot in the TLD label, and converts a U-label to its A-label. Thus COM and .com find the com association, and 网站 finds the xn--5tzm5g association. The lookup is in the scope of the tenant. An association of a
different tenant gives a 404 error.
Response: 200 OK with the same object that the operator endpoint
gives:
{
"data": {
"id": "uuid",
"tenant_id": "uuid",
"tld": "com",
"accreditation_type": "icann_accredited",
"state": "active",
"registry_account_id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z",
"config": { /* policy + notification settings */ },
"idn_language": {
"required": true,
"tags": [
{ "code": "AFR", "name": "Afrikaans" },
{ "code": "ALB", "name": "Albanian" }
]
}
}
}
The list endpoint returns data as an array of these objects, with one difference: its idn_language object has required only, for example "idn_language": { "required": true }. To get the tags of a TLD, read that one association.
The idn_language object tells you which values the idn_language field of Check and Register accepts for the TLD. The registry binding of the TLD sets it:
| Field | Description |
|---|---|
required |
true if an IDN name under the TLD needs idn_language on Register. Check accepts an IDN name with no tag. An IDN name is a name whose label to the left of the TLD is an A-label (xn--). |
tags |
The tags that the TLD accepts. Each entry has a code, which is the value to send, and a name, which you can show to a user. The list is empty when required is false. Only the response for one association has this field. |
Verisign .com and .net accept 112 three-letter codes, for example GER. Identity Digital TLDs accept 58 RFC 5646 tags, for example de and und-latn. The other TLDs need no tag and have an empty list.
The response never includes the EPP credentials or the certificate data.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 403 | forbidden |
The token does not agree with the tenant in the path. |
| 404 | not_found |
The identifier does not find an association of this tenant. |
Tenant TLD-association notification settings
PATCH /api/v1/tenants/:tenant_id/tld_associations/:id/notifications
This endpoint updates the notification settings of one association. The caller must be an owner or an admin of the tenant.
Request body. All the fields are optional:
| Field | Description |
|---|---|
low_balance_notification_email |
The email address that gets the low-balance warning and the critical alert from the registry for this association. To remove the address, set the field to "", an empty string. |
Response: 200 OK with the updated association object and its config map. The config map contains the new field. It also contains the credit fields for this association. The dispatcher writes these fields from the <lowbalance-poll> messages of the registry: last_low_balance_at, last_low_balance_warning_at, last_low_balance_critical_at, current_credit, current_credit_limit, current_credit_threshold, current_credit_at.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 403 | forbidden |
The caller is not an owner or an admin. |
| 404 | not_found |
The association ID is not an association of this tenant. |
| 422 | unprocessable_entity |
The low_balance_notification_email value is not correct. |
Members
List members
GET /api/v1/tenants/:tenant_id/members
This endpoint returns all the memberships of the tenant.
Response: 200 OK
{
"data": [
{
"id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"role": "owner",
"state": "active",
"tenant_id": "uuid",
"updated_at": "2025-01-01T00:00:00Z",
"user_id": "uuid"
}
]
}
Get member
GET /api/v1/tenants/:tenant_id/members/:user_id
This endpoint returns one membership.
Response: 200 OK with the membership object, or 404 Not Found.
Remove member
DELETE /api/v1/tenants/:tenant_id/members/:user_id
This endpoint removes the membership of a user from the tenant.
Response: 204 No Content
Update member role
PUT /api/v1/tenants/:tenant_id/members/:user_id/role
This endpoint changes the role of a member.
Request body:
| Field | Required | Description |
|---|---|---|
role |
yes | The new role, for example "owner", "admin", or "member". |
Response: 200 OK with the updated membership object.
Activate member
PUT /api/v1/tenants/:tenant_id/members/:user_id/activate
This endpoint makes a membership active again.
Response: 200 OK with the updated membership object.
Deactivate member
PUT /api/v1/tenants/:tenant_id/members/:user_id/deactivate
This endpoint makes a membership not active. The record of the member stays, but the member cannot get access to the resources of the tenant.
Response: 200 OK with the updated membership object.
Users
These endpoints give the identity of the users who hold a membership in the tenant. A membership record gives the role and the state. A user record gives the email address and the verification condition. The two reads are different, and one does not give the other.
The necessary scope is users:read or admin:write. The two scopes are an any-of set. A user who has no membership in the tenant is not visible: the service reports 404 not_found for that user ID, and also for an ID that is not known.
List tenant users
GET /api/v1/tenants/:tenant_id/users
This endpoint returns the identity of each user who holds a membership in the tenant. The list contains the users with an active membership and the users with a deactivated membership.
Response: 200 OK
{
"data": [
{
"email": "user@example.com",
"email_verified_at": "2025-01-01T00:00:00Z",
"id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"updated_at": "2025-01-01T00:00:00Z"
}
]
}
The response never contains a password, a second-factor secret, a lockout counter, or the memberships of the user in a different tenant.
Get tenant user
GET /api/v1/tenants/:tenant_id/users/:user_id
This endpoint returns the identity of one user who holds a membership in the tenant.
Response: 200 OK with the user object, or 404 not_found.
Invitations
List invitations
GET /api/v1/tenants/:tenant_id/invitations
This endpoint returns all the invitations of the tenant.
Response: 200 OK
{
"data": [
{
"accepted_at": null,
"email": "user@example.com",
"expires_at": "2025-01-08T00:00:00Z",
"id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"role": "member",
"state": "pending",
"tenant_id": "uuid"
}
]
}
Create invitation
POST /api/v1/tenants/:tenant_id/invitations
This endpoint invites a user to the tenant. The service sends the invitation token in an email message. The response does not contain the token.
Request body:
| Field | Required | Description |
|---|---|---|
email |
yes | The email address of the person that you invite. |
role |
yes | The role to give to that person after acceptance. |
Response: 201 Created with the invitation object, but without the token.
Revoke invitation
DELETE /api/v1/tenants/:tenant_id/invitations/:id
This endpoint revokes an invitation that is pending.
Response: 204 No Content
OAuth Clients
List OAuth clients
GET /api/v1/tenants/:tenant_id/oauth_clients
This endpoint returns all the registered OAuth clients of the tenant.
Response: 200 OK
{
"data": [
{
"access_token_ttl_seconds": 3600,
"allowed_flows": [
"client_credentials"
],
"allowed_scopes": [
"read",
"write"
],
"client_id": "hex-encoded-id",
"client_type": "confidential",
"id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"name": "my-integration",
"redirect_uris": [],
"tenant_id": "uuid"
}
]
}
Get OAuth client
GET /api/v1/tenants/:tenant_id/oauth_clients/:id
This endpoint returns one OAuth client.
Response: 200 OK with the client object, or 404 Not Found.
Register OAuth client
POST /api/v1/tenants/:tenant_id/oauth_clients
This endpoint registers a new OAuth client in the tenant.
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | The name of the client. |
client_type |
string | yes | "confidential" or "public". |
allowed_flows |
array | yes | One or both of ["authorization_code", "client_credentials"]. |
allowed_scopes |
array | yes | The scopes that the client can request. |
redirect_uris |
array | conditional | Necessary if allowed_flows contains authorization_code. |
access_token_ttl_seconds |
integer | no | The life of an access token in seconds. The default value is 3600. |
refresh_token_ttl_seconds |
integer | no | If you set this field, the service issues refresh tokens with this life. |
Response: 201 Created
For a confidential client, the response contains a client_secret field. The service shows this value one time only. It keeps the value as an Argon2 hash, and you cannot read the value again.
Revoke OAuth client
DELETE /api/v1/tenants/:tenant_id/oauth_clients/:id
This endpoint revokes an OAuth client. It also makes all the active tokens of that client not valid.
Response: 204 No Content
Rotate client secret
POST /api/v1/tenants/:tenant_id/oauth_clients/:id/rotate_secret
This endpoint makes a new client secret for a confidential OAuth client. The old secret becomes not valid immediately.
Response: 200 OK
{
"data": {
"client_id": "hex-encoded-id",
"client_secret": "new-plaintext-secret-shown-once"
}
}
The endpoint returns an error if the client is public, because a public client has no secret.
Sessions
List sessions
GET /api/v1/tenants/:tenant_id/sessions
This endpoint returns the active sessions of the current user in this tenant.
Response: 200 OK
{
"data": [
{
"expires_at": "2025-01-02T00:00:00Z",
"id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"ip_address": "127.0.0.1",
"user_agent": "Mozilla/5.0.."
}
]
}
Revoke session
DELETE /api/v1/tenants/:tenant_id/sessions/:id
This endpoint revokes one session.
Response: 204 No Content
Registry Client Certificate
Each RegistryAccount has a client TLS certificate. The EPP connection
to the registry uses this certificate for mutual TLS. The service
encrypts the certificate and its private key at rest. The private key
is write-only, and the API never returns it. A tenant can read the
certificate metadata, which includes the expiry date, the subject, the
algorithms, and the fingerprint. A tenant can also read the public
certificate in PEM format.
The tld_associations/:id/certificate paths are obsolete, but they
continue to operate for one release. They find the applicable registry
account.
Retrieve certificate metadata
GET /api/v1/tenants/:tenant_id/registry_accounts/:id/certificate
Response:
{
"data": {
"cert_pem": "-----BEGIN CERTIFICATE-----\n..",
"fingerprint_sha256": "abcdef..",
"id": "..",
"issuer": "CN=Registrar CA",
"key_size": 2048,
"not_after": "2027-04-22T00:00:00.000000Z",
"not_before": "2026-04-22T00:00:00.000000Z",
"public_key_algorithm": "RSA",
"registry_account_id": "..",
"rotated_at": "2026-04-22T00:00:00.000000Z",
"serial_number": "1a2b3c..",
"signature_algorithm": "sha256WithRSAEncryption",
"subject": "CN=registrar.example,O=Example Co,C=US"
}
}
The endpoint returns 404 Not Found if the account has no certificate.
Upload or rotate certificate
PUT /api/v1/tenants/:tenant_id/registry_accounts/:id/certificate
Body:
{
"cert_pem": "-----BEGIN CERTIFICATE-----\n..",
"key_pem": "-----BEGIN PRIVATE KEY-----\n.."
}
The server reads the two PEM values. It makes sure that the private key agrees with the certificate. It then keeps the two values encrypted at rest. You can use an RSA key or an EC key.
422 invalid_pem— the server cannot decode the PEM value.422 key_mismatch— the private key does not agree with the certificate.422 cert_pem_required/key_pem_required— the field is not present, or it is empty.
If the request is successful, the endpoint returns 200 OK with the same shape as the GET request.
Delete certificate
DELETE /api/v1/tenants/:tenant_id/registry_accounts/:id/certificate
This endpoint removes the certificate. An EPP connection that authenticates as this account then fails. To correct this condition, upload a new certificate. The endpoint is idempotent and returns 204 No Content.
Obsolete TLD-association paths
GET /api/v1/tenants/:tenant_id/tld_associations/:id/certificate
PUT /api/v1/tenants/:tenant_id/tld_associations/:id/certificate
DELETE /api/v1/tenants/:tenant_id/tld_associations/:id/certificate
These paths continue to operate. They use the certificate of the
registry account of the TLD association. For a new integration, use the
registry_accounts/:id/certificate paths.
Registry Accounts
These endpoints give a tenant read access to its registry accounts. A tenant can also set or clear the default account for a registry binding. Only an operator can create, rotate, disable, or delete the credentials.
List registry accounts
GET /api/v1/tenants/:tenant_id/registry_accounts
The optional query parameters are registry_binding and state. Each
item in the response has these fields: id, tenant_id,
registry_binding, name, state, is_default, rotated_at,
inserted_at, and updated_at. The response never contains the
credentials.
Set / clear default registry account
POST /api/v1/tenants/:tenant_id/registry_accounts/:id/set_default
POST /api/v1/tenants/:tenant_id/registry_accounts/:id/clear_default
These endpoints set or clear the default flag for one
(tenant_id, registry_binding) pair. See the
If a
binding has more than one active account, and the caller does not supply
registry_account_id, the service uses the default account. If the
account is disabled, set_default returns
422 registry_account_unavailable.
Domains
Check domain availability
POST /api/v1/tenants/:tenant_id/domains/check
This endpoint tells you if one or more domain names are available for registration. The endpoint accepts one name or many names, and the names can be in different TLDs. If you send many names, the endpoint makes one request to each registry binding.
Request body:
| Field | Required | Description |
|---|---|---|
name |
one of the two | One fully qualified domain name, for example "example.com". |
names |
one of the two | A list of fully qualified domain names. The list must not be empty. The names can be in more than one TLD and more than one registry binding. Do not use this field with name. |
idn_language |
no | The IDN language or script tag. You can check an IDN name without it, because Register is where the tag is necessary. The service applies the same tag to each name. It examines the tag only for an IDN name whose TLD association has idn_language.required set to true: for such a name, the tag must be a code from the idn_language.tags list of that TLD, and letter case is not important. Thus send a tag for IDN names in TLDs with different tag lists in different requests, or send no tag. The service does not examine the tag for an ASCII name, or for an IDN name in a TLD with an empty list. |
fee_commands |
no | A list of the fee-extension commands to show. Use one or more of "create", "renew", "transfer", and "restore". |
fee_period |
no | The period in years for the fee commands. The value must be a positive integer. The default value is 1. The endpoint ignores this field for restore. |
client_transaction_id |
no | An identifier from the caller. The response envelope contains it in client_transaction_id. |
This endpoint does not accept registry_account_id. The check goes to each applicable binding. For each binding, the service finds the account with the If a binding has more than one active account, set a default account with POST /registry_accounts/:id/set_default.
Response when you send one name in name: 200 OK
{
"data": {
"client_transaction_id": "caller-trid-001",
"fee_disclosure": "disclosed",
"fees": [
{
"amount": "9.99",
"command": "create",
"currency": "USD",
"period_years": 1,
"refundable": null
}
],
"name": "example.com",
"status": "available"
}
}
Response when you send many names in names: 200 OK. There is one result for each name, in the sequence that you sent:
{
"data": {
"results": [
{
"client_transaction_id": null,
"fee_disclosure": "not_requested",
"fees": [],
"name": "example.com",
"status": "available"
},
{
"client_transaction_id": null,
"fee_disclosure": "not_requested",
"fees": [],
"name": "example.io",
"status": "unavailable"
}
]
}
}
For a premium name, the entry also has "premium": { "marker": ".." }.
Each result has fee_disclosure. It tells you what happened to your fee_commands request:
| Value | Meaning |
|---|---|
not_requested |
You sent no fee_commands. |
disclosed |
The registry binding asked the registry for the fees. The fees list holds the answer. The list can be empty for a name with no special fee. Some registries price each command and have no command selector. For those, the list holds a fee for each command that the registry prices, and not only the commands that you asked for. |
unsupported |
You asked for fees, and the binding cannot ask the registry. The fees list is empty. Verisign .com and .net have no fee extension: each name has the same price, which you get from the rate card and from POST /pricing/quote. |
If the call to one registry fails, each name in the group of that binding has status: "error". The results from the other bindings stay correct. A result with status: "error" also has a detail object:
{
"detail": {
"message": "EPP 2001: Command syntax error",
"registry_code": 2001
},
"fees": [],
"name": "example.com",
"status": "error"
}
detail.registry_code is the result code from the registry, or null if the service could not reach the registry. detail.message holds the text from the registry for the result codes that you can act on, and is null for every other code.
IDN language error, 422: invalid_idn_language if you send a tag that is not in the idn_language.tags list of the TLD of an IDN name. The service then sends no request to a registry.
List domains
GET /api/v1/tenants/:tenant_id/domains
This endpoint returns the domains of the tenant. Use the query parameters to filter the list and to get one page at a time.
Query parameters:
state— select the domains in this life-cycle state.tld— select the domains in this TLD. The case of the letters is not important.name_like— select the domains whose name contains this text. The case of the letters is not important.updated_after/updated_before— RFC 3339 times. Select the domains whoseupdated_atvalue is in this range.expires_within_days— a positive integer. Select the domains whoseexpires_atvalue is between now and now plus N days. A domain that is already expired does not agree with this filter.renewed_within_days— a positive integer. Select the domains whoselast_renewed_atvalue is in the last N days. A domain with no renewal does not agree with this filter.in_grace— iftrue, select the domains in the grace period for auto-renewal. These domains havestate=expiredandexpires_at >= now − tld_config.auto_renewal_grace_period_days.in_redemption— iftrue, select the domains withstate=redemption.nameserver— a fully qualified host name. Select the domains whose nameservers include this host. The case of the letters is not important. If the FQDN is not correct, the endpoint returns 422.unicode_name_like— select the domains whose U-label (unicode_name) contains this text. Usename_liketo search the A-label.out_of_sync— a boolean. Iftrue, select the domains whose row does not agree with the data at the registry. Iffalse, remove those domains from the list.registrant_verification_state— one ofpending_verification,active,suspended, ordeleted. The service uses the registrant contact to apply this filter.order_by— one ofname,registered_at,expires_at,updated_at, orlast_renewed_at. Use this parameter only in offset mode. If you also usecursor, the endpoint returns 422.order—asc, which is the default, ordesc. The endpoint ignores this parameter iforder_byis not present.limit— the number of items on one page. The default value is 100 and the maximum value is 500.cursor— the token frompagination.next_cursorin the previous response. Do not use it for the first page.offset— an integer that is 0 or more. Useoffsetin place ofcursorto get the pages by number. The response then also has atotalcount.
If you supply cursor and offset, the endpoint uses cursor.
Response in cursor mode: 200 OK
{
"data": [..],
"pagination": { "next_cursor": "opaque-token-or-null", "has_more": true }
}
Response in offset mode: 200 OK
{
"data": [..],
"pagination": { "offset": 0, "limit": 100, "total": 237, "has_more": true }
}
Each item in data has the shape:
{
"acceptance": {
"acceptance_method": "api",
"accepted_at": "2025-01-01T00:00:00Z",
"agreement_version_id": "v_abc123",
"reacceptance_required": false,
"redemption_fee_disclosure": {
"amount": "79.0000",
"currency": "USD"
},
"state": "active",
"version_effective_at": "2025-01-01T00:00:00Z"
},
"auto_renew_grace_ends_at": null,
"auto_renewal": {
"enabled": true,
"grace_period_days": 30,
"mode": "AUTORENEW",
"opt_out_url": null,
"renewal_fee": {
"amount": "9.99",
"currency": "USD"
}
},
"auto_renewal_consented": false,
"deletion_scheduled_at": null,
"epp_statuses": [
"ok"
],
"expires_at": "2026-01-01T00:00:00Z",
"hold_reason": null,
"id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"last_reconciled_at": null,
"last_renewed_at": null,
"name": "example.com",
"nameservers": [
"ns1.example.com"
],
"out_of_sync": false,
"registered_at": "2025-01-01T00:00:00Z",
"registry_account": {
"id": "uuid",
"name": "primary",
"registry_binding": "verisign"
},
"registry_account_id": "uuid",
"state": "registered",
"tenant_id": "uuid",
"tld": "com",
"transfer_lock_until": null,
"underlying_hold_reason": null,
"updated_at": "2025-01-01T00:00:00Z"
}
The acceptance object is a short view of the current acceptance of the Registrant Agreement for this domain. The object is null if there is no active acceptance, for example immediately after a termination. The /agreements/acceptance/:domain_name endpoint gives the full evidence, which includes source_ip, user_agent, content_hash, and the termination history. If reacceptance_required is true, a new agreement version with a material change occurred after the acceptance of the domain. The registrant must then accept the agreement again, as [RAA §3.7.7] specifies.
The acceptance.redemption_fee_disclosure field gives the recovery fee for the redemption grace period. The service showed this fee to the registrant at registration, as [RAA §3.7.5.6] specifies. The Acceptance Record keeps this value, and no operation can change it. The field is null for a non-ICANN TLD, because the disclosure obligation does not apply to it. The field is also null for an acceptance from before the service recorded this disclosure.
The auto_renewal object is the disclosure of the auto-renewal grace period that [RAA §3.7.5.1] specifies. The mode field is the setting for this domain. It is one of DEFAULT, AUTORENEW, AUTOEXPIRE, RENEWONCE, or AUTODELETE. The enabled field is a boolean form of the applicable mode. The applicable mode is the mode of the domain, or, if the domain has no mode, the default mode of the TLD. The enabled field stays in the object for compatibility with an older version. If enabled is true, grace_period_days gives the number of days after the expiration in which you can renew the domain at the usual price. The renewal_fee field then gives that price. On every TLD, grace_period_days is the grace period of the TLD minus 5 days, because the service cancels a domain with no renewal consent 5 days before the registry grace period ends. If enabled is false, the TLD does not renew a domain automatically, and renewal_fee is null. The opt_out_url field is null at this time. It is reserved for a subsequent opt-out flow. The key is always present, thus a client can use the same shape.
The hold_reason field gives the reason of the hold of an on_hold domain. A dispute hold can go on top of a hold for a different reason. The underlying_hold_reason field then gives the reason of that other hold, and hold_reason is dispute. In all other cases underlying_hold_reason is null.
The service sets last_renewed_at for each renewal that a caller starts, and for each auto-renewal that the registry acknowledges. The field is null for a domain with no renewal after its registration. You can also sort on this field with order_by=last_renewed_at, and filter on it with renewed_within_days.
The auto_renew_grace_ends_at field gives the end of the registry grace period after a registry auto-renewal that the service adopted. It stays set after the grace period ends, until a renew, a delete, a restore or the next adoption. The auto_renewal_consented field is true when that auto-renewal has consent: the auto-renewal mode gave consent when the service adopted it, or a later auto-renewal toggle gave consent. The service then does not cancel the renewal. In the grace period, an auto-renewal toggle sets the field from the new mode, thus a toggle to a mode with no consent cancels the renewal. After the grace period ends, a change of the mode does not cancel the renewal. The field is false when there is no adopted auto-renewal, or when it had no consent. Read the two fields together.
Get domain
GET /api/v1/tenants/:tenant_id/domains/:name
This endpoint returns one domain. Use the fully qualified name of the domain.
Response: 200 OK with the domain object, or 404 Not Found.
Register domain
POST /api/v1/tenants/:tenant_id/domains
This endpoint registers a new domain. The request can include an acceptance object for the registrant agreement.
Request body: the parameters for the registration, which include the fully qualified name and the contact handles. The body can also include an acceptance block. The service adds the source IP address and the user agent of the request to the acceptance.
Payment assurance, as [RAA §3.7.4] specifies: the request must have a quote_id. This applies to each TLD. The quote must come from POST /quotes, and it must have the same fqdn, operation = "register", period = years, tier, and premium_class. You can use a quote one time only. If you use it again, the endpoint returns 422 quote_consumed.
| Field | Required | Description |
|---|---|---|
quote_id |
yes | The UUID of the quote from POST /quotes. |
registry_account_id |
conditional | Necessary if the tenant has more than one active RegistryAccount for the registry binding of the TLD. If only one account agrees, the registrar selects that account. The service keeps the selected account on the domain row, and each subsequent EPP command for that domain uses it. |
idn_language |
conditional | The IDN language or script tag for the IDN extension of the registry. The rules apply only to an IDN name whose TLD association has idn_language.required set to true. For such a name, the field is necessary and must be a code from the idn_language.tags list of that TLD. Letter case is not important, and the service sends the code as the list shows it. For an ASCII name, or for an IDN name in a TLD with an empty list, the service does not examine the tag and passes it to the registry binding unchanged. |
Nameservers: the optional nameservers field takes a list of host FQDNs. The service makes a host row automatically for an external name. A subordinate name is a name below a domain of this tenant. Such a name must already exist as an active host, because the operator must supply its IP addresses.
Response: 201 Created with the domain object.
A name that the tenant held before: when the tenant has a deleted domain row for the name, for example after a delete or a transfer out, the service reuses that row. The domain keeps its id, and its events and earlier acceptance records stay. All the other fields are those of the new registration. The service cancels each open change of registrant request of the earlier registration. These records of the earlier registration have no effect on the new one: an agreement termination, a dispute and its deletion clock, and a register quote. An abuse case of the earlier registration still applies to the domain.
Name in use, 422: not_available if the tenant has a domain row for the name that is not deleted. The service sends no request to the registry and does not consume the quote.
Payment-assurance errors, all 422: quote_required, quote_not_found, quote_consumed, quote_expired, quote_mismatch, payment_not_assured.
Registry-account errors, all 422: registry_account_required, for which the response also has meta.candidates: [{id, name},..], registry_binding_mismatch, and registry_account_unavailable.
Nameserver errors, all 422: invalid_nameservers, duplicate_nameserver, and subordinate_host_missing, for which the detail field has the FQDN.
The registry can refuse to create the host object for a nameserver. The usual cause is that a different registrar sponsors the parent domain of the host, or that the parent domain does not exist. The endpoint then returns 422 nameserver_host_not_registered with detail.host_name, and does not register the domain.
IDN language errors, both 422: idn_language_required if an IDN name has no idn_language and its TLD requires one, and invalid_idn_language if the tag is not in the idn_language.tags list of the TLD. The service then sends no request to a registry. The registry can also refuse a tag from the list, for example when the TLD does not offer that table. Identity Digital TLDs return that refusal as unknown_idn_table.
Reconcile domain with registry
POST /api/v1/tenants/:tenant_id/domains/:name/reconcile
This endpoint gets the current data for the domain from the registry
with the EPP domain:info command. It then writes the fields that can
change to the local row. The table shows what occurs to each field:
| Field | Reconciled? | Notes |
|---|---|---|
nameservers |
yes | The service uses the same path as update_nameservers. An unknown external name becomes a host row. A subordinate host of the domain at the registry with no active host row becomes one, with the registry IP addresses. Another unknown subordinate name causes an error. |
epp_statuses |
yes | The data from the registry has priority. |
expires_at |
yes | The data from the registry has priority. |
registrant_handle / admin / tech / billing |
no | The tenant data has priority. The registry handles have no known structure, and the service does not map them to the contacts of the tenant. |
auth_code_hash |
no | The operation only reads the value. It does not change the value. |
If one or more fields change, the service emits a
reconciled_with_registry domain event with the changes. If no field
changes, the service emits no event.
If the domain is redemption and the registry no longer recognizes the registrar as the sponsor, the service changes the domain to deleted. It records a deleted_finalized domain event with payload.source set to reconcile, and it writes one domain.purged notification.
When the expires_at of the registry is later than the local value, the service adopts a registry auto-renewal. A restore that moved the date is not an auto-renewal. For an auto-renewal, the service sets last_renewed_at and auto_renew_grace_ends_at, and it writes one domain.auto_renewed notification. The service also records whether the auto-renewal mode gave consent at that time. The renewal cancellation does not cancel an auto-renewal with a recorded consent. A toggle in the grace period can change that consent. A field that a different operation changes during the registry read keeps its local value until the next reconcile.
Response: 200 OK with the updated domain object.
Error conditions, all 422:
wrong_state: the domain is notregistered,on_holdorredemption.not_at_registry: the registry returnedobject_does_not_existfor a domain that is notredemption.subordinate_host_missing: the nameserver list of the registry has a subordinate host that the tenant did not create. The operator must supply the IP addresses of such a host, thus the service cannot make it.
The endpoint returns 404 Not Found if there is no local row for :name.
Renew domain
POST /api/v1/tenants/:tenant_id/domains/:name/renew
This endpoint renews a domain that is registered, on_hold or expired. A held domain stays on_hold with its hold reason. A domain cannot be renewed while a URS dispute of the domain is filed or complainant_won. A UDRP dispute does not stop the renewal.
Request body:
| Field | Required | Description |
|---|---|---|
years |
no | The number of years to add. The default value is 1. |
quote_id |
yes | The UUID of a quote from POST /quotes with operation = "renew" and period = years. |
Response: 200 OK with the updated domain object.
Auto-renew grace period: when auto_renew_grace_ends_at is in the future, the registry already renewed the domain for one year. The renew counts that year. A 1-year renew sends no request to the registry, keeps expires_at, and clears auto_renew_grace_ends_at. A renew of N years renews N - 1 years at the registry. The quote stays for N years. When expires_at has passed, the service first reconciles the domain with the registry, thus it finds an auto-renewal that it did not adopt yet. A reconcile error fails the renew and does not consume the quote.
Payment-assurance errors, all 422: quote_required, quote_not_found, quote_consumed, quote_expired, quote_mismatch.
Registry status error, 422: object_status_prohibits_operation if a status of the domain at the registry prohibits the renewal. Examples are a pending transfer and serverRenewProhibited.
Dispute error, 422: domain_on_hold_dispute if a URS dispute of the domain is filed or complainant_won.
Update domain nameservers
POST /api/v1/tenants/:tenant_id/domains/:name/update_nameservers
This endpoint replaces all the nameservers of a registered domain, or of a domain that is on_hold for contact inaccuracy or abuse. You must send the full set of nameservers, not only the changes. If the new set is the same as the current set, the endpoint does nothing and does not speak to the registry. A held domain keeps its hold, thus it does not resolve until the hold is released.
Request body:
| Field | Required | Description |
|---|---|---|
nameservers |
yes | The new set of host names as FQDNs. If an external name has no Host row, the service makes one automatically, but with no IP addresses. A subordinate name is a name below a domain of this tenant. Such a name must already exist as an active host of the tenant, because the operator must supply its IP addresses. See /hosts. |
Response: 200 OK with the updated domain object.
Error conditions. The endpoint returns 422 with one of these codes: subordinate_host_missing if a subordinate host does not exist, and the detail field then has the FQDN; duplicate_nameserver if the new set has the same name two times; invalid_nameservers if the syntax is not correct; empty_nameservers if the set is empty; wrong_state if the domain is in a different state; domain_on_hold_dispute if the domain is on_hold for a dispute; update_prohibited if the domain has the clientUpdateProhibited or serverUpdateProhibited status.
The registry can refuse to create the host object for a new nameserver. The usual cause is that a different registrar sponsors the parent domain of the host, or that the parent domain does not exist. The endpoint then returns 422 nameserver_host_not_registered with detail.host_name, and the domain keeps its current nameservers.
Replace DS records
POST /api/v1/tenants/:tenant_id/domains/:name/update_ds_records
This endpoint replaces all the DS records of a registered or on_hold
domain. You must send the full set of DS records. If the new set is the
same as the current set, the endpoint does nothing.
Request body:
| Field | Required | Description |
|---|---|---|
ds_records |
yes | An array of DS-record objects. Each object has key_tag (0–65535), algorithm (0–255), digest_type (0–255), and digest in hexadecimal. Each object can also have max_sig_life in seconds. |
Response: 200 OK with the updated domain object. The object has the
new ds_records array and the dnssec_signed boolean.
Error conditions. The endpoint returns 422 with one of these codes:
tld_not_dnssec if the TLD of the tenant has dnssec_supported=false;
invalid_ds_records if a DS record is not correct;
duplicate_ds_record if the new set has the same record two times;
inconsistent_max_sig_life if two records have a different
max_sig_life value that is not nil; wrong_state if the domain is in a
different state; update_prohibited if the domain has the
clientUpdateProhibited or serverUpdateProhibited status.
Remove DS records
DELETE /api/v1/tenants/:tenant_id/domains/:name/ds_records
This endpoint removes all the DS records from a domain. It uses the RFC
5910 <secDNS:rem><secDNS:all>true</…> command. The result is the same
as an empty ds_records array in update_ds_records.
Response: 200 OK with the updated domain object. The object then
has ds_records: [] and dnssec_signed: false.
Error conditions: the same state errors as Replace DS records.
Update domain contacts
POST /api/v1/tenants/:tenant_id/domains/:name/update_contacts
This endpoint replaces the admin, tech, or billing contact handles
of a registered or on_hold domain. You cannot change the registrant
here. Change of Registrant is a different operation.
The endpoint changes only the fields that you send. To remove a role,
send an empty string (""). A field that you do not send stays
unchanged.
Request body:
| Field | Required | Description |
|---|---|---|
admin_handle |
optional | The contact handle for the admin contact, or "" to remove the contact. |
tech_handle |
optional | The contact handle for the tech contact, or "" to remove the contact. |
billing_handle |
optional | The contact handle for the billing contact, or "" to remove the contact. |
Response: 200 OK with the updated domain object.
Error conditions: the endpoint returns 404 if a handle does not
exist in the tenant. It returns 422 if the domain is in a different
state.
Initiate inbound transfer
POST /api/v1/tenants/:tenant_id/domains/:name/transfer_in
This endpoint starts a domain transfer into this registrar.
Request body:
| Field | Required | Description |
|---|---|---|
auth_code |
yes | The authorization code, also known as the transfer code, from the registrar that loses the domain. |
registrant_handle |
conditional | The contact handle for the registrant. This field is necessary for an ICANN-accredited TLD, because the registrar must hold the WHOIS data at the moment that the sponsorship changes. See [RAA §3.7.7.1] and the 15-day RDDS verification period for an inbound transfer. The field is optional for a non-ICANN TLD. |
admin_handle |
optional | The contact handle for the admin contact at the time of the transfer. An unknown handle gives contact_not_found. |
tech_handle |
optional | The contact handle for the tech contact at the time of the transfer. An unknown handle gives contact_not_found. |
billing_handle |
optional | The contact handle for the billing contact at the time of the transfer. An unknown handle gives contact_not_found. |
years |
no | The period of the transfer in years. The default value is 1. If you supply a quote, this value must agree with the period of the quote. |
quote_id |
yes | The UUID of a quote from POST /quotes with operation = "transfer_in". |
registry_account_id |
conditional | Necessary if the tenant has more than one active RegistryAccount for the registry binding of the TLD. If only one account agrees, the registrar selects that account. The service keeps the selected account on the domain row, and each subsequent EPP command for that domain uses it. |
Response: 202 Accepted. The shape of the response depends on the domain row for this name in this tenant:
- External transfer, when there is no domain row for this name, or the row is
deleted. The response has a small object withname,tld,registrant_handle,admin_handle,tech_handle,billing_handle, and apending_transferblock. Thepending_transferblock has{ id, direction: "in", state: "pending", initiated_at, auto_ack_at }.auto_ack_atis the registry deadline for the auto-ack window, taken from theacDateof the transfer response. The service does not compute this date, because the losing registrar controls the window. The field isnullif the registry gives no date. There is no domainidfield and nostatefield, because this service records the domain only when the registry confirms the transfer. When the transfer is in progress,GET /domains/:namereturns404, or thedeletedrow when the tenant held the name before. UseGET /transfers/:pending_idto monitor the transfer. - Intra-tenant transfer, when this tenant already has a domain row for this name that is not
deleted. The response has the full domain object. When the transfer is pending,statestaysregisteredandregistry_account_iddoes not change. Thepending_transferrow holds the new account for this transfer. The service applies the new account to the domain when the transfer is complete.
When the registry confirms the transfer, the pending row changes to completed. For an external transfer, a domain row appears, or the deleted row of the name comes back with the same id. For an intra-tenant transfer, the service updates the domain row. The domain is then in the registered state, and it has the 60-day transfer lock. If the registry sends a NACK, or if the service cancels the transfer automatically, the pending row changes to rejected or cancelled. The domain row, if there is one, does not change.
For an external transfer, the service reads the domain from the registry at completion. The domain gets the nameservers and the epp_statuses of the registry, its DS records, and the registry expiry date when the transfer gives none. Each subordinate host of the domain at the registry becomes an active host of the tenant with the registry IP addresses. A client status that the losing registrar left, for example clientUpdateProhibited, shows in epp_statuses. Remove clientUpdateProhibited, clientDeleteProhibited or clientRenewProhibited with its release endpoint, for example DELETE /domains/:name/update_lock.
Some of the errors: 422 quote_required; 422 registrant_handle_required, for an ICANN-accredited TLD only; 422 auth_code_invalid; 422 domain_within_transfer_lock; 422 registry_account_required, with meta.candidates; 422 registry_binding_mismatch.
Get a single transfer
GET /api/v1/tenants/:tenant_id/transfers/:pending_id
This endpoint returns one inter-registrar transfer of the tenant. A transfer of a different tenant gives a 404 error.
Response: 200 OK with the same shape as an item from the list endpoint.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no transfer with that ID. |
Cancel a pending inbound transfer
POST /api/v1/tenants/:tenant_id/transfers/:pending_id/cancel
This endpoint cancels an inbound transfer that is pending. You can cancel the transfer only before the registrar that loses the domain sends an ACK. See ICANN Transfer Policy §I.A.4. The service sets the pending row to cancelled. The domain row, if there is one, does not change, because the start of the transfer changed nothing. If an intra-tenant domain row is part of the transfer, the service emits a transfer_in_cancelled event.
Response: 204 No Content.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no pending transfer with that ID. |
| 422 | wrong_state |
The pending row is not in the pending state. It is already completed, cancelled, or rejected. |
| 422 | cancel_too_late |
The registry already sent an ACK for the transfer. You cannot cancel it now. |
| 422 | wrong_direction |
The transfer is an outbound transfer. You can cancel an inbound transfer only. |
List inter-registrar transfers
GET /api/v1/tenants/:tenant_id/transfers
This endpoint lists the inter-registrar transfers of the tenant. The list includes the transfers from the past. The most recent transfer is first.
Query parameters. All the parameters are optional:
| Param | Description |
|---|---|
direction |
Select the transfers with the direction in or out. |
state |
Select the transfers in the state pending, completed, rejected, cancelled, or expired. |
Response: 200 OK with { data: [<transfer>] }. Each item has these fields: id, tenant_id, domain_id, domain_name, direction, state, registry_transfer_id, initiated_at, auto_ack_at, completed_at, nack_reason, explicit_decision, inserted_at, and updated_at.
Approve outbound transfer
POST /api/v1/tenants/:tenant_id/domains/:name/approve_transfer_out
This endpoint approves an outbound transfer that is pending.
Response: 200 OK with the updated domain object.
Reject outbound transfer
POST /api/v1/tenants/:tenant_id/domains/:name/reject_transfer_out
This endpoint rejects an outbound transfer that is pending.
Request body:
| Field | Required | Description |
|---|---|---|
reason |
yes | One of the NACK reasons that ICANN Transfer Policy §I.C.1 permits: evidence_of_fraud, udrp_action, court_order, registrant_identity_in_dispute, domain_locked, express_written_objection, or payment_not_received. |
Response: 200 OK with the updated domain object.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 422 | transfer_nack_reason_required |
The request has no reason field. |
| 422 | transfer_nack_reason_invalid |
The reason value is not one of the permitted values. |
Rotate auth code
POST /api/v1/tenants/:tenant_id/domains/:name/auth_code/rotate
This endpoint makes a new EPP auth code, also known as authInfo, for a registered domain. The service sends the new code to the registry. It keeps a hash of the code, and it gives the code in the response body one time only. The previous code becomes not valid.
Request body: none.
Response: 200 OK
{
"data": {
"auth_code": "A3K7M9QXJ2VW5PRT"
}
}
The service keeps the code with reversible encryption. Use the Retrieve endpoint that follows to read the code again. The caller must send the code to the RNH in five calendar days, as ICANN Transfer Policy §I.A.2 specifies.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no domain with that name. |
| 422 | wrong_state |
The domain is not in the registered state. |
| 422 | registry_error |
The registry rejected the update. |
Retrieve auth code
POST /api/v1/tenants/:tenant_id/domains/:name/auth_code/retrieve
This endpoint gives the current auth code of a registered domain. Use it to send the code to the RNH on request. The endpoint does not change the domain, but it is a privileged operation: the caller must have the domains:write scope and the admin role. The service records a domain.auth_code_retrieved event with the identity of the caller.
Request body: none.
Response: 200 OK
{
"data": {
"auth_code": "A3K7M9QXJ2VW5PRT",
"expires_at": "2025-02-01T00:00:00Z"
}
}
The expires_at field is null when the tenant has no auth_code_max_age value.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no domain with that name. |
| 422 | wrong_state |
The domain is not in the registered state. |
| 422 | domain_on_hold_dispute |
The domain is on hold because of a dispute. Use Rotate after the dispute closes. |
| 422 | auth_code_expired |
The code is expired. Use Rotate to make a new code. |
| 422 | auth_code_not_retrievable |
The domain has only a legacy one-way hash. Use Rotate to make a new code. |
Apply registrant transfer lock
POST /api/v1/tenants/:tenant_id/domains/:name/transfer_lock
This endpoint sets clientTransferProhibited on the domain. This status prevents an outbound transfer until you release the lock. This operation is different from Apply Hold: the state stays registered, and the service does not set clientHold.
Request body: none.
Response: 200 OK with the updated domain object. The endpoint is idempotent. If the domain already has the lock, the endpoint does nothing.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no domain with that name. |
| 422 | wrong_state |
The domain is not in the registered state. |
Release registrant transfer lock
DELETE /api/v1/tenants/:tenant_id/domains/:name/transfer_lock
This endpoint removes clientTransferProhibited from the domain. The endpoint is idempotent. If the domain does not have the lock, the endpoint does nothing.
Response: 200 OK with the updated domain object.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no domain with that name. |
| 422 | wrong_state |
The domain is not in the registered state. |
Apply update lock
POST /api/v1/tenants/:tenant_id/domains/:name/update_lock
This endpoint sets clientUpdateProhibited on the domain. When the domain has this lock, update_nameservers, update_contacts, replace_ds_records, and remove_ds_records return update_prohibited.
Request body: none.
Response: 200 OK with the updated domain object. The endpoint is idempotent.
Errors: the same errors as the transfer lock: 404 not_found and 422 wrong_state.
Release update lock
DELETE /api/v1/tenants/:tenant_id/domains/:name/update_lock
This endpoint removes clientUpdateProhibited from the domain. The endpoint is idempotent.
Response: 200 OK with the updated domain object.
Apply delete lock
POST /api/v1/tenants/:tenant_id/domains/:name/delete_lock
This endpoint sets clientDeleteProhibited on the domain. When the domain has this lock, DELETE /domains/:name returns delete_prohibited.
Request body: none.
Response: 200 OK with the updated domain object. The endpoint is idempotent.
Errors: 404 not_found and 422 wrong_state.
Release delete lock
DELETE /api/v1/tenants/:tenant_id/domains/:name/delete_lock
This endpoint removes clientDeleteProhibited from the domain. The endpoint is idempotent.
Response: 200 OK with the updated domain object.
Apply renew lock
POST /api/v1/tenants/:tenant_id/domains/:name/renew_lock
This endpoint sets clientRenewProhibited on the domain. When the domain has this lock, the Renew endpoint returns renew_prohibited.
Request body: none.
Response: 200 OK with the updated domain object. The endpoint is idempotent.
Errors: 404 not_found and 422 wrong_state.
Release renew lock
DELETE /api/v1/tenants/:tenant_id/domains/:name/renew_lock
This endpoint removes clientRenewProhibited from the domain. The endpoint is idempotent.
Response: 200 OK with the updated domain object.
Set the auto-renewal mode
PUT /api/v1/tenants/:tenant_id/domains/:name/auto_renewal
POST /api/v1/tenants/:tenant_id/domains/:name/auto_renewal
DELETE /api/v1/tenants/:tenant_id/domains/:name/auto_renewal
These endpoints set the auto-renewal override of one domain. The override replaces the TLD-wide default. This operation is the opt-out mechanism that the auto-renewal grace period disclosure names [RAA §3.7.5.4]. The operation is local only, and it does not send a command to the registry.
The PUT verb sets the mode. The POST verb is a shortcut for AUTORENEW. The DELETE verb is a shortcut for AUTOEXPIRE.
Request body for PUT. Send one field only:
| Field | Description |
|---|---|
mode |
One of DEFAULT, AUTORENEW, AUTOEXPIRE, RENEWONCE, AUTODELETE. |
enabled |
A boolean. Deprecated. true gives AUTORENEW, and false gives AUTOEXPIRE. Change your client to the mode field. |
Modes:
| Mode | Behavior |
|---|---|
DEFAULT |
The domain uses the TLD-wide default. |
AUTORENEW |
The registry renews the domain for the standard period. |
AUTOEXPIRE |
The domain expires and enters the redemption flow of the registry. |
RENEWONCE |
The registry renews the domain one time. The mode then becomes AUTOEXPIRE. |
AUTODELETE |
The domain expires, and the service deletes it in the auto-renew grace period. |
Response: 200 OK with the updated domain object. The auto_renewal block of the object gives the mode field and the enabled field. The enabled field is the boolean projection of the resolved mode. The operation is idempotent.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no domain with that name. |
| 422 | wrong_state |
The domain is not in the registered or the on_hold state. |
| 422 | domain_on_hold_dispute |
A URS dispute of the domain is filed or complainant_won. |
| 422 | auto_renewal_mode_required |
The PUT body has no mode field and no enabled field. |
| 422 | auto_renewal_conflicting_inputs |
The PUT body has the mode field and the enabled field together. |
| 422 | invalid_auto_renewal_mode |
The mode value is not in the list of modes. |
Delete domain
DELETE /api/v1/tenants/:tenant_id/domains/:name
This endpoint deletes a domain that is registered, or on_hold for contact inaccuracy or abuse. If the tenant has an active subordinate host below this domain, the endpoint returns 422 subordinate_hosts_exist. Delete or rename those hosts first.
Errors, all 422: on_hold_dispute for a domain on hold for a dispute, delete_prohibited for a domain with a delete-prohibited status, wrong_state for a domain in another state, and subordinate_hosts_exist.
Response: 204 No Content
Restore domain
POST /api/v1/tenants/:tenant_id/domains/:name/restore
This endpoint restores a domain that you deleted. You can restore the domain during the redemption grace period only.
Request body:
| Field | Required | Description |
|---|---|---|
quote_id |
yes | The UUID of a quote from POST /quotes with operation = "restore". Such a quote has no period. |
The endpoint sends the restore request and then the restore report (RFC 3915) to the registry. When the registry accepts the report, the endpoint reads the domain from the registry and sets expires_at from it. Then the domain is registered, or on_hold when it keeps a hold reason, and the endpoint consumes the quote. If the read fails, the restore still succeeds, and the domain keeps its current expires_at until the next reconcile. A completed restore writes one domain.restored notification.
Response: 200 OK with the updated domain object.
Payment-assurance errors, all 422: quote_required, quote_not_found, quote_consumed, quote_expired, quote_mismatch.
Registry errors: A refused restore request or a refused restore report gives the registry error, per the “Errors” section. The domain stays redemption and the quote is not consumed. After a refused report, the domain events show restore_report_refused. Send the request again with a quote that is not consumed and not expired. For seven days after the accepted restore request, the endpoint sends no new request. If the registry already restored the domain, the endpoint completes the restore without a report. Otherwise it sends the report again.
Apply hold
POST /api/v1/tenants/:tenant_id/domains/:name/apply_hold
This endpoint puts a hold on a domain.
Request body:
| Field | Required | Description |
|---|---|---|
reason |
no | The reason for the hold. The default value is "contact_inaccuracy". The endpoint accepts "contact_inaccuracy", "dispute" and "abuse". Any other value gives 422 invalid_hold_reason. |
Each reason uses the REGISTRAR HOLD and REGISTRAR LOCK statuses, clientHold and clientTransferProhibited. Use abuse for a fraud or abuse suspension. The service sends only the statuses that the domain does not already have. It records clientHold, and clientTransferProhibited only when it sent it.
release_hold removes each status that the hold recorded and that the domain still has. Thus the domain has no clientHold after the release, and a registrant transfer lock that was set before the hold stays.
The dispute reason also accepts a domain that is on_hold for a different reason. The dispute hold goes on top of the other hold, and the other hold stays. The domain then shows hold_reason dispute and the other reason in underlying_hold_reason. Each other reason needs a registered domain.
Each hold writes one domain.hold_applied notification, and each release writes one domain.hold_released notification.
Response: 200 OK with the updated domain object.
Release hold
POST /api/v1/tenants/:tenant_id/domains/:name/release_hold
This endpoint releases one hold on a domain.
Request body:
| Field | Required | Description |
|---|---|---|
reason |
no | The reason of the hold to release. The endpoint accepts "contact_inaccuracy", "dispute" and "abuse". Any other value gives 422 invalid_hold_reason. Without reason, the endpoint releases the only hold of the domain. |
A domain with a dispute hold on top of a different hold has two holds. The release of one hold keeps the other hold:
- The domain stays
on_hold, and the other hold moves tohold_reason. - The release removes only the statuses that the other hold did not record. Thus
clientHoldandclientTransferProhibitedstay. - When no hold stays, the domain goes to
registered.
While a UDRP or URS dispute holds the domain, the endpoint refuses each release that leaves the domain with no dispute hold. A dispute holds the domain while it is filed, after the panel decides for the complainant, and while its post-dispute deletion clock runs. Thus the endpoint refuses a release of the dispute hold. A release of the hold under the dispute hold passes, and the dispute hold and its lock stay. While the dispute is filed, only the decision of the dispute releases the dispute hold.
Response: 200 OK with the updated domain object.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no domain with that name. |
| 422 | wrong_state |
The domain is not in the on_hold state. |
| 422 | invalid_hold_reason |
reason is not a known hold reason. |
| 422 | hold_reason_required |
The domain has two holds. Send the reason of the hold to release. |
| 422 | hold_reason_mismatch |
The domain has no hold with this reason. |
| 422 | dispute_active |
A UDRP or URS dispute holds the domain, and the release leaves the domain with no dispute hold. The dispute is filed, the complainant won, or the post-dispute deletion clock runs. The domain stays on hold. |
List domain events
GET /api/v1/tenants/:tenant_id/domains/:name/events
This endpoint returns the history of the events of a domain.
Response: 200 OK
{
"data": [
{
"actor_id": "uuid",
"actor_type": "user",
"domain_id": "uuid",
"event_type": "registered",
"id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"payload": {}
}
]
}
Audit Events
The audit log records each action. It is the evidence of what occurred. A notification is different: it is an item that needs your attention.
The necessary scope is audit:read. The service always uses the tenant of the token. It ignores a tenant_id parameter, an actor_user_id parameter, and an actor_oauth_client_id parameter in the request.
List audit events
GET /api/v1/tenants/:tenant_id/audit_events
This endpoint returns the audit events of the tenant, with the most recent event first.
Filters, in the query string. All the filters are optional:
| Field | Description |
|---|---|
action |
The name of the action, for example domain.registered. |
target_type / target_id |
The object of the action. |
outcome |
The result of the action. |
correlation_id |
Select the events of one operation. |
registry_account_id |
Select the events of one registry account. An account of a different tenant gives an empty list. |
from / to |
The limits of the time window, in ISO-8601 format. |
limit |
The maximum quantity of rows. The default is 50, and the maximum is 200. |
cursor |
The next_cursor value from the previous page. |
Response: 200 OK
{
"data": [
{
"action": "domain.registered",
"actor": {
"kind": "user",
"label": "user@example.com"
},
"correlation_id": "uuid",
"id": "uuid",
"outcome": "success",
"payload": {},
"recorded_at": "2025-01-01T00:00:00Z",
"registry_account_id": "uuid",
"source": "api",
"target_id": "uuid",
"target_type": "domain"
}
],
"pagination": {
"has_more": true,
"next_cursor": "opaque"
}
}
The actor.kind field is user, oauth_client, operator, system, or anonymous. For an operator, the label is always "operator", thus no internal identifier of the staff becomes known. The response does not contain the request_id field, the source_ip field, or a raw actor identifier. The service also removes each *_token key and each *_secret key from the payload.
Get an audit event
GET /api/v1/tenants/:tenant_id/audit_events/:id
This endpoint returns one audit event with the same projection.
Response: 200 OK with the event object, or 404 not_found.
Notifications
The notifications inbox gives the messages of the system that need the attention of the tenant. Each notification has a stable kind, a severity, and a state. The states are unread, read, and dismissed. The transitions are one way.
List notifications
GET /api/v1/tenants/:tenant_id/notifications
This endpoint returns the notifications of the tenant, with the most recent occurred_at value first. The necessary scope is notifications:read. The list does not contain the dismissed notifications, but it contains them when you send state=all.
Filters, in the query string. All the filters are optional:
| Field | Description |
|---|---|
state |
unread, read, or all. The default gives the unread notifications and the read notifications. |
kind |
One kind, and the value must agree exactly. |
severity |
info, warning, or critical. |
related_type |
The type of the related object: domain, contact, host, transfer, registry_account, tld_association, or change_of_registrant. |
related_id |
The ID of the related object. Use it with related_type. |
occurred_after / occurred_before |
The limits of the time window, in ISO-8601 UTC format. |
limit |
The maximum quantity of rows. |
cursor |
The next_cursor value from the previous page. |
Response: 200 OK
{
"data": [
{
"acked_at": null,
"body": "..",
"dismissed_at": null,
"id": "uuid",
"inserted_at": "2025-01-01T00:00:00Z",
"kind": "domain.expiring_soon",
"occurred_at": "2025-01-01T00:00:00Z",
"related_object_id": "uuid",
"related_object_type": "domain",
"severity": "warning",
"state": "unread",
"subject": "example.com expires in 30 days",
"tenant_id": "uuid",
"updated_at": "2025-01-01T00:00:00Z"
}
],
"pagination": {
"has_more": true,
"next_cursor": "opaque"
}
}
The related object can be deleted. Thus your client must accept a reference that finds nothing.
Get a notification
GET /api/v1/tenants/:tenant_id/notifications/:id
This endpoint returns one notification. The necessary scope is notifications:read. A read does not change the state. Use the acknowledge endpoint to make the notification read.
Response: 200 OK with the notification object, or 404 not_found. A dismissed notification also gives 404 not_found.
Acknowledge a notification
POST /api/v1/tenants/:tenant_id/notifications/:id/ack
This endpoint changes the state from unread to read. The necessary scope is notifications:write. The endpoint is idempotent: if the notification is already read, the endpoint keeps the initial acked_at value.
Request body: none.
Response: 200 OK with the updated notification object.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no notification with that ID. |
| 422 | notification_dismissed |
The notification is dismissed. You cannot acknowledge it. |
Dismiss a notification
DELETE /api/v1/tenants/:tenant_id/notifications/:id
This endpoint changes the state to dismissed. The necessary scope is notifications:write. The service keeps the row for the audit, and you can read it with state=all. The endpoint is idempotent.
A subsequent event of the same kind makes a new notification. A dismissal does not stop the future notifications.
Response: 200 OK with the updated notification object, or 404 not_found.
Change of Registrant
A material change to the registrant of a domain needs a confirmation from the two parties. A material change is a change to the Name, the Organization, or the Email, as [Transfer Policy §II.A.1] specifies. A change to the address, the phone number, or the fax number is not material. The service applies a change that is not material at once, with no confirmation and no transfer lock.
Initiate
POST /api/v1/tenants/:tenant_id/domains/:name/change_of_registrant
Body: { "new_registrant_handle": "<handle>", "tenant_confirms": false }.
| Field | Required | Description |
|---|---|---|
new_registrant_handle |
Yes | The handle of the new registrant contact. |
tenant_confirms |
No | A JSON boolean. The default is false. Set it to true when your tenant is the Designated Agent for the two parties. The service then sends no consent email. You must confirm each party with the returned tokens through the confirm endpoint. The Apply step still sends the §II.C.1.6 notification to the prior and the new registrant. |
Response:
- For a material change,
201 Createdwith{ "data": <request>, "tokens": { "prior": "<plaintext>", "new": "<plaintext>" } }. The endpoint gives the plaintext tokens one time only. Send them to the confirm endpoint or the decline endpoint, or put them in the consent email messages. - For a change that is not material,
200 OKwith{ "data": { "material": false } }. The service makes no request row. When the new registrant is a different contact, the service sets it as the registrant of the domain. Get the domain to see the newregistrant_handle.
Errors: change_already_pending, new_registrant_unknown, new_registrant_suspended, domain_in_transfer, domain_on_hold_dispute, domain_state_invalid, prior_registrant_unknown, invalid_tenant_confirms. The service returns invalid_tenant_confirms when tenant_confirms is not a JSON boolean, for example the string "true".
List for a domain
GET /api/v1/tenants/:tenant_id/domains/:name/change_of_registrant
Get one
GET /api/v1/tenants/:tenant_id/change_of_registrant/:id
Confirm
POST /api/v1/tenants/:tenant_id/change_of_registrant/:id/confirm
Body: { "token": "<plaintext>", "party": "prior" | "new" }.
After the two parties confirm, the system does the Apply step. This step changes the registrant. It then sets transfer_lock_until = now + 60 days, but not if the new registrant or the tenant has transfer_lock_opt_out = true. The step also makes a new Acceptance Record with acceptance_method = change_of_registrant, and changes the prior acceptance record of the domain to superseded. Last, it sends the notification that §II.C.1.6 specifies to the prior and the new registrant. The tenant gets one change_of_registrant.completed notification in the notifications inbox. Its related_object_type is domain, and its related_object_id is the domain name. § Tenant Notification.
Decline
POST /api/v1/tenants/:tenant_id/change_of_registrant/:id/decline
The body has the same shape as the body for Confirm. If one party declines, the service stops the request and does not change the domain.
Errors for the confirm and decline endpoints: consent_token_invalid and not_found.
Cancel
POST /api/v1/tenants/:tenant_id/change_of_registrant/:id/cancel
This endpoint stops a request that is not terminal. Use it to retract a request that you started, and thus you do not wait for the token of a party. The request must be in the initiated state, the pending_prior_consent state, the pending_new_consent state, or the confirmed state. A request in the confirmed state can still be cancelled, because the Apply step is not complete.
The request becomes cancelled, and the two consent tokens become not valid. The domain does not change, and you can start a new request for the same domain.
Request body: none.
Response: 200 OK with the request object.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no request with that ID. |
| 422 | change_already_terminal |
The request is already applied, declined, expired, or cancelled. |
Disputes, read-only
A tenant can read the UDRP and URS disputes against its own domains. Only an operator can file a dispute or record a decision. See operator-api.md. A dispute provider notifies the registrar, not the tenant.
List disputes
GET /api/v1/tenants/:tenant_id/disputes
Response: 200 OK with { data: [<dispute>] }. The most recent dispute is first.
Get a single dispute
GET /api/v1/tenants/:tenant_id/disputes/:id
Response: 200 OK with the dispute object, or 404 not_found.
When a dispute is active, which is the state = "filed" condition, each domain response has a short dispute object with { id, type, provider, case_number, state, filed_at }.
Contacts
Check contact handle availability
POST /api/v1/tenants/:tenant_id/contacts/check
This endpoint tells you if a contact handle is available.
Request body:
| Field | Required | Description |
|---|---|---|
handle |
yes | The handle to examine. It must agree with [A-Za-z0-9][A-Za-z0-9_.-]{0,62}. |
Response: 200 OK
{
"data": {
"handle": "jdoe",
"status": "available"
}
}
List contacts
GET /api/v1/tenants/:tenant_id/contacts
This endpoint returns all the contacts of the tenant.
Response: 200 OK with an array of contact objects.
Get contact
GET /api/v1/tenants/:tenant_id/contacts/:handle
This endpoint returns one contact. Use the handle of the contact.
Response: 200 OK with the contact object, or 404 Not Found.
The suspension_reason field tells why a suspended contact is suspended. verification_expired means that the registrant did not verify the contact in the 15-day period, and a verification ends the suspension. investigation_nonresponsive means that the registrant did not cooperate with an investigation of inaccurate contact data, and only the operator can end the suspension. The field is null for a contact that is not suspended.
The dispute_locked field is true when a domain of which the contact is the registrant is on hold because of a dispute. While it is true, each update of the contact fails with 422 domain_on_hold_dispute. A contact that a held domain uses only as the admin, tech, or billing contact is not locked. Each response that contains the contact object has the field.
Create contact
POST /api/v1/tenants/:tenant_id/contacts
This endpoint makes a new contact.
The handle field is optional. If you do not send it, or if you send an
empty string, the service makes a random handle of 16 lowercase
hexadecimal characters. The response contains that handle. Send a
handle only if you must have your own identifier. For most tasks, a
random handle is better, because it puts no text with a meaning into the
EPP objects at the registry.
Phone validation. The phone field must be in the international
E.164 format (+CCXXXXXXXXX). It must also be valid for the country
of the dialling code, as the libphonenumber data specifies. A number can
have the correct E.164 syntax but be too short for its country. An
example is +15554545 for the +1 NANP plan. The endpoint rejects such a
number with unprocessable_entity and a phone error, for example
"is not a valid number for its country code".
Response: 201 Created with the contact object.
Update contact
PATCH /api/v1/tenants/:tenant_id/contacts/:handle
This endpoint updates the attributes of a contact. You cannot change the handle field or the tenant_id field with this endpoint.
A change of the name, the organization, or the email address of a registrant contact is a change of registrant. The service applies it at once to the registrant domains of the contact. On an icann_accredited TLD, the service makes a new acceptance record for each of these domains and changes the prior record to superseded. § Apply In-Place Edit (Designated Agent). The tenant gets one change_of_registrant.completed notification in the notifications inbox for each of these domains.
Response: 200 OK with the updated contact object.
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
This tenant has no contact with that handle. |
| 422 | unprocessable_entity |
A field is not valid. The details object names the field. |
| 422 | domain_on_hold_dispute |
A domain of which the contact is the registrant is on hold because of a dispute. Every update of the contact fails until the dispute hold ends. |
| 422 | domain_in_transfer |
The update changes the name, the organization, or the email address, and a domain of which the contact is the registrant has a transfer in flight. |
| 422 | no_published_agreement_version |
The update changes the name, the organization, or the email address, a domain of which the contact is the registrant is on an ICANN-accredited TLD, and the tenant has no published agreement version for it. |
In each error case, the contact does not change.
Delete contact
DELETE /api/v1/tenants/:tenant_id/contacts/:handle
This endpoint deletes a contact.
Response: 204 No Content
Transfer contact
POST /api/v1/tenants/:tenant_id/contacts/:handle/transfer
This endpoint transfers a contact. At this time, the endpoint returns an error, because the service does not do contact transfers.
Resend verification challenge
POST /api/v1/tenants/:tenant_id/contacts/:handle/verification_challenge/resend
This endpoint sends the active verification challenge to the registrant again, on the current channel of the contact. Use it when the registrant did not get the email or the SMS, or lost it. The token and the expiry do not change, thus a link from an earlier message stays valid. The 15-day clock does not reset. The necessary scope is contacts:write.
For a contact that is suspended because its verification period ended, and that has no active challenge, the endpoint issues a new challenge: a new token, with an expiry 15 days from now. The contact stays suspended until the registrant verifies it. The verification then releases the contact_inaccuracy holds. A second resend while that challenge is active sends the same token again. A contact that an investigation suspended gets no new challenge, and the endpoint returns 409 no_active_challenge. Only the operator can end that suspension.
The endpoint has a rate limit of one request for each contact in each minute.
Response: 200 OK
{
"data": {
"channel": "email",
"scheduled": true
}
}
Errors:
| Status | Code | Meaning |
|---|---|---|
| 404 | not_found |
The contact is not in the tenant, or it is deleted. |
| 409 | no_active_challenge |
The contact is not suspended for an ended verification period and has no active challenge. It is verified, its 15-day clock has ended, no challenge is on file, or an investigation suspended it. |
| 429 | rate_limited |
This contact already got a resend request in the current minute. The counter is for each contact, by the tenant and the handle. A request counts when it passes the authentication, the scope check, and the tenant check, also when it then gets 404 not_found or 409 no_active_challenge. Retry after the time in Retry-After. |
Agreement Versions
Create agreement version
POST /api/v1/tenants/:tenant_id/agreement_versions
This endpoint makes a new agreement version in the draft state.
Response: 201 Created with the agreement version object.
List agreement versions
GET /api/v1/tenants/:tenant_id/agreement_versions
This endpoint returns the agreement versions of the tenant. Use the query parameters to filter the list.
Response: 200 OK with an array of agreement version objects.
Get agreement version
GET /api/v1/tenants/:tenant_id/agreement_versions/:id
This endpoint returns one agreement version.
Response: 200 OK with the agreement version object, or 404 Not Found.
Publish agreement version
POST /api/v1/tenants/:tenant_id/agreement_versions/:id/publish
This endpoint publishes an agreement version that is in the draft state. That version then becomes the active version.
Response: 200 OK with the updated agreement version object.
Retire agreement version
POST /api/v1/tenants/:tenant_id/agreement_versions/:id/retire
This endpoint retires an agreement version that you published.
Response: 200 OK with the updated agreement version object.
Update agreement version — not permitted
PUT /api/v1/tenants/:tenant_id/agreement_versions/:id
PATCH /api/v1/tenants/:tenant_id/agreement_versions/:id
These endpoints return 405 Method Not Allowed. You cannot change an agreement version after you make it. Make a new version.
Acceptance Records
Get current acceptance
GET /api/v1/tenants/:tenant_id/domains/:name/acceptance
This endpoint returns the current acceptance record of a domain.
Response: 200 OK with the acceptance object. If the domain has no acceptance record, the endpoint returns 404 Not Found.
Create acceptance
POST /api/v1/tenants/:tenant_id/domains/:name/acceptance
This endpoint records that the registrant accepted the current agreement for a domain. The service also records the source IP address and the user agent of the request.
Response: 201 Created with the acceptance object.
Terminate acceptance
POST /api/v1/tenants/:tenant_id/domains/:name/acceptance/terminate
This endpoint terminates the current acceptance record of a domain.
Response: 200 OK with the updated acceptance object.
Acceptance history
GET /api/v1/tenants/:tenant_id/domains/:name/acceptance/history
This endpoint returns all the acceptance records of a domain. The list includes the records that you terminated, and the superseded records that a later acceptance replaced. A re-acceptance and a change of registrant supersede the prior record.
Response: 200 OK with an array of acceptance objects.
Hosts
Check host name availability
POST /api/v1/tenants/:tenant_id/hosts/check
This endpoint tells you if a host name is available. The endpoint examines the local records only. It does not send a command to the registry.
Request body:
| Field | Required | Description |
|---|---|---|
name |
yes | The fully qualified host name to examine. |
Response: 200 OK with the result.
{
"data": {
"name": "ns1.example.com",
"status": "available"
}
}
Check a host name at the registry
POST /api/v1/tenants/:tenant_id/hosts/registry_check
This endpoint sends an EPP host:check command to the registry of the TLD, and it gives the answer of the registry. Use it when you must know the condition at the registry, and not the local condition. The necessary scope is hosts:read.
Request body:
| Field | Required | Description |
|---|---|---|
name |
yes | The fully qualified host name to examine. |
Response: 200 OK
{
"data": {
"available": true,
"name": "ns1.example.com"
}
}
Errors:
| Status | Code | Meaning |
|---|---|---|
| 422 | invalid_host_name |
The name is not a correct host name. |
| 422 | tld_not_associated |
The tenant has no TLD association for the parent domain of the host. |
| 502 / 503 / 504 | registry_protocol_error, registry_unavailable, registry_timeout |
The registry did not answer correctly. Try again. |
List hosts
GET /api/v1/tenants/:tenant_id/hosts
This endpoint returns all the hosts of the tenant.
Response: 200 OK with an array of host objects.
Get host
GET /api/v1/tenants/:tenant_id/hosts/:name
This endpoint returns one host. Use the name of the host.
Response: 200 OK with the host object, or 404 Not Found.
Create host
POST /api/v1/tenants/:tenant_id/hosts
This endpoint makes a new host, which is also known as a glue record.
Response: 201 Created with the host object.
The registry can refuse to create the host object. The usual cause is that a different registrar sponsors the parent domain, or that the parent domain does not exist at the registry. The endpoint then returns 422 host_parent_unavailable.
Update host IPs
PATCH /api/v1/tenants/:tenant_id/hosts/:name
This endpoint updates the IP addresses of a host.
Response: 200 OK with the updated host object.
Rename host
POST /api/v1/tenants/:tenant_id/hosts/:name/rename
This endpoint changes the name of a host.
Response: 200 OK with the updated host object.
Delete host
DELETE /api/v1/tenants/:tenant_id/hosts/:name
This endpoint deletes a host.
Response: 204 No Content
Reconcile host
POST /api/v1/tenants/:tenant_id/hosts/:name/reconcile
This endpoint gets the current data for a subordinate host from the
registry with the RFC 5732 <host:info> command. It then writes the
statuses and the IP addresses to the local record. Use this endpoint to
remove a pendingCreate, pendingUpdate, or pendingDelete status
after the registry acknowledges an operation.
Response: 200 OK with the updated host object.
Errors: the endpoint returns 404 not_found if the host is not in
the tenant. It returns 422 not_subordinate for an external host,
because such a host has no object at the registry. It also returns 422
Pricing
The operator writes the rows of the rate card.
See operator-api.md. A tenant uses the endpoints that follow to read
its rate card and to request a quote.
List rate cards
GET /api/v1/tenants/:tenant_id/rate_card
This endpoint returns each row with a price, for all the TLDs of the tenant. The rows are in groups by TLD. The list does not include a TLD that has no row with a price.
Response: 200 OK
{
"data": [
{
"entries": [
{
"amount": "9.9900",
"currency": "USD",
"id": "…",
"inserted_at": "…",
"operation": "register",
"period": 1,
"premium_class": null,
"tier": "standard",
"tld": "com",
"updated_at": "…"
}
],
"tld": "com"
}
]
}
Get rate card
GET /api/v1/tenants/:tenant_id/tlds/:tld/rate_card
This endpoint returns each row with a price for this tenant and this TLD.
Path parameters:
| Field | Description |
|---|---|
tld |
The TLD, with no dot at the start. The tenant must have an association with this TLD. |
Response: 200 OK
{
"data": {
"entries": [
{
"amount": "9.9900",
"currency": "USD",
"id": "…",
"inserted_at": "…",
"operation": "register",
"period": 1,
"premium_class": null,
"tier": "standard",
"tld": "com",
"updated_at": "…"
}
],
"tld": "com"
}
}
Errors: the endpoint returns 422 tld_not_associated if the tenant
has no active association with this TLD.
Create quote
POST /api/v1/tenants/:tenant_id/quotes
This endpoint gives an exact price for one (fqdn, operation, period)
combination, at the tier that applies to the name. The caller supplies
the tier. For a premium name, the caller also supplies the premium class.
In a later release, the service will get these values automatically from
the availability check and from the domain record.
Request body:
| Field | Required | Description |
|---|---|---|
fqdn |
yes | The fully qualified domain name. The service gets the TLD from this name, and the tenant must have an association with that TLD. |
operation |
yes | One of register, renew, transfer_in, or restore. |
period |
conditional | The period in years, an integer from 1 to 10. This field is necessary for register, renew, and transfer_in. Do not send it for restore. |
tier |
no | standard, which is the default, or premium. |
premium_class |
conditional | Necessary if tier = premium. This is a label from the registry, for example gold. The label has no known structure. |
Response: 200 OK
{
"data": {
"amount": "9.9900",
"currency": "USD",
"fqdn": "example.com",
"operation": "register",
"period": 1,
"premium_class": null,
"quote_id": "f1a2b3c4-…",
"quoted_at": "2026-04-18T12:00:00.000000Z",
"tier": "standard",
"tld": "com",
"valid_until": "2026-04-18T13:00:00.000000Z"
}
}
The quote_id is the payment-assurance token that [RAA §3.7.4] specifies. You can use it one time only. The register, renew, transfer_in, and restore endpoints accept it. The sections above give the request shape and the error codes for each of those endpoints.
Errors: the endpoint returns 422 with one of these error codes:
price_not_configured— no row of the rate card agrees with the request.premium_class_unknown—tier = premium, but there is no row for thepremium_classvalue.period_not_allowed— theperiodfield is not present, or its value is not from 1 to 10 for this operation.tld_not_associated— the tenant has no association with the TLD of the name.invalid_operation— theoperationfield is not present, or its value is not permitted.premium_class_required—tier = premium, but the request has nopremium_classfield.fqdn_required— thefqdnfield is not present, or it is empty.
Abuse Reports, public
The abuse report endpoint does not need authentication. Thus any person can report possible abuse of a domain that this service manages, as [RAA §3.18.1] specifies.
File abuse report
POST /api/v1/abuse_reports
Request body:
| Field | Type | Required | Description |
|---|---|---|---|
domain_name |
string | yes | The domain in the report. The server changes the letters to lowercase and removes a dot at the end. |
description |
string | yes | A description of the possible abuse, in free text. |
reporter_name |
string | no | The name of the person who makes the report. |
reporter_contact |
object | no | The contact data of that person, for example { "email": "..", "phone": ".." }. |
reporter_reference |
string | no | A case number from that person. |
law_enforcement |
boolean | no | The person says that the report comes from a law-enforcement agency. This value starts the 24-hour period for the acknowledgement. The operator confirms or changes the value at the acknowledgement. |
source |
string | no | The channel of the report. The default value is "public_form". |
Response: 201 Created
{
"data": {
"domain_name": "example.com",
"id": "uuid",
"received_at": "2026-04-20T12:00:00.000000Z",
"state": "received"
}
}
If the domain in the report is a registered domain on this service, the case goes to the tenant of that domain. That tenant can then see the case with the endpoints that follow. If no domain agrees, the case goes to a queue that only an operator can see.
Errors: the endpoint returns 422 with one of these codes:
missing_domain_name— the field is not present, or it is empty.missing_description— the field is not present, or it is empty.
Abuse Cases, tenant read-only
GET /api/v1/tenants/:tenant_id/abuse_cases
GET /api/v1/tenants/:tenant_id/abuse_cases/:id
These endpoints return the cases of the tenant of the caller. Use the optional query parameters state and category to filter the list. In Phase A, only an operator can acknowledge, classify, resolve, or dismiss a case.
Directory API
These endpoints are read-only, and they find an object in any tenant.
The separate RDAP service uses them.
The endpoints are not in the scope of one tenant.
They need an OAuth 2.0 Client Credentials token, and that token must
have the directory:read scope. A token in the scope of one tenant,
with the read or write scope, gives 403 insufficient_scope.
Routes
GET /directory/v1/domains/by_name/:name
GET /directory/v1/contacts/:handle
GET /directory/v1/hosts/by_name/:name
GET /directory/v1/registrars/by_iana_id/:iana_id
Each response has the usual {"data":..} envelope. Each endpoint
returns 404 not_found if the object does not exist.
Fields in a domain response: handle, ldh_name, unicode_name,
tld, state, epp_statuses, registered_at, expires_at,
last_changed_at, nameservers, which is an array of host FQDN
strings, ds_records, registrant_handle, admin_handle,
tech_handle, billing_handle, tenant_id, iana_registrar_id, which
comes from the tenant of the domain, and hold_reason.
Fields in a contact response: the usual contact fields and a
disclosure object. The disclosure object has one flag for each
field, and each flag tells you if the service can publish that field.
The default values agree with the ICANN 2024 Registration Data Policy:
organization, state_province, and country are true, and the
other fields with personal data are false. A later release will give
the true flag for each field.
Fields in a host response: handle, ldh_name, ip_addresses,
which has a v4 array and a v6 array, statuses, created_at, and
last_changed_at.
Fields in a registrar response: iana_registrar_id, name,
abuse_email, abuse_form_url, and rdap_base_url. The
rdap_base_url field is always null at this time. It will have a
value after the tenant record gets this column.