Skip to content

Register a new domain and wait until it is active

7 steps · Checked against the API code 3 November 2026

A guide does one task from start to finish. Every endpoint, with all its fields and cases, is in the API reference.

Registering a domain is an order. You check that the name is free, order it and pay the invoice. Hostup then registers the domain with the registry. This page goes from the first check to a registered domain with its dom_… ID.

  • An API key with these scopes (Keys and scopes):
    • read:domains for the availability check and for finding the new domain.
    • write:orders for the order.
    • write:billing for paying the invoice.
    • write:domains only if a registration fails and you retry it.
  • The requests read the key from $HOSTUP_API_KEY. The Quickstart shows how to set it.
  • A saved card on the account. Account credit that covers the invoice works too.
  • uuidgen for the order’s retry key in step 4. Any other way to make a random UUID works too.
  • jq for the polling loop in step 7.

Ask for the name with GET /api/v2/domains/availability. Send the full name, extension included. The call waits for the check and answers with the name’s row. That can take up to about 20 seconds. Put your own name in place of example.se here and in the requests below.

GET /api/v2/domains/availability
curl "https://cloud.hostup.se/api/v2/domains/availability?name=example.se" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"name": "example.se",
"state": "available",
"actions": {
"canRegister": { "allowed": true, "reason": null },
"…": "…"
},
"billing": {
"amount": 79,
"currencyCode": "SEK",
"billingCycle": "annually"
},
"renewalAmount": 169,
"premium": false,
"supportedRegisterYears": [1, 2, 3, 5],
"registryRequirements": {
"registration": [
{ "key": "phoneNumber", "required": true, "…": "…" },
{ "key": "registrationIdentifier", "required": true, "…": "…" },
{
"key": "acceptedTerms",
"required": true,
"acceptedTermsKey": "se_registration_terms",
"…": "…"
}
],
"…": "…"
},
"…": "…"
}

Go on when actions.canRegister.allowed is true. state says where the name stands:

  • available: free to register.
  • unavailable: someone has registered the name already. If it is yours at another registrar, transfer it instead.
  • already_owned: the name is already registered at Hostup. existingDomainId is set when it is in your own account.
  • unknown: the check did not finish. unknownReason is upstream_timeout when the registry did not answer in time and upstream_unavailable otherwise. The name is not confirmed taken. Check it again.

supportedRegisterYears lists the periods you can order. billing.amount is the registration price for the default period. renewalAmount is the price to renew for one year. These prices are in SEK. The preview in step 3 gives the total in your account’s currency. GET /api/v2/products/domains/{tld} has the price of every period.

A name with premium: true is priced by the registry. The preview and the order treat a premium name as one year, whatever years says. Send "years": 1 for it. The preview in step 3 shows its price.

registryRequirements.registration is what the registry needs for a new registration. For .se and .nu that is:

  • a phone number for the holder (phoneNumber);
  • a personal identity number or organisation number (registrationIdentifier);
  • acceptance of the registry’s terms (acceptedTerms). Copy its acceptedTermsKey into the order item’s acceptedTerms.

The holder is the person or organisation the registry lists as the owner. By default it is the account holder. The order takes the holder’s name, address, phone number and identity number from the account profile. If the profile lacks something the registry needs, the order stops with 400 and names the field. Fill it in with Update your account profile or in the client area.

You can also send the missing fields in the order’s clientData, next to items. They then count for this order only. The profile stays as it is.

{
"clientData": {
"phoneNumber": "+46701740605",
"registrationIdentifier": { "value": "556000-0000", "countryCode": "SE" }
},
"…": "…"
}

Replace 556000-0000 with the holder’s real number.

To register the domain to someone else, add registrantContact to the order item. The order uses it as it is and fills no gaps from the profile. Send every field.

Today the order’s check of the registry rules reads a missing phoneNumber or registrationIdentifier from the account profile instead. It then accepts the order. The registration fails after payment. Always send both in registrantContact.

Check the names first with POST /api/v2/orders/validate-contact. It needs no key.

POST /api/v2/orders/validate-contact
curl -X POST "https://cloud.hostup.se/api/v2/orders/validate-contact" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Alex",
"lastName": "Example",
"companyName": "Example AB",
"registrationIdentifier": { "value": "556000-0000", "countryCode": "SE" },
"accountType": "organisation",
"countryCode": "SE"
}'
Response 200
{
"valid": true,
"warnings": []
}

A problem still answers 200, with valid: false. Each entry in warnings has the field, a code and a reason you can show. Fix each field it names and check again.

