Domain Contacts and Privacy

1 views Markdown

The nine endpoints governing who a domain is registered to and who sees it.

Overview

Every domain carries contact details in four roles: the registrant, the administrative, the technical and the billing one. They sit at the registry and on most extensions a public lookup shows them.

Contact profiles are kept at the account level to save writing the same details onto every domain. A profile applies to all four roles at once, and the default one is used by itself on new registrations.

The privacy setting stops those details showing in a public lookup. It is free on some extensions and wants an add-on on others.

Reference

Reading the Domain Contacts

get/api/v1/client/domains/{domain}/whois
Domains/GetWhoisContacts it reads from the provider

Returns the contact details in a domain's four roles.

Response fields data — 4
registrantobjectThe registrant. This is the domain's legal holder.
first_namestringThe first name.
last_namestringThe last name.
namestringThe full name shown. It is built from the two where none is stored.
companystringThe organisation.
emailstringThe e-mail.
phonestringThe phone.
addressstringThe address.
citystringThe city.
statestringThe state.
zipstringThe postcode.
countrystringThe country code.
administrativeobjectThe administrative contact. It carries the same fields.
technicalobjectThe technical contact.
billingobjectThe billing contact.
Errors 4
not_found404No such domain, it is not yours, or access to it is restricted.
whois_not_supported422The provider module could not be set up.
whois_rejected422A hook refused the read.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/domains/example.com/whois' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/domains/${domain}/whois`, {
  headers: { Authorization: `Bearer ${clientKey}` },
});

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

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// All four roles ALWAYS come back; where the provider keeps one contact, all four carry it.
$w = Kernel::internal('client:Domains/GetWhoisContacts',
    ['owner_id' => $uid, 'domain' => $domain])['data'];

$same = $w['registrant']['email'] === $w['technical']['email'];

Writing the Domain Contacts

put/api/v1/client/domains/{domain}/whois
Domains/UpdateWhoisContacts it writes to the registry

Writes one contact into a chosen role or into all four.

Body 2
rolestringWhich roles to write into. All four are written when it is left out.
contactobjectreqThe contact to write.
first_namestringreqThe person's first name.
last_namestringreqThe person's last name.
companystringThe organisation. It is the one optional field.
emailstringreqA valid e-mail.
phonestringreqThe phone.
addressstringreqThe address line.
citystringreqThe city.
statestringThe state or province. Required on a domain contact and not on a saved profile.
zipstringreqThe postcode.
countrystringreqThe country code. It is raised to upper case.
Response fields data — 4
dataobjectThe four roles as they now stand. Same shape as the read endpoint.
Errors 9
not_found404No such domain, it is not yours, or access to it is restricted.
not_actionable422The domain is not live.
role_invalid422The role is none of the five values.
contact_name_required422The first or last name is empty.
contact_email_invalid422The e-mail is empty or invalid.
contact_field_required422Another required field is empty. Which one comes in the answer's detail, in the provider's own key form.
whois_not_supported422The provider module cannot write contacts.
whois_rejected422A hook refused the change.
whois_failed422The provider refused the change.
Request
curl -X PUT 'https://panel.example.com/api/v1/client/domains/example.com/whois' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"role":"technical","contact":{"first_name":"Jane","last_name":"Cooper","email":"[email protected]","phone":"+15551112233","address":"Sample St 42","city":"Austin","state":"TX","zip":"73301","country":"US"}}'
const res = await fetch(`https://panel.example.com/api/v1/client/domains/${domain}/whois`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ role: 'technical', contact }),
});

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

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Changing THE REGISTRANT counts as a change of holder on some extensions and locks the domain.
// Name the role plainly to change one; the default writes into ALL FOUR.
Kernel::internal('client:Domains/UpdateWhoisContacts', ['owner_id' => $uid, 'domain' => $domain,
    'role' => 'technical', 'contact' => $contact]);

Applying a Saved Profile

post/api/v1/client/domains/{domain}/whois/apply
Domains/ApplyWhoisProfile it fills all four roles

Writes a saved contact profile into all four roles of the domain.

Body 1
profile_idintreqThe id of the profile to apply.
Response fields data — 4
dataobjectThe four roles as they now stand.
Errors 6
not_found404No such domain, it is not yours, or access to it is restricted.
not_found404The profile was not found or belongs to another account.
not_actionable422The domain is not live.
whois_not_supported422The provider module cannot write contacts.
whois_rejected422A hook refused the change.
whois_failed422The provider refused the change.
Request
curl -X POST 'https://panel.example.com/api/v1/client/domains/example.com/whois/apply' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"profile_id":7}'
const res = await fetch(`https://panel.example.com/api/v1/client/domains/${domain}/whois/apply`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ profile_id: profileId }),
});

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

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// This writes over ALL FOUR roles: use the write endpoint with a role to change the technical one alone.
Kernel::internal('client:Domains/ApplyWhoisProfile',
    ['owner_id' => $uid, 'domain' => $domain, 'profile_id' => $pid]);

