Simplifyd CloudSimplifyd Cloud
HomeHTTP APIDomains

Domains

Search, price, register, renew, and manage domains using the workspace wallet.

Domains belong to the workspace, not to a project. Every call on this page takes a workspace token (sk_ws_…) with the domains scope. A project token (sk_proj_…) is refused.

Before the first purchase, save the workspace's registrant, the legal owner of the domains it buys, in the console under Settings → Domains. Every purchase made with a token is registered to it.

Throughout this page:

BASE = https://api.cloud.simplifyd.com/v1/workspaces/{workspace}/domains
{workspace}The workspace's id. See how resources are addressed.
{domain}A domain's id, the slug returned when you buy it or list domains. Not its name.
MoneyEvery amount is an integer in centiKobo: ₦1 = 10,000. 189200000 is ₦18,920.

Endpoints

MethodPathWhat it does
GET{BASE}/searchPrices a word under the popular TLDs
GET{BASE}/quotePrices one name for a number of years
POST{BASE}Buys a domain
GET{BASE}Lists the workspace's domains
GET{BASE}/{domain}One domain, with its registrant verification, nameservers and orders
PATCH{BASE}/{domain}Turns auto-renew or the transfer lock on or off
POST{BASE}/{domain}/renewRenews a domain now
GET{BASE}/registrantThe registrant the workspace buys as

Search for names

GET {BASE}/search?q={query}

Returns one-year prices for the requested domain and the same name under .com, .ng, .com.ng, .io, .co, .app, .dev, .net, .org, .xyz and .tech.

Query parameters

ParameterDescription
qRequiredA word (acme) or a full name (acme.com). A pasted URL works too: https://www.acme.com/about searches acme.com.
curl "$BASE/search?q=acme" \
  -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN"
const response = await fetch(`${BASE}/search?q=${encodeURIComponent('acme')}`, {
  headers: { Authorization: `Bearer ${process.env.SIMPLIFYD_WORKSPACE_TOKEN}` },
});
const { results } = await response.json();
const forSale = results.filter((r) => r.available);
client := cloud.NewClient(cloud.WithToken(os.Getenv("SIMPLIFYD_WORKSPACE_TOKEN")))
domains := client.Workspace(workspace).Domains()

results, err := domains.Search(ctx, "acme")

Response

200 OK. One quote per name:

{
  "results": [
    { "name": "acme.com", "available": false, "reason": "already registered", "years": 1, "price": 0, "vat": 0, "total": 0, "renewal_price": 0 },
    { "name": "acme.io", "available": true, "years": 1, "price": 176000000, "vat": 13200000, "total": 189200000, "renewal_price": 211200000 },
    { "name": "acme.ng", "available": false, "reason": ".ng domains are not available yet", "years": 1, "price": 0, "vat": 0, "total": 0, "renewal_price": 0 }
  ]
}

Quote a name

GET {BASE}/quote?name={name}&years={years}

Returns a quote for a domain and registration period. Send the quoted total as confirm_total when purchasing.

Query parameters

ParameterDescription
nameRequiredThe full name, e.g. acme.com.
yearsOptional1 to 10. Defaults to 1.
curl "$BASE/quote?name=acme.io&years=2" \
  -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN"
const params = new URLSearchParams({ name: 'acme.io', years: '2' });
const response = await fetch(`${BASE}/quote?${params}`, {
  headers: { Authorization: `Bearer ${process.env.SIMPLIFYD_WORKSPACE_TOKEN}` },
});
const { quote } = await response.json();
quote, err := domains.Quote(ctx, "acme.io", 2)

Response

200 OK

{
  "quote": {
    "name": "acme.io",
    "available": true,
    "years": 2,
    "price": 352000000,
    "vat": 26400000,
    "total": 378400000,
    "renewal_price": 211200000
  }
}

The quote object