The check names the holder type accountType. In registrantContact the same value goes in type. registrantContact refuses accountType with 400. For an organisation, firstName and lastName are the contact person there.

This check covers the names and the organisation name. It does not check the identity number. The order does: a Swedish number in registrantContact must have 10 or 12 digits and a valid check digit.

Send this item in steps 3 and 4 in place of the item shown there:

{
"type": "domain",
"action": "register",
"domainName": "example.se",
"years": 1,
"acceptedTerms": ["se_registration_terms"],
"registrantContact": {
"type": "organisation",
"firstName": "Alex",
"lastName": "Example",
"companyName": "Example AB",
"email": "[email protected]",
"phoneNumber": "+46701740605",
"street": "Exempelgatan 1",
"city": "Stockholm",
"state": "Stockholm",
"postalCode": "111 22",
"countryCode": "SE",
"registrationIdentifier": { "value": "556000-0000", "countryCode": "SE" }
}
}

Replace 556000-0000 with the holder’s real number. This placeholder fails the order’s check on purpose: an order with it answers 400.

POST /api/v2/orders/preview prices the order without creating anything. Send the same items as the order.

POST /api/v2/orders/preview
curl -X POST "https://cloud.hostup.se/api/v2/orders/preview" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"type": "domain",
"action": "register",
"domainName": "example.se",
"years": 1,
"acceptedTerms": ["se_registration_terms"]
}
]
}'
Response 200
{
"items": [
{
"domainName": "example.se",
"type": "domain",
"periodYears": 1,
"nameserverMode": "hostup",
"…": "…"
}
],
"total": 98.75,
"currencyCode": "SEK",
"reverseCharge": false,
"…": "…"
}

total is what the invoice will come to, with VAT where VAT applies. With the key the preview uses the account’s country and currency.

nameserverMode hostup puts the domain on Hostup’s nameservers. To use other nameservers from the start, add "nameserverMode": "custom" and your nameservers to the item. Change a domain’s nameservers moves them later.

The preview does not check the terms, the phone number or the identity number. The order checks those.

Make a new attemptKey for this order and keep it in a variable:

export ATTEMPT_KEY="$(uuidgen)"

Today the key is not tied to your account. If anyone sent the same key in the last hour, you get that earlier order back with 200. Nothing is ordered for you. Never use a key copied from a page or an example.

Send the order to POST /api/v2/orders:

POST /api/v2/orders
curl -X POST "https://cloud.hostup.se/api/v2/orders" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"attemptKey": "'$ATTEMPT_KEY'",
"paymentMethod": "card",
"items": [
{
"type": "domain",
"action": "register",
"domainName": "example.se",
"years": 1,
"acceptedTerms": ["se_registration_terms"]
}
]
}'
Response 201
{
"id": "ord_01hxa3b4c5d6e7f8g9h0j1k2m3",
"status": "pending",
"invoiceId": "inv_01hxa3b4c5d6e7f8g9h0j1k2m3",
"invoice": {
"id": "inv_01hxa3b4c5d6e7f8g9h0j1k2m3",
"status": "unpaid",
"…": "…"
},
"paymentStatus": {
"status": "unpaid",
"reason": "Invoice has not been paid yet."
},
"actions": {
"canCancel": { "allowed": true, "reason": null },
"…": "…"
},
"domains": [
{ "name": "example.se", "…": "…" }
],
"…": "…"
}

Keep the order ID and the invoice ID:

export ORDER_ID="ord_…" # id above
export INVOICE_ID="inv_…" # invoiceId above

In the body, '$ATTEMPT_KEY' steps out of the single quotes. The shell then puts the key in. After a timeout or a lost answer, send the same request with the same key. Sending a change twice explains the key.

The order now exists with an unpaid invoice. Nothing is registered yet.

paymentMethod charges nothing. With card, Hostup holds back the e-mail with the unpaid invoice for a short while. That gives you time to pay before the account holder is asked to.

Payment starts the registration. An order placed with an API key is never registered before its invoice is paid. When paymentStatus.status in the order answer is already paid, go to step 6.

Send {} to POST /api/v2/billing/invoices/{id}/actions/pay to charge the primary saved card.

POST /api/v2/billing/invoices/{id}/actions/pay
curl -X POST "https://cloud.hostup.se/api/v2/billing/invoices/$INVOICE_ID/actions/pay" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
Response 200
{
"id": "inv_01hxa3b4c5d6e7f8g9h0j1k2m3",
"status": "paid",
"totals": {
"outstanding": 0,
"…": "…"
},
"…": "…"
}