Changing the Registration Privacy

put/api/v1/client/domains/{domain}/whois-privacy
Domains/UpdateWhoisPrivacy it can be paid for

Opens or closes the hiding of contact details in a public lookup.

Body 1
enabledboolreqWhether privacy is on. It has to be a true boolean.
Response fields data — 1
whois_privacyboolThe new state the provider confirmed.
Errors 7
not_found404No such domain, it is not yours, or access to it is restricted.
not_actionable422The domain is not live.
enabled_invalid422The value is not a boolean.
privacy_not_supported422The provider module lacks privacy support.
addon_required422Privacy is paid for on this extension and the add-on was not bought.
privacy_rejected422A hook refused the change.
privacy_failed422The provider refused the change.
Request
curl -X PUT 'https://panel.example.com/api/v1/client/domains/example.com/whois-privacy' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"enabled":true}'
const res = await fetch(`https://panel.example.com/api/v1/client/domains/${domain}/whois-privacy`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ enabled: true }),
});

if (res.status === 422) offerAddon(await res.json());
$ch = curl_init('https://panel.example.com/api/v1/client/domains/' . $domain . '/whois-privacy');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $clientKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['enabled' => true]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Privacy is FREE on some extensions and an ADD-ON on others: read the refusal and offer the purchase.
$d = Kernel::internal('client:Domains/GetDomain',
    ['owner_id' => $uid, 'domain' => $domain])['data'];

$state = $d['addons']['whois_privacy']['state'];

Listing the Saved Profiles

get/api/v1/client/domains/whois-profiles
Domains/GetWhoisProfiles account-wide

Returns the account's saved contact profiles.

Response fields data[] — 4
idintThe profile id.
namestringThe profile label.
is_defaultboolWhether it is the account default. One profile at most carries it.
contactobjectThe contact saved. The same fields as a domain contact, without the built full name, and the state can be empty.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/client/domains/whois-profiles' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch('https://panel.example.com/api/v1/client/domains/whois-profiles', {
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
const fallback = data.find((p) => p.is_default);
$ch = curl_init('https://panel.example.com/api/v1/client/domains/whois-profiles');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The default profile applies by itself on NEW registrations and never touches the domains you hold.
$rows = Kernel::internal('client:Domains/GetWhoisProfiles', ['owner_id' => $uid])['data'];
$def = array_values(array_filter($rows, fn ($p) => $p['is_default']))[0] ?? null;

Creating a Profile

post/api/v1/client/domains/whois-profiles
Domains/CreateWhoisProfile account-wide

Saves a contact profile you can reuse.

Body 2
namestringreqThe profile label.
contactobjectreqThe contact to save. The state is not required here.
first_namestringreqThe person's first name.
last_namestringreqThe person's last name.
companystringThe organisation. It is the one optional field.
emailstringreqA valid e-mail.
phonestringreqThe phone.
addressstringreqThe address line.
citystringreqThe city.
statestringThe state or province. Required on a domain contact and not on a saved profile.
zipstringreqThe postcode.
countrystringreqThe country code. It is raised to upper case.
Response fields data[] — 4
dataobject[]The profile list as it now stands. Same shape as the listing endpoint.
Errors 6
name_required422The label is empty.
contact_name_required422The first or last name is empty.
contact_email_invalid422The e-mail is empty or invalid.
contact_field_required422Another required field is empty. The state is not required here.
profile_rejected422A hook refused the save.
profile_failed500The profile record could not be made.
Request
curl -X POST 'https://panel.example.com/api/v1/client/domains/whois-profiles' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Company","contact":{"first_name":"Jane","last_name":"Cooper","email":"[email protected]","phone":"+15551112233","address":"Sample St 42","city":"Austin","zip":"73301","country":"US"}}'
const res = await fetch('https://panel.example.com/api/v1/client/domains/whois-profiles', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name, contact }),
});

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

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The STATE is not required on a profile and CAN BE on a domain; filling it in is the safe road.
Kernel::internal('client:Domains/CreateWhoisProfile',
    ['owner_id' => $uid, 'name' => $name, 'contact' => $contact]);

Updating a Profile

put/api/v1/client/domains/whois-profiles/{pid}
Domains/UpdateWhoisProfile the whole contact

Rewrites a saved profile.

