# Totalum App Builder API — Buy a Domain

This file is the complete reference for this area. The short guide for agents is https://www.totalum.app/totalum-api.md.

- Base URL: `https://builder-api.totalum.app/api/v1/vcaas`
- Authentication: header `api-key: tlm_sk_…` on every request, from a server only.
- Response envelope: `{ "errors": null | { "errorCode", "errorMessage", "errorDetails?" }, "data": … }`.


---

# Buy a Domain

> Register a domain on Totalum and connect it in one step.

A domain bought on Totalum is registered in your name, paid with the **card saved on your Totalum account** (not with credits), renewed every year automatically (the renewal is charged 30 days before the expiry and retried 21 and 15 days before; if it is still unpaid 14 days before the expiry, automatic renewal is switched off and you are told by e-mail — renew by hand until the expiry), and connected to a project without any DNS work. Buying domains needs the Starter plan or above; a plan holds up to 3 (Starter), 10 (Builder), 50 (Professional) or unlimited (Enterprise) domains, and at most 5 can be bought a day. Prices are in your billing currency.

These endpoints are account-level (no project in the path). Purchases, renewals, contacts and transfers are the account owner's.

**Before buying**, create the registrant contact (the domain's legal owner, published in WHOIS with privacy on): [Create Registrant Contact](#create-registrant-contact). The registrant must verify their e-mail address by the deadline shown in `verification.verifyBy`, or the domain is suspended until they do.

## Search Domains

```endpoint
method: GET
path: /api/v1/vcaas/domains/search
cost: Free
```

Which names are available, and their price.

**Query parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `q` | string | Yes | A name (`barbershop`) or a full domain (`barbershop.com`) |
| `tlds` | string | No | Comma-separated extensions to check (`com,io`); default: every extension sold |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.results[].domain` | string | The domain |
| `data.results[].available` | boolean | Can be bought now |
| `data.results[].premium` | boolean | Always `false` (premium names are not offered) |
| `data.results[].price` | number | First-year price |
| `data.results[].renewalPrice` | number | Yearly renewal price. It can be much higher than `price` (a registry's first-year promotion), so show both: "€3.59 first year, then €91.19/year" |
| `data.results[].currency` | string | `eur` or `usd` |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  "https://builder-api.totalum.app/api/v1/vcaas/domains/search?q=barbershop&tlds=com,io"
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "results": [{ "domain": "barbershop.io", "available": true, "premium": false, "price": 52.8, "renewalPrice": 52.8, "currency": "eur" }, { "domain": "barbershop.com", "available": false, "premium": false, "price": 0, "renewalPrice": 0, "currency": "eur" }] } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `VALIDATION_ERROR` | 400 | `q` is missing |

## List TLDs

```endpoint
method: GET
path: /api/v1/vcaas/domains/tlds
cost: Free
```

The extensions sold and their prices.

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.tlds[].tld` | string | Extension (`com`) |
| `data.tlds[].price` | number | First-year price |
| `data.tlds[].renewalPrice` | number | Yearly renewal |
| `data.tlds[].currency` | string | `eur` or `usd` |
| `data.tlds[].requirements` | boolean | The registry asks for extra registrant data |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/tlds
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "tlds": [{ "tld": "com", "price": 14.4, "renewalPrice": 14.4, "currency": "usd", "requirements": false }] } }
```
:::

## Buy Domain

```endpoint
method: POST
path: /api/v1/vcaas/domains/purchase
cost: Card payment (not credits)
```

Charge the saved card and register the domain. If the registration fails, the payment is refunded at once. Send an `Idempotency-Key` header so a retried request never buys twice (a retry of a domain you already hold returns it without a new charge). With `projectId`, the domain is attached to that project in the same call.

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `domain` | string | Yes | The domain to buy |
| `contactId` | string | Yes | The registrant contact |
| `years` | number | No | Always `1`: a domain is bought for one year, then renews every year. Any other value is refused (`400 VALIDATION_ERROR`) |
| `projectId` | string | No | Attach to this project after the purchase |
| `canonicalHost` | string | No | Where the site answers: `"www"` (default), `"app"` or any DNS label (letters, digits, inner hyphens) — the site is served at `<label>.<domain>` and the bare domain redirects there (301, path and query kept) — or `"apex"` (the bare domain itself). Refused with `400 VALIDATION_ERROR` before any charge when it is not a valid label |
| `expectedPrice` | number | No | The price you showed: if the live price differs, nothing is bought (`409 PRICE_CHANGED`) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.domain.id` | string | Domain id |
| `data.domain.domain` | string | The domain |
| `data.domain.status` | string | `"active"` |
| `data.domain.registeredAt` | string | ISO 8601 |
| `data.domain.expiresAt` | string | ISO 8601 |
| `data.domain.autoRenew` | boolean | `true` |
| `data.domain.pricePaid` | number | What was charged |
| `data.domain.renewalPriceEstimate` | number | The expected yearly renewal |
| `data.domain.attachedProjectId` | string \| undefined | The project it serves |
| `data.domain.contactVerificationPending` | boolean | The registrant still has to verify their e-mail |
| `data.warnings` | object[] \| undefined | `{ code }` — e.g. the attach did not happen (attach it with [Attach Domain](#attach-domain)) |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7d1e4c2a-buy-barbershop-io" \
  -d '{"domain":"barbershop.io","contactId":"ct_01k6y2","projectId":"barber-bookings","expectedPrice":52.8}' \
  https://builder-api.totalum.app/api/v1/vcaas/domains/purchase
```

:::success Success · 201 Created
```json
{ "errors": null, "data": { "domain": { "id": "dom_01k6y3", "domain": "barbershop.io", "status": "active", "registeredAt": "2026-10-08T12:30:00.000Z", "expiresAt": "2027-10-08T12:30:00.000Z", "autoRenew": true, "pricePaid": 52.8, "renewalPriceEstimate": 52.8, "attachedProjectId": "barber-bookings", "contactVerificationPending": true } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `VALIDATION_ERROR` | 400 | `domain` or `contactId` is missing |
| `TLD_NOT_ALLOWED` | 400 | The extension is not sold |
| `DOMAIN_PREMIUM_NOT_ALLOWED` | 400 | Premium names are not sold |
| `NO_PAYMENT_METHOD` | 402 | Add a card to your Totalum account first |
| `PAYMENT_FAILED` | 402 | The card was declined (`errorDetails.declineCode`) |
| `FEATURE_DISABLED` | 403 | Buying domains is not available yet on your account |
| `PLAN_REQUIRED` | 403 | Needs the Starter plan or above |
| `DOMAIN_LIMIT_REACHED` | 403 | Your plan's number of domains is reached |
| `OWNER_ONLY` | 403 | Only the account owner can buy |
| `DOMAIN_UNAVAILABLE` | 409 | The domain is taken |
| `PRICE_CHANGED` | 409 | The price differs from `expectedPrice` (`errorDetails.newPrice`) |
| `DOMAIN_CONTACT_REQUIRED` | 422 | Create the registrant contact first |
| `REGISTRATION_FAILED` | 422 | The registration was refused; the payment was refunded |
| `RATE_LIMIT_EXCEEDED` | 429 | More than 5 purchases today |

## List Domains

```endpoint
method: GET
path: /api/v1/vcaas/domains
cost: Free
```

The account's domains.

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.domains` | object[] | Domains, with the fields of [Get Domain](#get-domain) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "domains": [{ "id": "dom_01k6y3", "domain": "barbershop.io", "status": "active", "registeredAt": "2026-10-08T12:30:00.000Z", "expiresAt": "2027-10-08T12:30:00.000Z", "autoRenew": true, "renewal": { "status": "scheduled", "nextChargeAt": "2027-09-08T12:30:00.000Z", "attempts": [] }, "pricing": { "pricePaid": 52.8, "currency": "eur", "renewalPriceEstimate": 52.8 }, "privacy": true, "locked": true, "contactId": "ct_01k6y2", "contactVerified": false, "nameservers": ["ns1.totalum-dns.com"], "dnsManaged": true, "attachedProjectId": "barber-bookings", "canonicalHost": "www", "exportAvailableAt": "2026-12-07T12:30:00.000Z", "verification": { "contactVerificationPending": true, "verifyBy": "2026-10-23T12:30:00.000Z" } }] } }
```
:::

## Get Domain

```endpoint
method: GET
path: /api/v1/vcaas/domains/:domainId
cost: Free
```

One domain.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.id` | string | Domain id |
| `data.domain` | string | The domain |
| `data.status` | string | `"active"`, `"expired_grace"`, `"expired"`, `"export_pending"`, `"transferred_out"`, `"external_dns"` or `"suspended_verification"` |
| `data.registeredAt` | string | ISO 8601 |
| `data.expiresAt` | string | ISO 8601 |
| `data.autoRenew` | boolean | Renews automatically |
| `data.renewal` | object | `{ status: "scheduled" \| "charging" \| "renewed" \| "cancelled", nextChargeAt, attempts[], lastRenewedAt?, cancelledAt? }` |
| `data.pricing` | object | `{ pricePaid, currency, renewalPriceEstimate }` |
| `data.privacy` | boolean | WHOIS privacy |
| `data.locked` | boolean | Transfer lock |
| `data.contactId` | string | Registrant contact |
| `data.contactVerified` | boolean | Registrant e-mail verified |
| `data.nameservers` | string[] | Nameservers |
| `data.dnsManaged` | boolean | DNS is managed on Totalum |
| `data.attachedProjectId` | string \| undefined | The project it serves |
| `data.canonicalHost` | string \| undefined | `"www"` or `"apex"` |
| `data.exportAvailableAt` | string | From when it can be transferred away (60 days after registration) |
| `data.verification` | object | `{ contactVerificationPending, verifyBy }` |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "id": "dom_01k6y3", "domain": "barbershop.io", "status": "active", "registeredAt": "2026-10-08T12:30:00.000Z", "expiresAt": "2027-10-08T12:30:00.000Z", "autoRenew": true, "renewal": { "status": "scheduled", "nextChargeAt": "2027-09-08T12:30:00.000Z", "attempts": [] }, "pricing": { "pricePaid": 52.8, "currency": "eur", "renewalPriceEstimate": 52.8 }, "privacy": true, "locked": true, "contactId": "ct_01k6y2", "contactVerified": true, "nameservers": ["ns1.totalum-dns.com"], "dnsManaged": true, "attachedProjectId": "barber-bookings", "canonicalHost": "www", "exportAvailableAt": "2026-12-07T12:30:00.000Z", "verification": { "contactVerificationPending": false, "verifyBy": null } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `DOMAIN_NOT_FOUND` | 404 | No such domain in your account |

## Update Domain

```endpoint
method: PATCH
path: /api/v1/vcaas/domains/:domainId
cost: Free
```

Turn WHOIS privacy or automatic renewal on or off. Owner only.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `privacy` | boolean | No | WHOIS privacy |
| `autoRenew` | boolean | No | Automatic yearly renewal |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | object | The domain after the change, as in [Get Domain](#get-domain) |

**Example request**

```bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"autoRenew":false}' \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "id": "dom_01k6y3", "domain": "barbershop.io", "status": "active", "autoRenew": false, "renewal": { "status": "cancelled", "nextChargeAt": null, "attempts": [] } } }
```
:::

## Renew Domain

```endpoint
method: POST
path: /api/v1/vcaas/domains/:domainId/renew
cost: Card payment
```

Renew now for one year (within 90 days of expiry, or during the grace period after it). Owner only.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.domain` | object | The domain after the renewal |
| `data.order` | object \| null | The renewal order |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/renew
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "domain": { "id": "dom_01k6y3", "domain": "barbershop.io", "status": "active", "expiresAt": "2028-10-08T12:30:00.000Z" }, "order": { "kind": "renewal", "status": "completed", "customerPrice": 52.8, "currency": "eur" } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `DOMAIN_NOT_RENEWABLE` | 409 | Not within 90 days of expiry (`errorDetails.expiresAt`) |
| `PAYMENT_FAILED` | 402 | The card was declined |
| `REGISTRATION_FAILED` | 422 | The renewal was refused; the payment was refunded |

## Attach Domain

```endpoint
method: POST
path: /api/v1/vcaas/domains/:domainId/attach
cost: 4 credits (first domain of the project)
```

Serve a project from this domain; Totalum writes the DNS records itself (and the ones that prove ownership of each address). The project must be published. The site answers at `<canonicalHost>.<domain>` and the bare domain redirects there (301, path and query kept); follow both addresses with [Get Custom Domain](https://www.totalum.app/docs/api/domains#get-custom-domain).

**Change the prefix:** call it again with the same `projectId` and another `canonicalHost` (`{"projectId":"barber-bookings","canonicalHost":"app"}`): the site moves to the new address in a few minutes and the old one stops answering. No second fee; the same prefix again changes nothing.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `projectId` | string | Yes | The project |
| `canonicalHost` | string | No | `"www"` (default), `"app"` or any DNS label, or `"apex"` — as in [Buy Domain](#buy-domain) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.domain` | object | The domain, as in [Get Domain](#get-domain) |
| `data.customDomain` | object | The project's custom domain: `{ domain, hostnames, status, … }` |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"barber-bookings"}' \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/attach
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "domain": { "id": "dom_01k6y3", "domain": "barbershop.io", "attachedProjectId": "barber-bookings", "canonicalHost": "www" }, "customDomain": { "domain": "barbershop.io", "status": "pending" } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `VALIDATION_ERROR` | 400 | `projectId` is missing, or `canonicalHost` is not `"apex"` or one DNS label |
| `PROJECT_NOT_FOUND` | 404 | No such project in your account |
| `NO_DEPLOYMENT` | 404 | Publish the project first |
| `DOMAIN_ALREADY_ATTACHED` | 409 | Attached to another project; detach it first |
| `INSUFFICIENT_CREDITS` | 402 | Not enough credits for the domain fee |
| `PLATFORM_CAPABILITY_QUOTA_REACHED` | 403 | Custom domains are not included on your plan |

## Detach Domain

```endpoint
method: POST
path: /api/v1/vcaas/domains/:domainId/detach
cost: Free
```

Stop serving the project from this domain. The domain stays yours and is **not refunded**. Its automatic renewal keeps running unless you stop it: to remove the domain for good, detach it and then [Update Domain](#update-domain) with `{"autoRenew":false}` — it stays registered to you until `expiresAt`, then expires. Afterwards the project can [attach](#attach-domain) another domain you bought, [buy one](#buy-domain) with `projectId`, or [connect one you own](https://www.totalum.app/docs/api/domains#add-custom-domain).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | object | The domain, as in [Get Domain](#get-domain) |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/detach
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "id": "dom_01k6y3", "domain": "barbershop.io", "status": "active" } }
```
:::

## List DNS Records

```endpoint
method: GET
path: /api/v1/vcaas/domains/:domainId/dns/records
cost: Free
```

The domain's DNS records. `managed: true` records serve the attached project and cannot be changed.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.records[].id` | string | Record id |
| `data.records[].type` | string | `A`, `AAAA`, `CNAME`, `ALIAS`, `MX`, `TXT`, `SRV` or `NS` |
| `data.records[].host` | string | Host (`""` = the domain itself) |
| `data.records[].answer` | string | Value |
| `data.records[].ttl` | number | Seconds (≥ 300) |
| `data.records[].priority` | number \| undefined | MX/SRV priority |
| `data.records[].managed` | boolean | Written by Totalum for the attached project |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/dns/records
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "records": [{ "id": "rec_1", "type": "MX", "host": "", "answer": "mx.mailprovider.com", "ttl": 300, "priority": 10, "managed": false }] } }
```
:::

## Create DNS Record

```endpoint
method: POST
path: /api/v1/vcaas/domains/:domainId/dns/records
cost: Free
```

Add a record (for example your e-mail provider's MX and TXT records).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `record` | object | Yes | `{ type, host, answer, ttl, priority? }` as in [List DNS Records](#list-dns-records) |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | object | The record, with `id` and `managed: false` |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"record":{"type":"TXT","host":"","answer":"v=spf1 include:_spf.mailprovider.com ~all","ttl":300}}' \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/dns/records
```

:::success Success · 201 Created
```json
{ "errors": null, "data": { "id": "rec_2", "type": "TXT", "host": "", "answer": "v=spf1 include:_spf.mailprovider.com ~all", "ttl": 300, "managed": false } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `DNS_RECORD_CONFLICT` | 400 | Conflicts with an existing record |
| `DNS_RECORD_LIMIT` | 400 | The domain already has 200 records |
| `VALIDATION_ERROR` | 400 | Invalid record |

## Update DNS Record

```endpoint
method: PATCH
path: /api/v1/vcaas/domains/:domainId/dns/records/:recordId
cost: Free
```

Change a record you created.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |
| `recordId` | string | The record id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `record` | object | Yes | `{ type, host, answer, ttl, priority? }` |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | object | The record after the change |

**Example request**

```bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"record":{"type":"MX","host":"","answer":"mx2.mailprovider.com","ttl":300,"priority":20}}' \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/dns/records/rec_1
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "id": "rec_1", "type": "MX", "host": "", "answer": "mx2.mailprovider.com", "ttl": 300, "priority": 20, "managed": false } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `DOMAIN_NOT_FOUND` | 404 | No such record |
| `DNS_RECORD_MANAGED` | 409 | The record serves the attached project; detach the domain first |