FieldDescription
nameThe name, lower-cased.
availableWhether it can be bought now.
reasonWhy not, when available is false: taken, a TLD not sold yet, not a valid name.
yearsThe term priced.
priceThe charge for years, before VAT.
vatVAT on price.
totalprice + vat: what leaves the wallet. Send this as confirm_total.
renewal_priceWhat one year's renewal costs later, before VAT.

Buy a domain

POST {BASE}
Content-Type: application/json

Charges the wallet and registers the name to the workspace's saved registrant, on Simplifyd Cloud's nameservers, with the transfer lock on and WHOIS privacy. .ng and .com.ng names are registered without WHOIS privacy, so the registrant's details are public. Its DNS zone goes live with it: manage the records with the DNS API.

Body

FieldDescription
nameRequiredThe full name to buy.
confirm_totalRequired with a tokenThe total you agreed to, from a quote. Nothing is charged unless it is exactly the price at that moment. See confirming the price.
yearsOptional1 to 10. Defaults to 1.
auto_renewOptionalRenew from the wallet 30 days before expiry. Defaults to true.

A token cannot send registrant: it always buys as the workspace's saved registrant.

TOTAL=$(curl -s "$BASE/quote?name=acme.io&years=1" \
  -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN" | jq '.quote.total')

curl -X POST "$BASE" \
  -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"name\":\"acme.io\",\"years\":1,\"confirm_total\":$TOTAL}"
const headers = {
  Authorization: `Bearer ${process.env.SIMPLIFYD_WORKSPACE_TOKEN}`,
  'Content-Type': 'application/json',
};

const { quote } = await (await fetch(`${BASE}/quote?name=acme.io&years=1`, { headers })).json();
if (!quote.available) throw new Error(`acme.io cannot be bought: ${quote.reason}`);

// Your own limit, checked before anything is spent.
if (quote.total > 250_000_000) throw new Error(`acme.io costs more than ₦25,000`);

const response = await fetch(BASE, {
  method: 'POST',
  headers,
  body: JSON.stringify({ name: 'acme.io', years: 1, confirm_total: quote.total }),
});
const body = await response.json();

if (response.status === 409) {
  // The price moved since the quote. Nothing was charged; body.total is the new price.
  throw new Error(body.message);
}
if (!response.ok) throw new Error(body.message);

const domain = body.domain; // domain.status is "active" (201) or "registering" (202)
quote, err := domains.Quote(ctx, "acme.io", 1)
if err != nil {
	return err
}
if !quote.Available {
	return fmt.Errorf("acme.io cannot be bought: %s", quote.Reason)
}

d, err := domains.Register(ctx, cloud.RegisterDomainInput{
	Name:         "acme.io",
	Years:        1,
	ConfirmTotal: &quote.Total,
})
if pce, ok := cloud.AsPriceConfirmation(err); ok {
	return fmt.Errorf("the price moved to %s; nothing was charged", cloud.FormatNaira(pce.Total))
}

Response

201 Created when registration is confirmed immediately:

{
  "domain": {
    "slug": "0192f3d4-1b2a-7c88-a1e0-5d6c7b8a9f01",
    "name": "acme.io",
    "status": "active",
    "expires_at": "2027-10-14T09:30:00Z",
    "auto_renew": true,
    "registrant": {
      "first_name": "Ada", "last_name": "Obi", "company_name": "Acme Ltd",
      "address1": "1 Marina", "address2": "", "city": "Lagos", "state": "LA", "zip": "101001",
      "country": "NG", "email": "ada@acme.ng", "phone": "+2348012345678"
    },
    "registrant_verification": { "status": "pending" },
    "created_at": "2026-10-14T09:30:00Z"
  }
}

202 Accepted with "status": "registering" when confirmation is pending. The wallet has been charged. Poll Get a domain approximately once per minute until the status is active or failed. Failed registrations are refunded. .ng and .com.ng registrations can stay registering for longer while the registry reviews them.

active is not always final. In rare cases the registry rejects a registration after the registrar has confirmed it. The domain then becomes failed, and everything charged for it is refunded to the wallet. See failed domains.

The registrar emails the registrant to confirm their address. It must be confirmed within 15 days, or the domain stops resolving until it is. Follow this in registrant_verification. .ng and .com.ng domains have no such email, and their registrant_verification stays unknown.

Confirming the price

confirm_total is checked against the price at the moment the wallet is debited. If it is missing (with a token) or different, nothing is charged and the answer is 409 Conflict with the current total:

{
  "success": false,
  "code": "ERR_PRICE_CHANGED",
  "message": "the price is ₦18,920, not the ₦18,000 confirmed; nothing was charged. Send confirm_total=189200000 to pay it",
  "total": 189200000
}
codeWhen
ERR_PRICE_CONFIRMATION_REQUIREDA token left out confirm_total.
ERR_PRICE_CHANGEDconfirm_total was not the price.

Check total against what you are willing to pay, then send the same request with it. That also means a script can skip the quote: buy without confirm_total, read the price from the 409, and confirm it.

Errors

StatusWhen
400The name cannot be bought (taken, or a TLD not sold yet); years is outside 1–10; no registrant is saved; the wallet is short (the message says by how much); the workspace is suspended for an unpaid balance.
403A token sent registrant, or the token's creator can no longer buy domains.
409The price was not confirmed. See above.

List domains

GET {BASE}
curl "$BASE" -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN"
const response = await fetch(BASE, {
  headers: { Authorization: `Bearer ${process.env.SIMPLIFYD_WORKSPACE_TOKEN}` },
});
const { domains, sales_enabled } = await response.json();
list, err := domains.List(ctx)
// list.Domains, list.SalesEnabled

Response

200 OK. Every domain in the workspace, sorted by name, in one response. The list is not paginated, so a name that is not in it is not one of this workspace's domains. Failed domains are left out.

A purchase is recorded together with its charge, before the registrar is asked. If a buy request times out, list the domains:

  • The name is listed: the wallet was charged, and the domain is registering or active. Follow it with get.
  • The name is not listed: either nothing was charged, or the registration failed and was refunded.

Buying the same name again while the first request is still running answers 400 saying it is already registered, so one name cannot be charged twice.

{
  "domains": [
    {
      "slug": "0192f3d4-1b2a-7c88-a1e0-5d6c7b8a9f01",
      "name": "acme.io",
      "status": "active",
      "expires_at": "2027-10-14T09:30:00Z",
      "auto_renew": true,
      "registrant": { "first_name": "Ada", "last_name": "Obi", "email": "ada@acme.ng", "country": "NG", "...": "..." },
      "registrant_verification": { "status": "verified" },
      "created_at": "2026-10-14T09:30:00Z"
    }
  ],
  "sales_enabled": true
}
FieldDescription
slugThe domain's id, for the calls below.
statusregistering, active, or expired. A failed domain is not listed; read it by its id with get.
expires_atWhen the registration ends. Absent while registering.
auto_renewWhether it renews itself from the wallet.
registrant_verificationWhether the registrant has confirmed their address. See registrant verification.
renewal_errorWhy the last automatic renewal failed, e.g. the wallet was short. Absent otherwise.
sales_enabledWhether new domains can be bought right now.

Get a domain

GET {BASE}/{domain}

Path parameters

ParameterDescription
domainThe domain's id (slug).
curl "$BASE/0192f3d4-1b2a-7c88-a1e0-5d6c7b8a9f01" \
  -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN"
const response = await fetch(`${BASE}/${domainId}`, {
  headers: { Authorization: `Bearer ${process.env.SIMPLIFYD_WORKSPACE_TOKEN}` },
});
const { domain } = await response.json();

if (domain.registrant_verification.status === 'locked') {
  console.warn(`${domain.name} has stopped resolving: ${domain.registrant.email} has not confirmed their address`);
}
detail, err := domains.Get(ctx, domainID)

Response

200 OK. The fields from the list, and:

{
  "domain": {
    "slug": "0192f3d4-1b2a-7c88-a1e0-5d6c7b8a9f01",
    "name": "acme.io",
    "status": "active",
    "expires_at": "2027-10-14T09:30:00Z",
    "auto_renew": true,
    "registrant": { "...": "..." },
    "registrant_verification": { "status": "pending", "verify_by": "2026-10-29T09:30:00Z" },
    "created_at": "2026-10-14T09:30:00Z",
    "locked": true,
    "nameservers": ["taiwo.ns.simplifyd.net", "kehinde.ns.simplifyd.net"],
    "transfer_locked_until": "2026-12-13T09:30:00Z",
    "orders": [
      {
        "slug": "0192f3d4-1b2b-7a10-b3c4-1e2f3a4b5c6d",
        "kind": "registration",
        "years": 1,
        "amount": 176000000,
        "vat": 13200000,
        "status": "completed",
        "created_at": "2026-10-14T09:30:00Z"
      }
    ]
  }
}
FieldDescription
registrant_verificationWhether the registrant has confirmed their address. See below.
lockedThe registrar transfer lock. While it is on, the domain cannot be moved to another registrar. It says nothing about verification.
nameserversThe nameservers the registrar has for it.
transfer_locked_untilWhen the registry's own lock after a registration or transfer ends. No setting lifts it. Absent once it has passed.
ordersEvery charge for the domain, newest first: kind is registration or renewal, status is pending, completed or refunded, and failure says why one was refunded.

locked, nameservers and transfer_locked_until come from the registrar when you ask. For a domain that is not active or expired, or if the registrar cannot be reached, locked is false and the other two are absent.

An id that is not one of this workspace's domains answers 400 with "message": "domain not found".

Registrant verification

FieldDescription
registrant_verification.statusOne of the statuses below.
registrant_verification.verify_byThe registrar's deadline for confirming the address, once it has given one. Absent otherwise, and once verified.
statusWhat it means
unknownThe registrar has not reported on this domain yet. Its report can take a few minutes after a purchase. Domains are also checked with the registrar once a day.
pendingWaiting for the registrant to confirm the email the registrar sent them. The domain still resolves.
verifiedConfirmed.
lockedThe address was not confirmed in time, and the domain has stopped resolving until it is and the lock lifts.

The same address can cover several domains, so confirming it can move them all at once. A domain stays locked until the lock itself is lifted, which can be shortly after the address is confirmed.

Failed domains

A domain is failed when its registration was refused, was never completed, or was rejected by the registry after being confirmed. In every case:

  • Each order for it has status: "refunded", and failure says why. A rejection refunds renewals bought in the meantime as well, VAT included.
  • The name is free to buy again, under a new id.
  • If it had become active, its DNS zone is removed along with its records, unless a service still uses the name. Either way, the name does not resolve.
  • It can be read with this call, but it is no longer listed, and changing or renewing it answers 400 with "message": "domain not found".

Change auto-renew or the transfer lock

PATCH {BASE}/{domain}
Content-Type: application/json

Body

Send either field, or both. A field left out is not changed.

FieldDescription
auto_renewtrue to renew from the wallet 30 days before expiry, false to let it lapse.
lockedtrue to turn the registrar transfer lock on, false to turn it off.
curl -X PATCH "$BASE/0192f3d4-1b2a-7c88-a1e0-5d6c7b8a9f01" \
  -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"auto_renew": false}'
