CustomDomain · Schema
DomainCheck
CompanyCustom DomainsDNSSSLEdge ProxySaaS InfrastructureWhite LabelMCP
Properties
| Name | Type | Description |
|---|---|---|
| domain | string | |
| sandbox | boolean | `true` when the check ran for a test mode application: nothing was looked up, the answer is always the manual rail, and the domain must be a reserved `.test` name. Omitted on a real check. |
| subdomain | string | Server's authoritative Public-Suffix-List parse verdict: the subdomain labels of `domain`, empty string when the domain IS the registrable apex. Clients must adopt this boundary instead of computing t |
| registrable_domain | string | The registrable domain (eTLD+1) of `domain` per the full PSL. |
| public_suffix | string | The effective TLD (public suffix) of `domain`, e.g. `com`, `co.uk`, `com.ng`. |
| provider | stringnull | |
| setup_type | object | |
| supports_automatic | boolean | |
| oauth_available | boolean | the OAuth-into-provider rail can be offered |
| domain_connect | boolean | the one-click (Domain Connect sync) rail is offered for this domain. Same predicate as `rails.domain_connect.available`: the provider answered discovery AND, when a provider allowlist is configured (D |
| registered | boolean | Whether the domain resolves to a delegated zone. OMITTED (not false) when detection failed for a transient reason; only an NXDOMAIN answer reports `false`. Do not tell a user their domain is unregiste |
| detection_error | string | Present only on a degraded check (detection failed): `nxdomain` means the domain is not delegated (`registered` is false, `blocked_reason` is `unregistered`); `resolver` means a transient resolver fai |
| authoritative_dns_provider | stringnull | |
| ns_support | object | |
| wildcard_support | boolean | |
| cname_flattening | boolean | |
| apex_supported | boolean | The provider can serve CNAME-like data at the zone root (ALIAS/ANAME/flattened CNAME). Always agrees with `cname_flattening`. Reported for every check, apex target or not. |
| apex_message | string | Present only when the record set this check ran against needs an apex record the provider cannot host, i.e. exactly when the apply would refuse with `apex_not_supported`. Gate any warning UI on this f |
| target_mode | string | Where this application's domains point: `edge` (proxied by the platform) or `custom` (the application's own connection target, see `PUT /applications/{id}/connect-target`). |
| apex_strategy | string | How a ROOT domain is pointed for this application on the detected provider: `alias` (an ALIAS, ANAME or flattened CNAME record to a hostname), `a_records` (address records to `apex_targets`), or `unsu |
| apex_record_type | string | The record type to choose in the provider's panel for the root (ALIAS, ANAME, CNAME or A). |
| apex_targets | array | The addresses a root domain's A records point at when `apex_strategy` is `a_records`. |
| spf_override_support | boolean | |
| caa_support | boolean | |
| supports_social_login | string | yes | no | n-a |
| rails | object | Rail truth: per rail, whether it can run for THIS domain right now, and when it can't, one machine reason why. `setup_type: automatic` / `supports_automatic` only ever meant "an adapter exists"; this |
| also_provider | object | The second way, when the registrar and the nameservers name two providers: a former Google Domains name (now Squarespace's) keeps Google's `ns-cloud-*.googledomains.com` nameservers, which Google Clou |
| recommended_rail | string | The rail the server recommends: `saved_access` when the caller already keeps a key that may act for the domain, else by precedence oauth → domain_connect → cloud_browser → api_key → manual (least typi |
| end_user_automatic | boolean | True when the recommended rail needs nothing typed by the end user (OAuth or one-click). This is the fact `supports_automatic` was always misread as. An API-key rail is "automatic" only for someone ho |
| blocked_reason | string | Present when no rail can connect this domain. `unregistered`: no nameserver delegation at all (fix it at the registrar). `platform_subdomain`: a hosting platform's own subdomain (www. |
| dashboard_url | string | Best-effort deep link into the detected provider's DNS panel for this zone, to send the user to when they add records by hand. Absent when unknown. |
| nameservers | array | The nameserver delegation the detector observed (normalized hostnames). |
| managed_dns | object | |
| nameserver_migration | object | |
| integration_warnings | array | Problems with how YOU configured the integration, not with the end user's domain. They are returned only when you supplied your own `records`. The widget never renders these; they are for your logs an |
| record_conflicts | array | Existing DNS records the automatic write would replace (`value-mismatch`, `cname-collision`, `spf-merge`); these count toward `will_fallback_to_manual`. Within /v1 a `caa-blocks-letsencrypt` advisory |
| advisories | array | Warnings that are not a record the automatic write would replace, so they never count toward `will_fallback_to_manual`. Today one kind: `caa-blocks-letsencrypt`, a CAA record that does not authorize L |
| conflict_tolerance | integer | Provider's automated-write conflict threshold (0/absent = no modeled limit). |
| will_fallback_to_manual | boolean | True when current record conflicts exceed the provider's threshold, so an automated flow would silently drop to manual. Have the user clear conflicts first. |
JSON Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://artifacts.apis.io/customdomain-ai/json-schema/customdomain-ai-domain-check-schema.json",
"title": "DomainCheck",
"x-generated": "2026-11-04",
"x-method": "derived",
"x-generator": "derive-json-schema.py",
"x-source": "openapi/customdomain-ai-openapi.yml#/components/schemas/DomainCheck",
"type": "object",
"required": [
"domain",
"setup_type"
],
"properties": {
"domain": {
"type": "string"
},
"sandbox": {
"type": "boolean",
"description": "`true` when the check ran for a test mode application: nothing was looked up, the answer is always the manual rail, and the domain must be a reserved `.test` name. Omitted on a real check."
},
"subdomain": {
"type": "string",
"description": "Server's authoritative Public-Suffix-List parse verdict: the subdomain labels of `domain`, empty string when the domain IS the registrable apex. Clients must adopt this boundary instead of computing their own."
},
"registrable_domain": {
"type": "string",
"description": "The registrable domain (eTLD+1) of `domain` per the full PSL."
},
"public_suffix": {
"type": "string",
"description": "The effective TLD (public suffix) of `domain`, e.g. `com`, `co.uk`, `com.ng`."
},
"provider": {
"type": [
"string",
"null"
]
},
"setup_type": {
"$ref": "#/$defs/SetupType"
},
"supports_automatic": {
"type": "boolean"
},
"oauth_available": {
"type": "boolean",
"description": "the OAuth-into-provider rail can be offered"
},
"domain_connect": {
"type": "boolean",
"description": "the one-click (Domain Connect sync) rail is offered for this domain. Same predicate as `rails.domain_connect.available`: the provider answered discovery AND, when a provider allowlist is configured (DC_SYNC_PROVIDERS, the default), the provider is on it, i.e. is measured to serve our template catalog. Providers that speak the protocol but have not onboarded our template (GoDaddy, IONOS, Cloudflare) report false even with DC_SYNC_ENABLED on. A domain reported true will not have its domainconnect:start refused for lack of the rail."
},
"registered": {
"type": "boolean",
"description": "Whether the domain resolves to a delegated zone. OMITTED (not false) when detection failed for a transient reason; only an NXDOMAIN answer reports `false`. Do not tell a user their domain is unregistered unless this field is present and false."
},
"detection_error": {
"type": "string",
"enum": [
"nxdomain",
"resolver"
],
"description": "Present only on a degraded check (detection failed): `nxdomain` means the domain is not delegated (`registered` is false, `blocked_reason` is `unregistered`); `resolver` means a transient resolver failure; the provider is unknown for now, retry the check."
},
"authoritative_dns_provider": {
"type": [
"string",
"null"
]
},
"ns_support": {
"type": "object",
"properties": {
"root": {
"type": "boolean"
},
"subdomains": {
"type": "boolean"
}
}
},
"wildcard_support": {
"type": "boolean"
},
"cname_flattening": {
"type": "boolean"
},
"apex_supported": {
"type": "boolean",
"description": "The provider can serve CNAME-like data at the zone root (ALIAS/ANAME/flattened CNAME). Always agrees with `cname_flattening`. Reported for every check, apex target or not."
},
"apex_message": {
"type": "string",
"description": "Present only when the record set this check ran against needs an apex record the provider cannot host, i.e. exactly when the apply would refuse with `apex_not_supported`. Gate any warning UI on this field, not on `apex_supported`: a subdomain connect on a provider with no apex capability reports `apex_supported: false` and no message, because nothing in that flow breaks."
},
"target_mode": {
"type": "string",
"enum": [
"edge",
"custom"
],
"description": "Where this application's domains point: `edge` (proxied by the platform) or `custom` (the application's own connection target, see `PUT /applications/{id}/connect-target`)."
},
"apex_strategy": {
"type": "string",
"enum": [
"alias",
"a_records",
"unsupported"
],
"description": "How a ROOT domain is pointed for this application on the detected provider: `alias` (an ALIAS, ANAME or flattened CNAME record to a hostname), `a_records` (address records to `apex_targets`), or `unsupported` (neither is possible; `apex_message` explains)."
},
"apex_record_type": {
"type": "string",
"description": "The record type to choose in the provider's panel for the root (ALIAS, ANAME, CNAME or A)."
},
"apex_targets": {
"type": "array",
"items": {
"type": "string"
},
"description": "The addresses a root domain's A records point at when `apex_strategy` is `a_records`."
},
"spf_override_support": {
"type": "boolean"
},
"caa_support": {
"type": "boolean"
},
"supports_social_login": {
"type": "string",
"description": "yes | no | n-a"
},
"rails": {
"type": "object",
"description": "Rail truth: per rail, whether it can run for THIS domain right now, and when it can't, one machine reason why. `setup_type: automatic` / `supports_automatic` only ever meant \"an adapter exists\"; this block is what to show a user. Present on every check; a degraded check (detection failed) reports only `manual` available.",
"properties": {
"oauth": {
"$ref": "#/$defs/RailInfo"
},
"domain_connect": {
"$ref": "#/$defs/RailInfo"
},
"api_key": {
"$ref": "#/$defs/RailInfo"
},
"manual": {
"$ref": "#/$defs/RailInfo"
},
"cloud_browser": {
"$ref": "#/$defs/RailInfo",
"description": "The cloud browser sign in (see `POST /connections/{id}/browser:start`). Present only while the deployment has it on; absent otherwise."
},
"nameservers": {
"type": "object",
"description": "The DNS hosting verdict: whether the application offers to host the domain's DNS by moving its nameservers (`nameserver_migration`), and where it stands among the other ways. Absent while the application's setting is off.",
"required": [
"verdict",
"reason"
],
"properties": {
"verdict": {
"type": "string",
"enum": [
"offered",
"offered_first",
"recommended",
"unavailable"
],
"description": "`offered`: after the other ways. `offered_first`: the application's setting is `prefer`. `recommended`: hosting saves the person the most for this domain (`reason` says why: no automatic way works, or the records are many). `unavailable`: offered, but not for this domain (`reason` says why)."
},
"reason": {
"type": "string",
"enum": [
"application_offers",
"application_prefers",
"no_automatic_way",
"many_records",
"plan_zone_limit",
"registrar_locks_nameservers",
"disabled",
"platform_subdomain",
"unsupported_tld",
"reserved"
],
"description": "Why. `application_offers` and `application_prefers`: the application's setting. `no_automatic_way`: nothing but copying records by hand works for this domain. `many_records`: the domain needs four or more records, a lot to copy by hand. `plan_zone_limit`: hosting would lead, but the workspace holds as many hosted zones as its plan counts (or CustomDomain™ is near its own limit), so it is offered after the other ways; the verdict is then `offered`. For `unavailable`, the reason of `nameserver_migration` (`registrar_locks_nameservers`, and the reasons of `managed_dns`: `disabled`, `platform_subdomain`, `unsupported_tld`, `reserved`)."
}
}
},
"saved_access": {
"type": "object",
"description": "The kept keys that may act for this domain for the caller, best first (see `GET /connections/{id}/saved-access`): no sign in and no new key. Present only when there is one, and then `recommended_rail` is `saved_access`. An API key caller has the workspace's keys; an end user's widget token has that end user's own, under the conditions of `GET /connections/{id}/saved-access`; a delegated agent token never gets the block.",
"required": [
"available",
"accounts"
],
"properties": {
"available": {
"type": "boolean"
},
"accounts": {
"type": "array",
"items": {
"$ref": "#/$defs/SavedAccess"
}
}
}
}
}
},
"also_provider": {
"type": "object",
"description": "The second way, when the registrar and the nameservers name two providers: a former Google Domains name (now Squarespace's) keeps Google's `ns-cloud-*.googledomains.com` nameservers, which Google Cloud DNS zones use too. `provider` is then the registrar's (`squarespace`, found over RDAP, so its secure sign-in is offered) and this is the nameservers' (`gcpdns`). Absent otherwise, and when the registrar lookup answers nothing.",
"required": [
"provider",
"provider_name",
"reason"
],
"properties": {
"provider": {
"type": "string"
},
"provider_name": {
"type": "string"
},
"reason": {
"type": "string",
"enum": [
"former_google_domains"
]
}
}
},
"recommended_rail": {
"type": "string",
"enum": [
"saved_access",
"oauth",
"domain_connect",
"cloud_browser",
"api_key",
"manual"
],
"description": "The rail the server recommends: `saved_access` when the caller already keeps a key that may act for the domain, else by precedence oauth → domain_connect → cloud_browser → api_key → manual (least typing for the end user first). `cloud_browser` is recommended only when it is available and the API key rail is unavailable, carries a caveat, or the sign in makes the API key itself (`rails.cloud_browser.key`). Absent when `blocked_reason` is set. The console and the widget follow this field so the surfaces never disagree."
},
"end_user_automatic": {
"type": "boolean",
"description": "True when the recommended rail needs nothing typed by the end user (OAuth or one-click). This is the fact `supports_automatic` was always misread as. An API-key rail is \"automatic\" only for someone holding an API key."
},
"blocked_reason": {
"type": "string",
"enum": [
"unregistered",
"platform_subdomain"
],
"description": "Present when no rail can connect this domain. `unregistered`: no nameserver delegation at all (fix it at the registrar). `platform_subdomain`: a hosting platform's own subdomain (www.<name>.vercel.app). Its DNS is the platform's; `POST /v1/connections` refuses it with `platform_subdomain`."
},
"dashboard_url": {
"type": "string",
"description": "Best-effort deep link into the detected provider's DNS panel for this zone, to send the user to when they add records by hand. Absent when unknown."
},
"nameservers": {
"type": "array",
"items": {
"type": "string"
},
"description": "The nameserver delegation the detector observed (normalized hostnames)."
},
"managed_dns": {
"$ref": "#/$defs/ManagedDNSAvailability"
},
"nameserver_migration": {
"$ref": "#/$defs/NameserverMigrationOffer"
},
"integration_warnings": {
"type": "array",
"description": "Problems with how YOU configured the integration, not with the end user's domain. They are returned only when you supplied your own `records`. The widget never renders these; they are for your logs and your developers.\n\nPresent because this class of failure is invisible to the party who can fix it: an integrator testing with `www.` sees everything work and ships, and only their end users hit the wall.",
"items": {
"type": "object",
"properties": {
"code": {
"type": "string",
"enum": [
"supplied_apex_unrealizable"
],
"description": "`supplied_apex_unrealizable`: you supplied a record for the zone apex pointing at your own host, and this domain's DNS provider has no ALIAS/ANAME/CNAME-flattening type, so a hostname cannot sit at a root there. Providers WITH flattening (Cloudflare, Route 53, DNSimple) accept it unchanged."
},
"message": {
"type": "string",
"description": "Human-readable, with the available alternatives named."
}
}
}
},
"record_conflicts": {
"type": "array",
"description": "Existing DNS records the automatic write would replace (`value-mismatch`, `cname-collision`, `spf-merge`); these count toward `will_fallback_to_manual`. Within /v1 a `caa-blocks-letsencrypt` advisory is also listed here, as before `advisories` existed; it never counts toward the fallback, and new clients should read it from `advisories`.",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "value-mismatch | cname-collision | spf-merge | caa-blocks-letsencrypt"
},
"host": {
"type": "string"
},
"type": {
"type": "string"
},
"existing": {
"type": "string"
},
"desired": {
"type": "string"
}
}
}
},
"advisories": {
"type": "array",
"description": "Warnings that are not a record the automatic write would replace, so they never count toward `will_fallback_to_manual`. Today one kind: `caa-blocks-letsencrypt`, a CAA record that does not authorize Let's Encrypt, which blocks the certificate; the fix is to add a CAA record for `letsencrypt.org` next to the existing ones, never to remove them. Returned only when the application's domains route to the edge.",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"description": "caa-blocks-letsencrypt"
},
"host": {
"type": "string"
},
"type": {
"type": "string"
},
"existing": {
"type": "string"
},
"desired": {
"type": "string"
}
}
}
},
"conflict_tolerance": {
"type": "integer",
"description": "Provider's automated-write conflict threshold (0/absent = no modeled limit)."
},
"will_fallback_to_manual": {
"type": "boolean",
"description": "True when current record conflicts exceed the provider's threshold, so an automated flow would silently drop to manual. Have the user clear conflicts first."
}
},
"$defs": {
"ManagedDNSAvailability": {
"type": "object",
"description": "Whether CustomDomain™ can host this domain's zone (managed DNS). Always present on the domains:check response. It is the deployment's and the domain's verdict only: whether the end user is offered DNS hosting is the application's `nameserver_migration` setting, and a widget token can start managed DNS only when that setting is on with target `managed`.",
"required": [
"available",
"reason"
],
"properties": {
"available": {
"type": "boolean"
},
"reason": {
"type": "string",
"enum": [
"",
"disabled",
"platform_subdomain",
"unsupported_tld",
"reserved"
],
"description": "Empty when available. `disabled`: managed DNS is off on this deployment. `platform_subdomain`: the name belongs to a hosting platform (someone.github.io), so its nameservers can't be changed. `unsupported_tld`: there is no registrable domain whose nameservers a registrar can change. `reserved`: CustomDomain™'s own domain."
}
}
},
"NameserverMigrationOffer": {
"type": "object",
"description": "What the widget offers for this domain, present only when the calling application's `nameserver_migration.mode` is `offer` or `prefer`.",
"required": [
"mode",
"target",
"available",
"path"
],
"properties": {
"mode": {
"type": "string",
"enum": [
"offer",
"prefer"
]
},
"target": {
"type": "string",
"enum": [
"managed",
"dns_host",
"custom"
]
},
"dns_host": {
"type": "string",
"description": "The DNS host of target `dns_host`."
},
"available": {
"type": "boolean"
},
"reason": {
"type": "string",
"enum": [
"disabled",
"platform_subdomain",
"unsupported_tld",
"reserved",
"registrar_locks_nameservers"
],
"description": "Why the nameservers can't move, when `available` is false: the reasons of `managed_dns`, or `registrar_locks_nameservers` (the domain is registered at a registrar that requires its own nameservers, Cloudflare Registrar; moving them needs a transfer first)."
},
"path": {
"type": "string",
"enum": [
"agent",
"manual"
],
"description": "How the widget's move would change them: `agent`, the end user signs in to their registrar in the cloud browser and approves the new nameservers; `manual`, they set them with the values shown. A connection linked to a connected account changes them through the registrar's API instead."
},
"provider": {
"type": "string",
"description": "Where the nameservers would be changed: the registrar's provider id when `registrar` names one we route to, else the detected DNS host as a guess (then `registrar` is null and nothing names it)."
},
"registrar": {
"description": "Who the domain is registered with, from RDAP; null when unknown.",
"oneOf": [
{
"$ref": "#/$defs/NameserverRegistrar"
},
{
"type": "null"
}
]
},
"nameservers": {
"type": "array",
"items": {
"type": "string"
},
"description": "The new nameservers for target `custom`. For `managed` and `dns_host` they are known once the zone exists (the migration's `new_nameservers`)."
}
}
},
"NameserverRegistrar": {
"type": "object",
"description": "The company a domain is registered with, where its nameservers are changed. Looked up over RDAP at the TLD's registry (found through the IANA bootstrap file) and matched to a provider by its IANA Registrar ID, so a domain registered at GoDaddy with its DNS at Cloudflare is changed at GoDaddy.",
"required": [
"name",
"source"
],
"properties": {
"name": {
"type": "string",
"description": "The brand (\"GoDaddy\"), the reseller the registry names, or RDAP's own name."
},
"provider": {
"type": "string",
"description": "Our provider id for it, when we route to it (`godaddy`)."
},
"iana_id": {
"type": "integer",
"description": "The IANA Registrar ID, when the registry gives one."
},
"url": {
"type": "string",
"description": "The registrar's website."
},
"source": {
"type": "string",
"enum": [
"rdap",
"request",
"account",
""
],
"description": "`rdap`: looked up. `request`: the caller named it with a key or an account. `account`: RDAP said nothing, and a connected account read the domain's registration. Empty: RDAP named a registrar we do not route to (`name` only)."
}
}
},
"RailInfo": {
"type": "object",
"required": [
"available"
],
"properties": {
"available": {
"type": "boolean"
},
"reason": {
"type": "string",
"enum": [
"unknown_provider",
"provider_has_no_oauth",
"oauth_client_not_registered",
"not_discovered",
"template_not_onboarded",
"redirect_unavailable",
"rail_disabled",
"custom_routing",
"no_adapter",
"unregistered",
"platform_subdomain",
"provider_not_listed",
"not_enabled_for_workspace",
"oauth_available",
"script_unhealthy",
"vendor_unavailable",
"monthly_cap",
"sign_in_not_wired",
"site_not_supported",
"caller_cannot_start",
"token_not_bound"
],
"description": "Set exactly when `available` is false. `provider_has_no_oauth`: the provider publishes no OAuth rail. `oauth_client_not_registered`: it does, but this deployment has not set it up: no client registered, or an MCP server sign-in not listed in `MCP_SIGNIN_PROVIDERS` (operator task). `not_discovered`: no Domain Connect discovery answer. `template_not_onboarded`: the provider speaks Domain Connect but has not onboarded our template. `redirect_unavailable`: the provider requires our return link on a one-click apply and the template cannot carry it yet. `rail_disabled`: the sync rail is off in this deployment. `custom_routing`: the application routes domains to its own infrastructure (a connect target) and the one-click templates point domains at the CustomDomain™ edge. `no_adapter`: no write adapter for the API-key rail. `unregistered` / `platform_subdomain`: the domain is blocked (see `blocked_reason`). For `rails.cloud_browser` only: `provider_not_listed` (no script for this provider, or the deployment does not offer it), `not_enabled_for_workspace` (the workspace is not on the pilot list), `oauth_available` (the provider has a sign in of its own, which wins), `script_unhealthy` (its script failed in recent sessions and the rail is paused), `vendor_unavailable` (the browser vendor failed to create sessions and the rail is paused), `monthly_cap` (the monthly browser minutes are used), `sign_in_not_wired` (the sign in token cannot be signed), `site_not_supported` (the provider keeps each account on one country site, and the site of the visitor's country is one the rail does not serve yet), `caller_cannot_start` (this caller could never start the secure sign in: an agent, a plain API call without the console, or a role that cannot write connections) and `token_not_bound` (the caller's widget token is not bound to this domain: it is bound to no domain, or only to others. Mint one with `domain` and `end_user_ref`; the widget's `getToken` is where its host hands it one. `browser:start` refuses the same token with `403 token_not_bound`)."
},
"caveat": {
"type": "string",
"description": "A note on an AVAILABLE rail: what the end user must create or qualify for before it works (a GoDaddy Personal Access Token; Namecheap's API eligibility and IP allowlist). Show it; do not treat it as unavailability."
},
"flow": {
"type": "string",
"enum": [
"oauth2",
"vercel_integration",
"porkbun_handoff",
"porkbun_dnsconnect",
"ovh_consent",
"mcp_oauth"
],
"description": "Available `oauth` rail only: the sign-in it runs, so a client can label its button (see `POST /connections/{id}/oauth:start`)."
},
"managed": {
"type": "boolean",
"description": "Available `oauth` rail only: the connect will keep its credential (managed mode). Omitted when it will not."
},
"regions": {
"type": "array",
"items": {
"type": "string",
"enum": [
"eu",
"ca",
"us"
]
},
"description": "Available `ovh_consent` rail only: the OVHcloud API regions this deployment can run the consent in, the default first."
},
"key": {
"type": "object",
"description": "Available `cloud_browser` rail only, and only when the sign in also makes a lasting API key for the person's provider account (the provider's key page is one the agent runs). The rail then leads ahead of a pasted API key: nothing is typed, and the key is the one the person would have had to find, name, scope and paste. Names what that key can do and when it ends, so a surface can say so before it asks.",
"required": [
"rights",
"ends_in_days"
],
"properties": {
"name": {
"type": "string",
"description": "The name a key made now would get at the provider (\"CustomDomain Weldon 2026-10-21 1528\"): the application's name, cut to the provider's key page's length and punctuation, then the day and minute (UTC) of this check. The sign-in types it from its own start and adds a four character code of its own at the end (\"CustomDomain Weldon 2026-10-21 1528 3f9a\"), so two sign-ins never make keys of one name; a person finds the key on the provider's page by the name it starts with. A sign in **makes or reuses** a key (it may find one of its own in the account and use that instead of making another), and the session names a new key from its own start, so this is the name to look for, not a promise of the minute. The session's `kept_key` names the key that was kept. Absent for a provider whose key is the account's own and has no name of its own (Namecheap: its API access is turned on and no key is made)."
},
"delete_url": {
"type": "string",
"format": "uri",
"description": "The provider's own page that lists and deletes its API keys, for a person who wants the key gone (most providers offer no API to delete one). Absent when none is known."
},
"rights": {
"type": "array",
"items": {
"type": "string",
"enum": [
"transfer",
"billing",
"account"
]
},
"description": "What the key can do beyond the domain's DNS records: `transfer` (it can transfer the domain away), `billing` (it can make charges), `account` (it has no scopes and holds every right the provider's API gives the account). Empty for a key that reaches DNS only."
},
"ends_in_days": {
"type": [
"integer",
"null"
],
"description": "How long the key lives from the day it is made; null for a key with no end date."
}
}
}
}
},
"SavedAccess": {
"type": "object",
"description": "A kept provider key that may act for a domain, for the caller. Never the key.",
"required": [
"account_id",
"provider",
"provider_name",
"label",
"how",
"owner_kind",
"scope",
"zone",
"ready",
"status"
],
"properties": {
"account_id": {
"type": "string"
},
"provider": {
"type": "string"
},
"provider_name": {
"type": "string"
},
"label": {
"type": "string",
"description": "The account's label. A delegated agent token reads `<Provider> key` for a key made for another application and shared with its own, and an end user's token always reads `<Provider> key`."
},
"how": {
"type": "string",
"enum": [
"api_key",
"secure_sign_in"
],
"description": "How the key came: a person pasted it, or the agent made it at a secure sign in."
},
"owner_kind": {
"type": "string",
"enum": [
"workspace",
"end_user"
]
},
"scope": {
"type": "string",
"enum": [
"all",
"selected"
]
},
"zone": {
"type": "string",
"description": "The domain's zone at the provider."
},
"ready": {
"type": "boolean",
"description": "True when the key's scope holds the zone. False: a read with the key showed it reaches the zone, and using it adds the zone to its scope."
},
"status": {
"type": "string",
"enum": [
"active",
"needs_reconnect"
],
"description": "`needs_reconnect` for a key that still works until its end date."
},
"application_id": {
"type": "string",
"description": "The application the key is bound to. A delegated agent token's answer leaves it out for a key made for another application, and an end user's token's answer for every key."
},
"key_name": {
"type": "string",
"description": "The name an agent key was made with. A delegated agent token's and an end user's token's answers leave it out for every key."
},
"key_expires_at": {
"type": "string",
"format": "date-time"
},
"last_used_at": {
"type": "string",
"format": "date-time"
},
"from_domain": {
"type": "string",
"description": "Only on an end user's own key, in an end user's token's answer: the newest domain of that end user the key already serves (a live or propagating connection of theirs in this application, not the domain asked about), as the widget words it in \"Use your Namecheap access from shop.example.com\". Left out when there is none."
}
}
},
"SetupType": {
"type": "string",
"description": "How a connection reaches live.",
"enum": [
"automatic",
"manual",
"async",
"mcp"
]
}
}
}
Work with this as data
Every JSON Schema here is available over the APIs.io API and to AI agents over MCP.
MCP server
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
Tools for schemas
4 MCP tools reach this
find_json_schemasBrowse and filter every JSON Schema in the catalog.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.
Call it yourself
curl for this page
This JSON Schema
curl "https://apis.io/api/v1/json-schemas/customdomain-ai-domain-check"
All schemas
curl "https://apis.io/api/v1/json-schemas?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Get an API key
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.