## Delete DNS Record

```endpoint
method: DELETE
path: /api/v1/vcaas/domains/:domainId/dns/records/:recordId
cost: Free
```

Delete a record you created.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |
| `recordId` | string | The record id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.deleted` | boolean | `true` |

**Example request**

```bash
curl -X DELETE -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/dns/records/rec_2
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "deleted": true } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `DOMAIN_NOT_FOUND` | 404 | No such record |
| `DNS_RECORD_MANAGED` | 409 | The record serves the attached project |

## Get Forwarding

```endpoint
method: GET
path: /api/v1/vcaas/domains/:domainId/forwarding
cost: Free
```

The domain's URL forwarding rules (a host of the domain redirecting to another URL).

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.forwarding[].host` | string | Host of the domain (`shop.barbershop.io`) |
| `data.forwarding[].forwardsTo` | string | Target URL |
| `data.forwarding[].type` | string | `"redirect"` (301), `"302"` or `"masked"` |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/forwarding
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "forwarding": [{ "host": "shop.barbershop.io", "forwardsTo": "https://store.example.com", "type": "redirect" }] } }
```
:::

## Set Forwarding

```endpoint
method: PUT
path: /api/v1/vcaas/domains/:domainId/forwarding
cost: Free
```

Replace the whole set of forwarding rules (up to 50). A host the attached project serves is refused.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `forwarding` | object[] | Yes | `{ host, forwardsTo, type }`; `[]` removes every rule |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.forwarding` | object[] | The rules now in place |

**Example request**

```bash
curl -X PUT \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"forwarding":[{"host":"shop.barbershop.io","forwardsTo":"https://store.example.com","type":"redirect"}]}' \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/forwarding
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "forwarding": [{ "host": "shop.barbershop.io", "forwardsTo": "https://store.example.com", "type": "redirect" }] } }
```
:::

## Get Transfer Code

```endpoint
method: POST
path: /api/v1/vcaas/domains/:domainId/export
cost: Free
```

Unlock the domain and get its transfer (auth) code, to move it to another registrar. Also e-mailed to the owner. Possible 60 days after registration. Owner only.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.authCode` | string | The transfer code |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/export
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "authCode": "Xk7#pQ2!mZ9w" } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `DOMAIN_TRANSFER_LOCKED` | 423 | Still inside the 60-day lock (`errorDetails.availableAt`) |
| `DOMAIN_NOT_RENEWABLE` | 409 | A renewal charge is in progress; retry shortly |