Body 2
namestringreqThe new label.
contactobjectreqThe whole contact. The saved one is replaced entirely.
first_namestringreqThe person's first name.
last_namestringreqThe person's last name.
companystringThe organisation. It is the one optional field.
emailstringreqA valid e-mail.
phonestringreqThe phone.
addressstringreqThe address line.
citystringreqThe city.
statestringThe state or province. Required on a domain contact and not on a saved profile.
zipstringreqThe postcode.
countrystringreqThe country code. It is raised to upper case.
Response fields data[] — 4
dataobject[]The profile list as it now stands. Same shape as the listing endpoint.
Errors 6
not_found404No such profile, or it belongs to another account.
name_required422The label is empty.
contact_name_required422The first or last name is empty.
contact_email_invalid422The e-mail is empty or invalid.
contact_field_required422Another required field is empty.
profile_rejected422A hook refused the save.
Request
curl -X PUT 'https://panel.example.com/api/v1/client/domains/whois-profiles/7' \
  -H "Authorization: Bearer $CLIENT_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Company","contact":{"first_name":"Jane","last_name":"Cooper","email":"[email protected]","phone":"+15551112233","address":"New St 7","city":"Austin","zip":"73301","country":"US"}}'
const res = await fetch(`https://panel.example.com/api/v1/client/domains/whois-profiles/${pid}`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${clientKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ name, contact }),
});

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

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Updating a profile DOES NOT reach the domains USING it: apply it again to each of them.
Kernel::internal('client:Domains/UpdateWhoisProfile',
    ['owner_id' => $uid, 'pid' => $pid, 'name' => $name, 'contact' => $contact]);

foreach ($domains as $d)
    Kernel::internal('client:Domains/ApplyWhoisProfile',
        ['owner_id' => $uid, 'domain' => $d, 'profile_id' => $pid]);

Removing a Profile

delete/api/v1/client/domains/whois-profiles/{pid}
Domains/DeleteWhoisProfile account-wide

Removes a saved contact profile.

Response fields data[] — 4
dataobject[]The profile list as it now stands. Deleting the default promotes the one left at the top.
Errors 2
not_found404No such profile, or it belongs to another account.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/client/domains/whois-profiles/7' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/domains/whois-profiles/${pid}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${clientKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/domains/whois-profiles/' . $pid);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $clientKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Removing a profile DOES NOT change the contacts on the domains using it: the registry keeps them.
Kernel::internal('client:Domains/DeleteWhoisProfile', ['owner_id' => $uid, 'pid' => $pid]);

Making a Profile the Default

post/api/v1/client/domains/whois-profiles/{pid}/default
Domains/SetDefaultWhoisProfile account-wide

Sets the profile applied by itself on new registrations.

Body
No body is needed; send an empty one. The profile comes from the path, and there are no query parameters either.
Response fields data[] — 4
dataobject[]The profile list as it now stands, the default first. Same shape as the listing endpoint.
Errors 2
not_found404No such profile, or it belongs to another account.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/client/domains/whois-profiles/7/default' \
  -H "Authorization: Bearer $CLIENT_KEY"
const res = await fetch(`https://panel.example.com/api/v1/client/domains/whois-profiles/${pid}/default`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${clientKey}` },
});

const { data } = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/client/domains/whois-profiles/' . $pid . '/default');
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 default is used on new registrations and transfers ALONE; it never reaches the domains you hold.
Kernel::internal('client:Domains/SetDefaultWhoisProfile', ['owner_id' => $uid, 'pid' => $pid]);

Pitfalls

With no role named all four are written

The role field on the contact write is optional, and leaving it out writes the contact into all four roles. Forgetting it while meaning to change the technical contact changes the registrant as well. The profile apply endpoint always writes all four.

Changing the registrant can count as a transfer of holder

The registrant is the domain's legal holder. On some extensions changing those details counts as a transfer of holder: the registry sends a confirmation e-mail and closes the domain to transfer for a while. That can follow even when you are only fixing a typo.

Updating a profile does not update the domains

A saved profile is a template: applying it copies the details onto the domain there and then. Editing it later does not reach those domains, and removing it takes nothing off the registry. Apply it again to each domain to spread a change.

The state field behaves differently in two places

The state field is not required on a saved profile and is on a domain contact. Applying a profile saved without it can fail where the provider wants it. Save your profiles with the state filled in.

Privacy is not free on every extension

Privacy is free on some extensions and opens straight away, while others want an add-on bought first and refuse the call without it. Read which case you are in from the add-on block in the domain detail.

The default profile works on new registrations alone

Making a profile the default applies it by itself on the registrations and transfers that follow. It touches none of the domains you hold. Apply it to each of them separately to bring the existing ones into line.

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.