const response = await fetch(`${BASE}/${domainId}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.SIMPLIFYD_WORKSPACE_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ auto_renew: false }),
});
const { domain } = await response.json();
off := false
detail, err := domains.Update(ctx, domainID, cloud.UpdateDomainInput{AutoRenew: &off})

Response

200 OK with the domain as get returns it.


Renew a domain

POST {BASE}/{domain}/renew
Content-Type: application/json

Adds years to the registration now, charged to the wallet. A domain with auto_renew on renews itself 30 days before it expires, so this is only needed to add years early, or to renew one with auto-renew off.

Body

FieldDescription
confirm_totalRequired with a tokenThe total you agree to pay, VAT included. Works as it does for buying.
yearsOptional1 to 10. Defaults to 1.

There is no quote for a renewal. To learn the price, send the request without confirm_total: a token is answered 409 with the price in total, and nothing is charged. Then send it again with that total.

# 1. Learn the price: answered 409, nothing charged.
TOTAL=$(curl -s -X POST "$BASE/$DOMAIN_ID/renew" \
  -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"years":1}' | jq '.total')

# 2. Pay it.
curl -X POST "$BASE/$DOMAIN_ID/renew" \
  -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"years\":1,\"confirm_total\":$TOTAL}"
const url = `${BASE}/${domainId}/renew`;
const headers = {
  Authorization: `Bearer ${process.env.SIMPLIFYD_WORKSPACE_TOKEN}`,
  'Content-Type': 'application/json',
};

// Asking without a price charges nothing and answers 409 with it.
const ask = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ years: 1 }) });
const { total } = await ask.json();
if (total > 250_000_000) throw new Error('renewal costs more than ₦25,000');

const response = await fetch(url, {
  method: 'POST',
  headers,
  body: JSON.stringify({ years: 1, confirm_total: total }),
});
const { domain } = await response.json();
_, err := domains.RenewWithOptions(ctx, domainID, cloud.RenewDomainOptions{Years: 1})
pce, ok := cloud.AsPriceConfirmation(err)
if !ok {
	return err
}
if pce.Total > 250_000_000 {
	return fmt.Errorf("renewal costs %s", cloud.FormatNaira(pce.Total))
}
renewed, err := domains.RenewWithOptions(ctx, domainID, cloud.RenewDomainOptions{Years: 1, ConfirmTotal: &pce.Total})

Response

200 OK with the domain and its new expires_at:

{
  "domain": {
    "slug": "0192f3d4-1b2a-7c88-a1e0-5d6c7b8a9f01",
    "name": "acme.io",
    "status": "active",
    "expires_at": "2028-10-14T09:30:00Z",
    "auto_renew": true,
    "registrant": { "...": "..." },
    "created_at": "2026-10-14T09:30:00Z"
  }
}

If the registrar refuses the renewal, the wallet is refunded and the answer is 400 saying why. A domain still registering cannot be renewed yet.


Read the registrant

GET {BASE}/registrant

The legal owner every purchase is registered to. A token can read it but not change it: that is done by an owner or developer in the console, under Settings → Domains, or with edge domain registrant set.

curl "$BASE/registrant" -H "Authorization: Bearer $SIMPLIFYD_WORKSPACE_TOKEN"
registrant, err := domains.Registrant(ctx) // nil when none is saved

Response

200 OK

{
  "registrant": {
    "first_name": "Ada",
    "last_name": "Obi",
    "company_name": "Acme Ltd",
    "address1": "1 Marina",
    "address2": "",
    "city": "Lagos",
    "state": "LA",
    "zip": "101001",
    "country": "NG",
    "email": "ada@acme.ng",
    "phone": "+2348012345678"
  },
  "updated_at": "2026-10-01T12:00:00Z"
}

registrant is null until one is saved, and buying fails, charging nothing, until it is.


Errors

Error responses include a message describing the failure:

{
  "type": "VALIDATION_ERROR",
  "message": "your wallet is ₦4,200 short of the ₦18,920 this costs; top up and try again"
}
StatusMeaning
400The request cannot be done as asked; message says why.
401The token is missing, malformed, deleted, or its workspace was deleted.
403The token is for another workspace, lacks the domains scope, tried something tokens cannot do (see workspace tokens), or its creator can no longer buy domains.
409A price was not confirmed. The body carries the current total.
5xxOur side, or the registrar is unreachable. Nothing is charged for a request that fails before the registrar is asked; one that fails after is settled in the background.