Pay an invoice covers every way to pay.

Cancel the order with POST /api/v2/orders/{id}/actions/cancel and $ORDER_ID while actions.canCancel.allowed on the order is true. That also cancels its unpaid invoice.

The order answer has no domain ID. The domain is in your domain list from the moment the order exists. Look it up by name with GET /api/v2/domains. Every domain endpoint takes its dom_… ID. IDs explains the prefix.

GET /api/v2/domains
curl "https://cloud.hostup.se/api/v2/domains?name=example.se" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"data": [
{
"id": "dom_01hxa3b4c5d6e7f8g9h0j1k2m3",
"name": "example.se",
"serviceStatus": "pending",
"awaitingPayment": false,
"actionRequired": false,
"…": "…"
}
],
"hasMore": false,
"nextCursor": null
}
export DOMAIN_ID="dom_…" # data[0].id above

serviceStatus stays pending until the registry has registered the domain. awaitingPayment is true while the invoice is unpaid.

GET /api/v2/domains/{id}/lifecycle shows where the registration stands.

GET /api/v2/domains/{id}/lifecycle
curl "https://cloud.hostup.se/api/v2/domains/$DOMAIN_ID/lifecycle" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"actionRequired": false,
"registrationStatus": {
"kind": "pending",
"reason": "Registration is being processed.",
"autoRetry": null,
"…": "…"
},
"…": "…"
}

Poll it while registrationStatus.kind is pending. Keep polling while registrationStatus.autoRetry.active is true too. The registry then refused an attempt and Hostup tries again by itself. kind shows failed during that wait.

while true; do
BODY=$(curl -s "https://cloud.hostup.se/api/v2/domains/$DOMAIN_ID/lifecycle" \
-H "Authorization: Bearer $HOSTUP_API_KEY")
KIND=$(echo "$BODY" | jq -r '.registrationStatus.kind // empty')
RETRYING=$(echo "$BODY" | jq -r '.registrationStatus.autoRetry.active // false')
echo "${KIND:-no status in this answer, polling again}"
case "$KIND" in
active|unknown) break ;;
failed) [ "$RETRYING" = "true" ] || break ;;
esac
sleep 60
done

The loop stops only at a final state. An answer without registrationStatus prints a line and the loop polls again. That happens when one call fails, for example with 429. If the line keeps coming, run the lifecycle request above and read detail.

At active the domain is registered. serviceStatus in the domain list is active too. At failed the registration needs you. Retry the registration says what to do. unknown means the domain is no longer being registered, for example because the order was cancelled.

When the registry refuses the registration, registrationStatus.kind is failed and actionRequired is true. registrationStatus.reason says what is wrong. When details are missing from the account profile, registrationFailure.recovery.missing lists them. Each entry’s profileField names the profile field. For .se and .nu domains recovery is always null. There the retry below answers 422 registration_recovery_required and names what is missing. Fill the details in with Update your account profile or in the client area.

Then check registrationStatus.actions.canRetryRegistration.allowed in the lifecycle. When it is true, retry the registration with POST /api/v2/domains/{id}/actions/retry-registration. Send {"profileUpdated": true} after you changed the account details. Otherwise send {}. One exception: when a .se or .nu registration was refused over the identity number, allowed stays false even after you correct it. Send the retry with {"profileUpdated": true} once the number is corrected.

POST /api/v2/domains/{id}/actions/retry-registration
curl -X POST "https://cloud.hostup.se/api/v2/domains/$DOMAIN_ID/actions/retry-registration" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"profileUpdated": true}'
Response 200
{
"domainId": "dom_01hxa3b4c5d6e7f8g9h0j1k2m3",
"registrationStatus": "completed",
"reason": "Registration completed.",
"…": "…"
}

When the answer has operation, follow that job with wait-for-job.sh before you read the domain again:

export JOB_ID="dbj_…" # operation.jobId above
./wait-for-job.sh "/api/jobs/$JOB_ID"

Then run the polling loop from step 7 again.

Some extensions need extra details for the retry. The call then answers 422 registration_recovery_required. recovery.suggestedBody in that answer is the body to send. Fill in its empty values and send it back. Stop when a refused retry has registration.terminal set to true. Another retry will not help then.

To compare names before you choose one, send up to 50 full names in names to POST /api/v2/domains/availability. Names that share the part before the extension (example in example.se) are checked with at most eight extensions. Further extensions are dropped.