## Cancel Transfer

```endpoint
method: POST
path: /api/v1/vcaas/domains/:domainId/export/cancel
cost: Free
```

Lock the domain again and cancel a pending transfer. Owner only.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data` | object | The domain, as in [Get Domain](#get-domain) |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/export/cancel
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "id": "dom_01k6y3", "domain": "barbershop.io", "status": "active", "locked": true } }
```
:::

## List Domain Orders

```endpoint
method: GET
path: /api/v1/vcaas/domains/:domainId/orders
cost: Free
```

The domain's payments: registration, renewals and refunds.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `domainId` | string | The domain id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data[].kind` | string | `"registration"`, `"renewal"` or `"refund"` |
| `data[].status` | string | Order status |
| `data[].customerPrice` | number | Amount |
| `data[].currency` | string | `eur` or `usd` |
| `data[].at` | string | ISO 8601 |
| `data[].stripePaymentIntentId` | string \| undefined | The card payment |
| `data[].refunded` | boolean | The payment was refunded |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/dom_01k6y3/orders
```

:::success Success · 200 OK
```json
{ "errors": null, "data": [{ "kind": "registration", "status": "completed", "customerPrice": 52.8, "currency": "eur", "at": "2026-10-08T12:30:00.000Z", "stripePaymentIntentId": "pi_3Q…", "refunded": false }] }
```
:::

## List Registrant Contacts

```endpoint
method: GET
path: /api/v1/vcaas/domains/contacts
cost: Free
```

The account's registrant contacts.

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.contacts[].id` | string | Contact id (for `contactId`) |
| `data.contacts[].label` | string \| undefined | Your label |
| `data.contacts[].contact` | object | The contact data (see [Create Registrant Contact](#create-registrant-contact)) |

**Example request**

```bash
curl -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/contacts
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "contacts": [{ "id": "ct_01k6y2", "label": "Main", "contact": { "firstName": "Ana", "lastName": "García", "email": "ana@barbershop.io", "phone": "+34600111222", "address1": "Calle Mayor 1", "city": "Madrid", "zip": "28013", "country": "ES" } }] } }
```
:::

## Create Registrant Contact

```endpoint
method: POST
path: /api/v1/vcaas/domains/contacts
cost: Free
```

Create the legal owner data used to register domains. Owner only.

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | string | No | Up to 100 characters |
| `contact` | object | Yes | The registrant |
| `contact.firstName` | string | Yes | First name |
| `contact.lastName` | string | Yes | Last name |
| `contact.organization` | string | No | Company |
| `contact.email` | string | Yes | E-mail (must be verified after a purchase, see `verification.verifyBy`) |
| `contact.phone` | string | Yes | International format: `+`, the country calling code and the number, `+34644479777`. Spaces, dashes, dots and parentheses are accepted and removed, and a leading `00` is read as `+` (`0034 644 47 97 77` is saved as `+34644479777`) |
| `contact.address1` | string | Yes | Address |
| `contact.address2` | string | No | Address, second line |
| `contact.city` | string | Yes | City |
| `contact.state` | string | No | State or province |
| `contact.zip` | string | Yes | Postal code |
| `contact.country` | string | Yes | Two-letter ISO 3166-1 alpha-2 code (`ES`, `US`), any case, saved uppercase. A country name (`United States`) is refused |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.id` | string | Contact id |
| `data.label` | string \| undefined | Label |
| `data.contact` | object | The contact |

**Example request**

```bash
curl -X POST \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"label":"Main","contact":{"firstName":"Ana","lastName":"García","email":"ana@barbershop.io","phone":"+34600111222","address1":"Calle Mayor 1","city":"Madrid","zip":"28013","country":"ES"}}' \
  https://builder-api.totalum.app/api/v1/vcaas/domains/contacts
```

:::success Success · 201 Created
```json
{ "errors": null, "data": { "id": "ct_01k6y2", "label": "Main", "contact": { "firstName": "Ana", "lastName": "García", "email": "ana@barbershop.io", "phone": "+34600111222", "address1": "Calle Mayor 1", "city": "Madrid", "zip": "28013", "country": "ES" } } }
```
:::

**Error codes**

| Code | HTTP | Meaning |
|---|---|---|
| `VALIDATION_ERROR` | 400 | A field is missing or invalid; the message names it, e.g. `country must be a 2-letter ISO code like ES, US` or `phone must be in international format like +34644479777 (spaces are removed)` |
| `OWNER_ONLY` | 403 | Only the account owner |

## Update Registrant Contact

```endpoint
method: PATCH
path: /api/v1/vcaas/domains/contacts/:contactId
cost: Free
```

Change a contact. Every active domain registered with it is updated; a change of registrant restarts the 60-day transfer lock of those domains.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `contactId` | string | The contact id |

**Body parameters**

| Field | Type | Required | Description |
|---|---|---|---|
| `label` | string | No | Label |
| `contact` | object | Yes | The full contact, as in Create Registrant Contact |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.contact` | object | The saved contact |
| `data.warnings` | object[] | `{ code: "DOMAIN_TRANSFER_LOCKED", domains }` when domains were re-locked |

**Example request**

```bash
curl -X PATCH \
  -H "api-key: tlm_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"contact":{"firstName":"Ana","lastName":"García","email":"ana@barbershop.io","phone":"+34600111222","address1":"Calle Mayor 2","city":"Madrid","zip":"28013","country":"ES"}}' \
  https://builder-api.totalum.app/api/v1/vcaas/domains/contacts/ct_01k6y2
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "contact": { "id": "ct_01k6y2", "label": "Main" }, "warnings": [] } }
```
:::

## Resend Registrant Verification

```endpoint
method: POST
path: /api/v1/vcaas/domains/contacts/:contactId/resend-verification
cost: Free
```

Send the registrant's verification e-mail again.

**Path parameters**

| Parameter | Type | Description |
|---|---|---|
| `contactId` | string | The contact id |

**Response fields**

| Field | Type | Description |
|---|---|---|
| `data.sent` | number | Verification e-mails sent (0 when nothing is pending) |

**Example request**

```bash
curl -X POST -H "api-key: tlm_sk_your_key" \
  https://builder-api.totalum.app/api/v1/vcaas/domains/contacts/ct_01k6y2/resend-verification
```

:::success Success · 200 OK
```json
{ "errors": null, "data": { "sent": 1 } }
```
:::
