Taking a Domain

3 views Markdown

The nine endpoints that register, transfer and control the movement of a domain.

Overview

A domain is taken in one of two ways: registering a new one or moving one from another provider. These nine endpoints cover both roads and the transfer controls around them.

Two questions come before an order: what the extension costs and for which terms it sells, and whether the name is free right now. The answer to the second is a snapshot.

The transfer controls work both ways. Handing a domain to someone else means opening the lock and asking for the code; bringing one here means having it opened at the other end and writing the code down here.

Reference

Reading the Extension Prices

get/api/v1/client/domains/tlds
Domains/GetTlds the wallet currency

Returns the extensions on sale with their terms and prices.

Query 1
tldstringOne extension. It narrows the list and adds the price table per term.
Response fields data[] — 7
tldstringThe extension.
min_yearsintThe shortest term.
max_yearsintThe longest term.
registerobjectThe one-year registration price. The price before a discount comes too where a promotion runs.
transferobjectThe one-year transfer price. It comes empty where the extension has no transfer price.
renewalobjectThe one-year renewal price.
register_yearsobjectThe registration price per term. It comes only when one extension is asked for, and a term with no entry is not sold.
Errors 3
not_found404The extension asked for is not sold.
domains_not_offered422The operator does not sell domains.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/domains/tlds?tld=com' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch('https://panel.example.com/api/v1/client/domains/tlds?tld=com', {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
const years = Object.keys(data[0].register_years ?? {});
$ch = curl_init('https://panel.example.com/api/v1/client/domains/tlds?tld=com');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The price per term comes ONLY when one extension is asked for: the general listing holds no such table.
$one = Kernel::internal('client:Domains/GetTlds', ['owner_id' => $uid, 'tld' => 'com'])['data'][0];
$priced = array_keys($one['register_years'] ?? []);

Asking Whether a Name Is Free

post/api/v1/client/domains/check
Domains/CheckDomain it asks the registrar

Asks live whether a domain can be taken.

Body 1
domainstringreqThe full domain to ask about.
Response fields data — 5
domainstringThe name as it was read.
statusstringThe outcome: free, taken or unknown.
premiumboolWhether the registry prices this name apart.
premium_priceobjectThe customer price of a specially priced name. It comes with the margin applied and in the wallet currency.
registerobjectThe extension's one-year price. It comes on a free name without a special price alone.
Errors 6
domains_not_offered422The operator does not sell domains.
domain_required422No domain was sent.
domain_invalid422It is not a valid domain name.
tld_not_offered422The extension is not sold.
domain_check_throttled422The shared query pool is full. Try again shortly.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/client/domains/check' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"domain":"example-shop.com"}'
const res = await fetch('https://panel.example.com/api/v1/client/domains/check', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ domain }),
});

const { data } = await res.json();
if (data.premium) showPremiumPrice(data.premium_price);
$ch = curl_init('https://panel.example.com/api/v1/client/domains/check');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['domain' => $name]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Do not read UNKNOWN as free: the registrar could not be reached and the order can still fail.
$r = Kernel::internal('client:Domains/CheckDomain',
    ['owner_id' => $uid, 'domain' => $name])['data'];

$safe = $r['status'] === 'available';

Registering a Domain

post/api/v1/client/domains/register
Domains/RegisterDomain the balance or a saved card

Orders a new domain and collects payment.

Body 12
termsboolreqAccepting the terms. It has to be sent true.
domainstringreqThe full domain name.
yearsintThe term in years. It has to sit inside the extension's window, and a specially priced name is registered for one year whatever is sent.
accept_premiumboolAccepting the special price. It is required on such a name.
addonsstring[]The add-ons to take with it. Name management, registration privacy and forwarding, where the extension offers them.
nameserversstring[]The name servers. Two to four, and the account defaults stand in when left out.
whois_profile_idintA saved registrant profile. It fills all four roles.
contactsobjectThe contacts per role. Each role carries either a saved profile id or an inline contact, and it overrides the profile for that role.
couponsstring[]The coupon codes.
billing_profile_idintThe billing profile id.
notesstringThe order note.
paymentobjectThe payment source. The account balance or a saved card.
Response fields 201 — data — 7
order_idintThe order id.
numberstringThe order number.
actionstringThe job done: a registration or a transfer.
domainobjectThe domain service made. It carries the service id, the name, a state and the term.
totalobjectThe order total.
paymentobjectHow the collection went. It carries the road, the state, the card charged and any reason it failed.
invoiceobjectThe order invoice summary.
Errors 10
not_found404The registrant profile, the billing profile or the card belongs to another account.
domains_not_offered422The operator does not sell domains.
terms_required422The terms were not accepted.
verification_required422The account is waiting on verification.
tld_not_offered422The extension is not sold.
years_invalid422The term sits outside the extension's window.
addon_not_offered422The add-on is not offered on this extension or the key is unknown.
nameservers_invalid422The name server count or form is invalid.
contact_field_required422A required field is missing on an inline contact. Every field but the company is required.
insufficient_balance422The wallet does not cover the total.
Request
curl -X POST 'https://panel.example.com/api/v1/client/domains/register' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"terms":true,"domain":"example-shop.com","years":1,"payment":{"method":"balance"}}'
const res = await fetch('https://panel.example.com/api/v1/client/domains/register', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    terms: true,
    domain,
    years: 1,
    whois_profile_id: profileId,
    payment: { method: 'balance' },
  }),
});