POST /api/v2/domains/availability
curl -X POST "https://cloud.hostup.se/api/v2/domains/availability" \
-H "Authorization: Bearer $HOSTUP_API_KEY" \
-H "Content-Type: application/json" \
-d '{"names": ["example.se", "example-shop.se"]}'
Response 202
{
"operation": {
"status": "queued",
"jobId": "dcheck_01hxa3b4c5d6e7f8g9h0j1k2m3",
"pollUrl": "/api/v2/domains/availability/dcheck_01hxa3b4c5d6e7f8g9h0j1k2m3"
}
}
export JOB_ID="dcheck_…" # operation.jobId above

A check of one name with three extensions or fewer usually answers at once. That answer is 200 with the rows in data, in the same shape as below. Larger checks answer 202 with a dcheck_… job, as above.

Poll the job every few seconds until status is completed or failed. This job has its own poll URL. The general job endpoint and wait-for-job.sh do not know dcheck_… IDs.

GET /api/v2/domains/availability/{jobId}
curl "https://cloud.hostup.se/api/v2/domains/availability/$JOB_ID" \
-H "Authorization: Bearer $HOSTUP_API_KEY"
Response 200
{
"status": "completed",
"data": [
{
"name": "example.se",
"state": "available",
"actions": {
"canRegister": { "allowed": true, "reason": null },
"…": "…"
},
"billing": {
"amount": 79,
"currencyCode": "SEK",
"billingCycle": "annually"
},
"…": "…"
},
{
"name": "example-shop.se",
"state": "unknown",
"unknownReason": "upstream_timeout",
"…": "…"
}
]
}

Each row has the shape of the single check in step 1. While status is processing, data may already hold the names that are done. A failed job has only a reason. Send the check again then.

A name missing from a completed job was not checked. That has one of three causes:

  • Hostup does not sell its extension.
  • It was past the eighth extension for the same name.
  • Its check failed.

Check a missing name again in a request of its own. A name whose extension Hostup does not sell then answers 400.

An answer given at once can have two more states:

  • checking: the check of this name is still running.
  • invalid: not a name Hostup can check, for example an extension Hostup does not sell. A job leaves such names out.

Every name and extension counts against the account’s lookup budget. Rate limits gives its size.

  • The account holder gets the invoice by e-mail.
  • When the invoice is paid, Hostup sends the registration to the registry. Until then registrationStatus.reason says “Waiting for the registration invoice to be paid.”
  • Temporary registry errors are retried automatically. registrationStatus.autoRetry.nextRetryAt says when the next attempt runs.
  • When the registry refuses the registration and the holder’s details are the problem, the account holder also gets an e-mail about it. Retry the registration says what to do.
  • The registry may ask the holder to confirm the holder’s e-mail address. Get the holder e-mail verification shows whether a confirmation is pending and which address it went to. Until it is confirmed, the registry can suspend the domain.

Next: Point a domain at your server. For DNS somewhere else, change the nameservers.

  • 403 human_verification_required from the availability check: the key lacks read:domains. Without it the API treats the call as an anonymous visitor. Add the scope to the key.
  • 429 rate_limit_exceeded from the availability check: the account’s lookup budget is used up. Wait the number of seconds in Retry-After before you check again.
  • 502 upstream_failed from the availability check: the check did not finish within about 20 seconds. Send it again.
  • 422 domain_not_eligible from the preview: the holder may not hold the extension, for example because of the holder’s country. With registrantContact in the item, the preview checks that holder’s country and type. Without it, it checks the account’s.
  • 400 invalid_request from the order, with domainRequirements in the body: the registry needs something the order lacks. missingAcceptedTerms names a key for acceptedTerms. missingProfileFields names profile fields to fill in or send in clientData. missingRegistrantContactFields names fields for registrantContact.
  • 409 domain_not_available from the order: the order checks the name with the registry again. Someone registered it after your check. Choose another name.
  • 409 domain_conflict from the order: the name is already at Hostup. You may also have ordered it recently.
  • 409 domain_order_in_progress from the order: two orders for the same name arrived at once. This one is the second.
  • 503 domain_verification_unavailable from the order: the registry did not answer the order’s own check. Try again in a moment with the same attemptKey.
  • 504 with the code TIMEOUT from the order: the API stopped waiting for the answer. The order may still be placed. Send the same request with the same attemptKey a little later. If the first request placed the order, you get that order back with 200.
  • 400 payment_failed from the payment: the card was declined. detail says why. Pay with another card or with account credit.
  • 409 from a retry: the registry did not accept it. code names the reason. When registration.terminal is true, another retry will not help.

No order is created when the order answers 409 or 503.