Public API Documentation

This page gives an example request and an example response for each tenant-scoped public endpoint. Back to home.

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>",..] }
}

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:

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_\-:.+]>
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:

  1. It gets the token from the Authorization: Bearer <token> header.
  2. It verifies the JWT signature and the expiration time with the configured secret.
  3. It finds the jti (JWT ID) of the token in the oauth_tokens table. This shows if the token is revoked.
  4. It puts the claims of the token into the request context. The claims include sub, which is the current user ID, and tenant_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:

  1. The tenant_id in the URL must agree with the tenant_id claim in the bearer token.
  2. The tenant must exist. The tenant must not be suspended.
  3. 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:

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:

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:

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


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:


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:

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:


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:


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.

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:

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:

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:

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:

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:

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:

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:

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.