const { data } = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/domains/register');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($order),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A SPECIALLY PRICED name registers for ONE YEAR whatever is sent, and it wants the acceptance flag.
$c = Kernel::internal('client:Domains/CheckDomain',
    ['owner_id' => $uid, 'domain' => $name])['data'];

Kernel::internal('client:Domains/RegisterDomain', ['owner_id' => $uid, 'terms' => true,
    'domain' => $name, 'accept_premium' => $c['premium'],
    'payment' => ['method' => 'balance']]);

Transferring a Domain

post/api/v1/client/domains/transfer
Domains/TransferDomain a code may be needed

Moves a domain held at another provider over here.

Body 11
termsboolreqAccepting the terms. It has to be sent true.
domainstringreqThe full domain name.
auth_codestringThe transfer code from the current provider. It is required where the extension says so and is written to the service either way.
addonsstring[]The add-ons to take with it. Name management, registration privacy and forwarding, where the extension offers them.
nameserversstring[]The name servers. Two to four, and the account defaults stand in when left out.
whois_profile_idintA saved registrant profile. It fills all four roles.
contactsobjectThe contacts per role. Each role carries either a saved profile id or an inline contact, and it overrides the profile for that role.
couponsstring[]The coupon codes.
billing_profile_idintThe billing profile id.
notesstringThe order note.
paymentobjectThe payment source. The account balance or a saved card.
Response fields 201 — data — 7
order_idintThe order id.
numberstringThe order number.
actionstringThe job done: a registration or a transfer.
domainobjectThe domain service made. It carries the service id, the name, a state and the term.
totalobjectThe order total.
paymentobjectHow the collection went. It carries the road, the state, the card charged and any reason it failed.
invoiceobjectThe order invoice summary.
Errors 8
domains_not_offered422The operator does not sell domains.
terms_required422The terms were not accepted.
transfer_not_offered422The extension carries no transfer price.
auth_code_required422The extension wants a transfer code and none was sent.
domain_not_registered422The live check found the name free. A name that is not registered cannot be transferred.
domain_transfer_locked422The live check shows a transfer lock. Open it at the current provider.
checkout_blocked422A hook refused the order.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/client/domains/transfer' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"terms":true,"domain":"example.com","auth_code":"EPP-CODE","payment":{"method":"balance"}}'
const res = await fetch('https://panel.example.com/api/v1/client/domains/transfer', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ terms: true, domain, auth_code: code, payment }),
});

const { data } = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/domains/transfer');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode($order),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A transfer is ALWAYS one year and does not finish at payment: follow it on the transfer status endpoint.
$o = Kernel::internal('client:Domains/TransferDomain', ['owner_id' => $uid] + $order)['data'];
$sid = $o['domain']['service_id'];

Reading the Transfer Lock

get/api/v1/client/domains/{domain}/transfer-lock
Domains/GetTransferLock it reads live

Reads from the provider whether the domain is closed to transfer.

Response fields data — 2
lockedboolWhether the lock is on.
liveboolWhether the value came from the provider. False means the last known record is shown.
Errors 2
not_found404No such domain, it is not yours, or access to it is restricted.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/domains/example.com/transfer-lock' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/domains/${domain}/transfer-lock`, {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
if (! data.live) warnStaleValue();
$ch = curl_init('https://panel.example.com/api/v1/client/domains/' . $domain . '/transfer-lock');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Where the live flag is false the value can be old: weigh the lock decision against it.
$l = Kernel::internal('client:Domains/GetTransferLock',
    ['owner_id' => $uid, 'domain' => $domain])['data'];
$trusted = $l['live'];

Changing the Transfer Lock

put/api/v1/client/domains/{domain}/transfer-lock
Domains/UpdateTransferLock it writes to the provider

Closes the domain to transfer or opens it.

Body 1
lockedboolreqThe new lock state. It has to be a true boolean, and text or a number is refused.
Response fields data — 1
lockedboolThe new state the provider confirmed.
Errors 6
not_found404No such domain, it is not yours, or access to it is restricted.
not_actionable422The domain is not in a live state.
locked_invalid422The value is not a boolean.
lock_not_supported422The provider module has no lock.
lock_rejected422A hook refused the change.
lock_failed422The provider refused the change.
Request
curl -X PUT 'https://panel.example.com/api/v1/client/domains/example.com/transfer-lock' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"locked":false}'
const res = await fetch(`https://panel.example.com/api/v1/client/domains/${domain}/transfer-lock`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ locked: false }),
});

