Custom domain

Verify the custom domain

Runs one verification pass against the DNS you published.

POST/workspace/domain/verify

Runs one verification pass against the DNS you published. It is safe to call repeatedly and it never throws for a domain that simply is not ready yet: a failed check returns 200 with the domain's new status and failureReason, not an error status. Branch on domain.status, not on the HTTP code.

Verification is two-staged. First the Envless ownership TXT record must resolve, which flips challengeVerifiedAt. Only then is the certificate requested and its own validation records appear in records, meaning a full setup normally needs at least two calls with a DNS publish between them.

Calls are debounced. Verifying inside the debounce window returns the current state untouched, with retryAfterSeconds telling you when the next real attempt is allowed. Attempts are also capped: once spent, remove the domain and claim it again.

Requires the workspace:write scope.

curl --request POST \
  --url 'https://api.envless.cloud/workspace/domain/verify' \
  --header 'Authorization: Bearer ev_sk_xxxxxxxxxxxx' \
  --header 'Content-Type: application/json' \
  --data '{
  "reattempt": false
}'
Try itPOSThttps://api.envless.cloud/workspace/domain/verify

Authorization

  • Authorizationstringrequiredheader

    Bearer token: an Envless API key (ev_sk_…), created in the dashboard or with POST /keys. The key is bound to one workspace; every request runs inside it.

Request body

application/json · optional

  • reattemptboolean

    Set true to force a fresh certificate attempt after a failure, instead of only re-reading the current state.

    Default: false

Responses

200OK

Verify the custom domain succeeded.

  • successbooleanrequired
  • codeintegerrequired

    The HTTP status of this response, repeated in the body.

  • messagestringrequired
  • errornullrequired
  • dataobjectrequired
    Show child attributesHide child attributes
    • domainobject | nullrequired

      One of: object, null

  • _reqobject
    Show child attributesHide child attributes
    • reqIdstring
    • resTimeinteger | null
{
    "success": true,
    "code": 200,
    "message": "OK",
    "error": null,
    "data": {
        "domain": {
            "uniqueId": "<string>",
            "hostname": "<string>",
            "hostnameUnicode": "<string>",
            "status": "<string>",
            "records": [
                {
                    "type": "<string>",
                    "name": "<string>",
                    "value": "<string>",
                    "note": "<string>"
                }
            ],
            "verificationErrors": [
                "<string>"
            ],
            "failureReason": "<string>",
            "hint": "<string>",
            "lastCheckedAt": "2026-05-30T10:14:22.000Z",
            "retryAfterSeconds": 1,
            "canReattempt": true,
            "lockedByPlan": true,
            "graceEndsAt": "2026-05-30T10:14:22.000Z",
            "verifiedAt": "2026-05-30T10:14:22.000Z",
            "createdAt": "2026-05-30T10:14:22.000Z"
        }
    },
    "_req": {
        "reqId": "<string>",
        "resTime": 1
    }
}

Error envelope

Every failure below returns the same shape with success: false; branch on error.code, never on message.

  • successbooleanrequired
  • codeintegerrequired

    The HTTP status of this response, repeated in the body.

  • messagestringrequired
  • errorobjectrequired

    Machine-readable failure detail. Null on success.

    Show child attributesHide child attributes
    • codeenum<string>required

      Stable error code. Branch on this, never on message.

      Options: "auth.api_key_required", "auth.api_key_not_found", "auth.api_key_user_missing", "auth.api_key_owner_not_member", "auth.scope_missing", "auth.scope_ceiling_exceeded", "auth.key_project_restricted", "auth.product_restricted", "auth.permission_denied", "auth.self_mutation_forbidden", "auth.step_up_required", "auth.step_up_fresh_required", "auth.cli_token_not_accepted", "auth.two_factor_required", "auth.workspace_two_factor_required", "auth.two_factor_invalid", "auth.two_factor_replayed", "auth.two_factor_locked", "auth.two_factor_not_enabled", "auth.two_factor_already_enabled", "request.body_must_be_json", "request.body_invalid", "request.body_too_large", "request.rate_limited", "request.invalid_pagination", "workspace.not_found", "workspace.slug_conflict", "workspace.limit_reached", "workspace.delete_last", "workspace.delete_too_early", "workspace.delete_has_projects", "domain.not_configured", "domain.not_found", "domain.invalid", "domain.taken", "domain.attempt_limit", "product.not_found", "product.slug_conflict", "product.locked", "project.not_found", "project.ambiguous_product", "project.slug_conflict", "environment.not_found", "environment.slug_conflict", "environment.expose_disabled", "environment.expose_private_product", "variable.not_found", "variable.name_conflict", "variable.ciphertext_invalid", "variable.baseline_drifted", "version.not_found", "member.not_found", "member.already_exists", "member.self_invite", "member.self_mutation", "member.owner_immutable", "member.owner_cannot_leave", "member.seat_limit", "invite.already_pending", "role.owner_not_assignable", "role.limit_reached", "role.immutable", "role.in_use", "invite.not_found", "invite.expired", "role.not_found", "role.name_conflict", "key.not_found", "key.limit_reached", "webhook.not_found", "webhook.delivery_not_found", "plan.limit_reached", "plan.upgrade_required", "request.not_found", "request.method_not_allowed", "request.conflict", "auth.unauthorized", "auth.forbidden", "request.invalid", "server.error"

    • resourcestring

      The resource or scope the failure relates to.

    • fieldstring

      The request field that caused the failure.

  • dataobject | nullrequired

    Null on most failures. A plan limit carries { limit, current }.

  • _reqobject
    Show child attributesHide child attributes
    • reqIdstring
    • resTimeinteger | null
{
    "success": false,
    "code": 401,
    "message": "Something went wrong.",
    "error": {
        "code": "auth.api_key_required",
        "resource": "<string>",
        "field": "<string>"
    },
    "data": {},
    "_req": {
        "reqId": "<string>",
        "resTime": 1
    }
}

401Unauthorized

  • auth.api_key_required - the Authorization header is missing, or its value does not start with ev_sk_.
  • auth.api_key_not_found - the key is unknown, inactive or revoked.
  • auth.api_key_user_missing - the key is valid but its owner no longer exists.
  • auth.api_key_owner_not_member - the key's owner is no longer a member of the key's workspace.

403Forbidden

  • auth.scope_missing - the key lacks workspace:write.
  • auth.workspace_two_factor_required - the workspace requires two-factor authentication and the key's owner has not turned it on.

404Not Found

domain.not_found - no custom domain has been claimed for this workspace.

409Conflict

domain.attempt_limit - the verification attempt limit is spent; remove the domain and claim it again.

413Payload Too Large

request.body_too_large - the request body is larger than 1 MiB (1,048,576 bytes).

500Internal Server Error

  • domain.not_configured - this deployment has no custom-domain provider configured.
  • server.error - an unexpected error; retry with backoff.

503Service Unavailable

server.error - the API is in maintenance mode or is missing its configuration; retry with backoff.