> ## Documentation Index
> Fetch the complete documentation index at: https://docs.joinmarkt.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Merchant domains

> Read and manage the store custom domain with a Bearer API key.

All routes require:

```txt theme={null}
Authorization: Bearer <your_key>
```

The key must include the scope below **and** access to that store. The dashboard permission ceiling is `manageSettings`.

```http theme={null}
GET    /api/merchant/v1/stores/{storeId}/domain
PUT    /api/merchant/v1/stores/{storeId}/domain
POST   /api/merchant/v1/stores/{storeId}/domain/verify
DELETE /api/merchant/v1/stores/{storeId}/domain
```

`PUT` and `POST /verify` require a **Premium** or **Business** plan (`customDomains`). **Free** returns `403`. `GET` and `DELETE` are not plan-gated, so a downgraded store can still be read and disconnected.

Saving a domain does not check DNS and does not make the hostname live. Call `POST /verify` after the CNAME exists. The storefront uses the custom domain only when `customDomainStatus` is `active`.

Seller steps and the DNS table are in [Custom domain](https://docs.joinmarkt.com/guides/custom-domain.md).

## Domain object

`GET`, `PUT`, and `DELETE` return this object. `POST /verify` returns the same object plus `verification`.

**subdomain** `string`

Store subdomain label, without the platform host.

**subdomainUrl** `string`

Public subdomain URL, for example `https://acme.joinmarkt.com`. This address stays available after a custom domain is connected.

**customDomain** `string | null`

Saved hostname, or `null` when none is connected. Stored lowercase, without a scheme, path, port, or leading `www.`.

**customDomainStatus** `string | null`

`pending`, `active`, `failed`, or `null`. Assign sets `pending`. Verify sets `active` when the check passes and `pending` when it does not. `null` means no domain is connected.

**customDomainVerifiedAt** `string | null`

ISO time of the last successful verification, or `null`. A later failed check does not clear it.

**primaryUrl** `string`

`https://{customDomain}` while status is `active`. Otherwise the same value as `subdomainUrl`.

**cnameTarget** `string`

Hostname the traffic CNAME must point at. The Domains page copies this value. The platform default is `stores.joinmarkt.com`.

**verificationHost** `string | null`

`_join-markt-verify.{domain}` when a domain is saved, otherwise `null`. Add a TXT on this host only when `dnsInstructions` includes that TXT row.

**dnsInstructions** `array`

Rows to create at the seller's DNS provider. Empty when no domain is saved. With Cloudflare for SaaS this is one CNAME unless Cloudflare has asked for another row.

**row**

**type** `string`

`CNAME`, `TXT`, or `A`.

**name** `string`

DNS name. `@` for the root domain, otherwise the relative label (`shop`).

**content** `string`

Record target or TXT value. For the traffic CNAME this is `cnameTarget`.

**value** `string`

Same string as `content`.

**host** `string`

Full hostname the row applies to.

**proxyStatus** `string | null`

`DNS only` or `Proxied`. `null` on TXT rows, which have no proxy toggle.

**description** `string`

What the row is for.

**cloudflareSaaSConfigured** `boolean`

`true` when Join Markt is using Cloudflare for SaaS for this hostname. Verification then requires the traffic CNAME plus an active Cloudflare hostname and certificate. The ownership TXT is not required.

**cloudflareHostnameStatus** `string | null`

Cloudflare custom-hostname status, or `null` when SaaS has no hostname yet.

**cloudflareSslStatus** `string | null`

Cloudflare certificate status, or `null`. The domain becomes `active` only when this is `active` and `cloudflareHostnameStatus` is `active`.

```json theme={null}
{
  "subdomain": "acme",
  "subdomainUrl": "https://acme.joinmarkt.com",
  "customDomain": "shop.example.com",
  "customDomainStatus": "pending",
  "customDomainVerifiedAt": null,
  "primaryUrl": "https://acme.joinmarkt.com",
  "cnameTarget": "stores.joinmarkt.com",
  "verificationHost": "_join-markt-verify.shop.example.com",
  "dnsInstructions": [
    {
      "type": "CNAME",
      "name": "shop",
      "content": "stores.joinmarkt.com",
      "value": "stores.joinmarkt.com",
      "host": "shop.example.com",
      "proxyStatus": "DNS only",
      "description": "Connects your domain to your Join Markt store. Keep Proxy status on DNS only (grey cloud)."
    }
  ],
  "cloudflareSaaSConfigured": true,
  "cloudflareHostnameStatus": "pending",
  "cloudflareSslStatus": "pending_validation"
}
```

## Get domain

```http theme={null}
GET /api/merchant/v1/stores/{storeId}/domain
```

Scope: `domain:read`. No body.

**storeId** `string` *required*

The store id.

Returns the [domain object](#domain-object). `dnsInstructions` is empty until a domain is saved.

```bash theme={null}
curl https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/domain \
  -H "Authorization: Bearer <your_key>"
```

## Assign domain

```http theme={null}
PUT /api/merchant/v1/stores/{storeId}/domain
```

Scope: `domain:write`. Replaces any hostname already saved on this store. Status is set to `pending` and `customDomainVerifiedAt` is cleared. DNS is not checked in this call.

**storeId** `string` *required*

The store id.

**domain** `string` *required*

Hostname to connect, 3–253 characters after trim. `https://`, a path, a port, and a leading `www.` are removed before save. Example: `shop.example.com` or `example.com`.

Rejected hostnames (`400`): empty after normalize, longer than 253 characters, not a domain (`Enter a valid domain like shop.example.com`), reserved (`joinmarkt.com`, `www.joinmarkt.com`, `dash.joinmarkt.com`, `stores.joinmarkt.com`, `fallback.joinmarkt.com`, `cdn.joinmarkt.com`), a local or internal name, or any Join Markt platform host. A hostname already connected to another store returns `409`.

```bash theme={null}
curl -X PUT https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/domain \
  -H "Authorization: Bearer <your_key>" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "shop.example.com" }'
```

Response is the [domain object](#domain-object) with `customDomainStatus` `pending`.

## Verify domain

```http theme={null}
POST /api/merchant/v1/stores/{storeId}/domain/verify
```

Scope: `domain:write`. No body. Requires a domain already saved with `PUT`. Checks DNS, then (when SaaS is configured) waits until Cloudflare reports the hostname and certificate active.

**storeId** `string` *required*

The store id.

The response is the [domain object](#domain-object) plus:

**verification** `object`

Result of this check. `customDomainStatus` on the parent object is the stored outcome (`active` or `pending`).

**properties**

**cnameVerified** `boolean`

The hostname CNAMEs to `cnameTarget`, or resolves to the same addresses as that target.

**txtVerified** `boolean`

The Join Markt ownership TXT was found. Ignored when `cloudflareSaaSConfigured` is true.

**message** `string`

What to do next. Same text the Domains page shows under the status banner.

**step** `string`

`needs_dns` — traffic CNAME is missing. `needs_ssl_record` — add the extra HTTPS row in `dnsInstructions`, then verify again. `ssl_issuing` — DNS is correct and the certificate is still issuing; the domain stays off the storefront. `ready` — hostname and certificate are active. `error` — Cloudflare returned a validation error, or setup could not be finished.

**cloudflareStatus** `string | null`

Cloudflare hostname status at check time.

**cloudflareSslStatus** `string | null`

Cloudflare certificate status at check time.

```bash theme={null}
curl -X POST https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/domain/verify \
  -H "Authorization: Bearer <your_key>"
```

`400` with `No custom domain configured` when nothing has been saved.

## Remove domain

```http theme={null}
DELETE /api/merchant/v1/stores/{storeId}/domain
```

Scope: `domain:write`. No body. Clears the hostname, status, and verification time, and removes the Cloudflare custom hostname when one exists. The subdomain keeps working.

**storeId** `string` *required*

The store id.

```bash theme={null}
curl -X DELETE https://dash.joinmarkt.com/api/merchant/v1/stores/STORE_ID/domain \
  -H "Authorization: Bearer <your_key>"
```

Response is the [domain object](#domain-object) with `customDomain`, `customDomainStatus`, `customDomainVerifiedAt`, and `verificationHost` set to `null`, and `dnsInstructions` set to `[]`.

## Errors

Failed responses use `{ "success": false, "error": "...", "code": "..." }`.

| Status | Code | When |
| - | - | - |
| 400 | `BAD_REQUEST` | Invalid JSON, body is not an object, `domain` missing or out of range, hostname rejected, or verify called with no domain saved |
| 401 | `UNAUTHORIZED` | Missing, invalid, expired, or revoked token |
| 403 | `FORBIDDEN` | Key cannot access this store, or the scope is missing |
| 403 | `plan_custom_domains` | `PUT` or `POST /verify` on a Free plan. Also includes `requiredPlan` (`premium`) and `upgradePath` (`/billing`) |
| 404 | `NOT_FOUND` | Store not found |
| 409 | `CONFLICT` | Hostname is already connected to another store |
| 429 | `RATE_LIMITED` | More than 120 requests per minute |

```json theme={null}
{
  "success": false,
  "error": "Custom domains require Premium or Business. Upgrade in Billing.",
  "code": "plan_custom_domains",
  "requiredPlan": "premium",
  "upgradePath": "/billing"
}
```

## Related

* [Custom domain guide](https://docs.joinmarkt.com/guides/custom-domain.md)
* [API keys](https://docs.joinmarkt.com/developers/api-keys.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.