const { data } = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/domains/' . $domain . '/transfer-lock');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['locked' => false]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// OPENING the lock leaves the domain open to being taken: close it again as soon as the move is done.
Kernel::internal('client:Domains/UpdateTransferLock',
    ['owner_id' => $uid, 'domain' => $domain, 'locked' => false]);

Asking for the Transfer Code

post/api/v1/client/domains/{domain}/auth-code
Domains/SendAuthCode no code in the answer

Sends the transfer code to the domain owner's e-mail.

Body
No body is needed, send an empty one. The domain comes from the path and the address is the account's registered one; there is no field to send the code somewhere else.
Response fields data — 1
sentboolWhether the send ran. The code itself never appears in the answer.
Errors 5
not_found404No such domain, it is not yours, or access to it is restricted.
not_actionable422The domain is not in a live state.
auth_code_not_supported422The provider module gives no code.
auth_code_rejected422A hook refused the request.
auth_code_failed422The provider could not give the code.
Request
curl -X POST 'https://panel.example.com/api/v1/client/domains/example.com/auth-code' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/domains/${domain}/auth-code`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${clientKey}` },
});

if (res.ok) tellUserToCheckEmail();
$ch = curl_init('https://panel.example.com/api/v1/client/domains/' . $domain . '/auth-code');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The code NEVER comes back in the answer: it goes to the owner's e-mail, which is a deliberate safeguard.
Kernel::internal('client:Domains/SendAuthCode', ['owner_id' => $uid, 'domain' => $domain]);

Saving an Incoming Transfer Code

put/api/v1/client/domains/{domain}/auth-code
Domains/SaveAuthCode the key's owner

Updates the code for a transfer under way.

Body 1
codestringreqThe new transfer code. Markup is stripped and the spaces are trimmed.
Response fields data — 1
savedboolWhether the save ran.
Errors 4
not_found404No such domain, it is not yours, or access to it is restricted.
not_actionable422The domain is not in a live state.
code_required422The code is empty or ends up empty once cleaned.
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/client/domains/example.com/auth-code' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"code":"NEW-EPP-CODE"}'
const res = await fetch(`https://panel.example.com/api/v1/client/domains/${domain}/auth-code`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ code }),
});

const { data } = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/domains/' . $domain . '/auth-code');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['code' => $code]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// This endpoint SAVES and does not restart the transfer: a changed code still wants a fresh attempt at the provider.
Kernel::internal('client:Domains/SaveAuthCode',
    ['owner_id' => $uid, 'domain' => $domain, 'code' => $code]);

Asking Where a Transfer Is

get/api/v1/client/domains/{domain}/transfer-status
Domains/GetTransferStatus it asks live

Reads from the provider where a transfer under way stands.

Response fields data — 4
service_idintThe domain's service id.
domainstringThe domain name.
statusstringThe service's raw state.
transferobjectWhere the transfer stands.
statestringWhether it runs or finished.
liveboolWhether the provider answered live.
messagestringThe provider's note on progress. It carries the error where the live read failed.
expires_atstringThe expiry date reported once it finishes.
last_checkedstringWhen it was last asked. This call moves the value.
Errors 3
not_found404No such domain, it is not yours, or access to it is restricted.
not_a_transfer422The domain was not taken through a transfer.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/domains/example.com/transfer-status' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/domains/${domain}/transfer-status`, {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
showProgress(data.transfer.state, data.transfer.message);
$ch = curl_init('https://panel.example.com/api/v1/client/domains/' . $domain . '/transfer-status');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Every call goes LIVE to the provider: do not poll it often in a watcher, hourly is plenty.
$t = Kernel::internal('client:Domains/GetTransferStatus',
    ['owner_id' => $uid, 'domain' => $domain])['data'];
$done = $t['transfer']['state'] === 'completed';

Pitfalls

An unknown answer does not mean free

The availability check gives three answers and unknown is one of them: the registrar could not be reached. Treating it as free and ordering leads to a failure at registration. Trust a plain free answer alone.

A specially priced name registers for one year

On a name the registry prices apart the term field is ignored and the registration runs one year. Such a name also wants the flag accepting the special price, and the order is refused without it. Read the price from the availability answer.

Opening the lock leaves the domain exposed

The transfer lock stops your domain being moved without permission. Opening it takes that protection away for as long as it stays open. Close it again as soon as the move is done, since nobody does it for you.

The transfer code never comes back in the answer

The endpoint asking for a code says sent and nothing more; the code goes to the domain owner's e-mail. That is a deliberate safeguard: a stolen API key cannot move a domain in one call. Do not try to show the code in an interface.

The lock value is not always live

The lock read carries a live flag. Where the provider offers no reader or the read fails, the last known value comes back and it may not match reality. Check that flag in any flow that decides on the lock.

A transfer does not end at payment

Paying for a transfer order is where the work begins: the domain waits on approval at the other end and that can take days. The term is one year either way. Follow it on the transfer status endpoint, and ask rarely since every call reaches the provider.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.