# acme.sh "Verify error": read the detail and fix validation

> acme.sh shows Verify error when the CA fails to validate your domain. The detail after it (404, 403, timeout, NXDOMAIN, wrong TXT) names the failed check.

Source: https://ssl.sh/errors/acme-sh-verify-error/
Last updated: 2026-11-10

**`Verify error` means the CA tried to validate your domain and failed; acme.sh prints the CA's explanation after the colon.** Read that explanation: an HTTP status (404, 403), a timeout, `NXDOMAIN` or `Incorrect TXT record` each point to a different fix.

## Quick fix

Rerun with debugging against staging so failures do not count against production:

```bash
acme.sh --issue -d example.com -w /var/www/html --staging --debug 2
```

Then map the detail to a cause:

| Detail after `Verify error:` | Most likely cause | Section |
|---|---|---|
| `Invalid response ...: 404` | wrong webroot or server block | 1 |
| `Invalid response ...: 403` | deny rule, WAF, permissions | 2 |
| `Timeout during connect`, `Connection refused` | port 80 closed, wrong IP | 3 |
| `Incorrect TXT record`, `NXDOMAIN looking up TXT` | DNS-01 record not visible | 4 |
| `CAA record ... prevents issuance` | CAA does not list the CA | 5 |

When it works on staging, run the same command without `--staging` and with `--force` once.

## Causes

The detail after `Verify error:` maps to five causes: a webroot mismatch (404), access rules (403), connection problems, an invisible DNS-01 record, and CAA or other CA refusals.

### 1. HTTP-01 webroot mismatch (404)

acme.sh writes the token into `<webroot>/.well-known/acme-challenge/`. If the web server serves a different directory for that hostname, the CA gets 404.

- **Confirm:** `echo ok > /var/www/html/.well-known/acme-challenge/t` and `curl http://example.com/.well-known/acme-challenge/t` returns 404.
- **Fix:** pass the real `root` of the server block (`nginx -T | grep root`), or use `--nginx` / `--apache` mode so acme.sh configures the server temporarily. A reverse proxy needs a `location` that serves the challenge directory locally.

### 2. Access denied (403)

- **Fix:** add an exception for `/.well-known/acme-challenge/` before dotfile deny rules and WAF rules; see [Certbot 403 unauthorized](https://ssl.sh/errors/certbot-unauthorized-403/), the fixes are identical.

### 3. Connection problems

- **Fix:** open TCP 80 in every firewall layer, remove stale AAAA records and avoid geo-blocking; see [timeout during connect](https://ssl.sh/errors/certbot-timeout-during-connect/). In `--standalone` mode acme.sh needs port 80 free, and installs that ask for `socat` need it installed first.

### 4. DNS-01 record not visible

The DNS API hook created the record in the wrong zone, the API credentials lack permission, or the CA checked before all authoritative servers had the record.

- **Confirm:** `dig +short TXT _acme-challenge.example.com @<authoritative server>` during the run shows nothing or an old value.
- **Fix:** check the API environment variables (for example `CF_Token` for Cloudflare, `Ali_Key`/`Ali_Secret` for Alibaba Cloud DNS, `DP_Id`/`DP_Key` for DNSPod), make sure the domain's NS records point at that provider, and add `--dnssleep 120` if propagation is slow. With CNAME delegation use `--challenge-alias`.

### 5. CAA or other CA-side refusals

- **Fix:** add the CA's identifier to CAA, for example `letsencrypt.org` or the one your ZeroSSL documentation gives; see [CAA record prevents issuance](https://ssl.sh/errors/caa-record-prevents-issuance/).

## Fixes by environment

Outside validation itself, repeat failures usually trace back to the default CA, a missing install step, lost Docker state or an old acme.sh version.

**Switching CA:**

```bash
acme.sh --set-default-ca --server letsencrypt
```

**Installing the certificate** (so renewals reach the web server):

```bash
acme.sh --install-cert -d example.com \
  --key-file /etc/nginx/ssl/example.com.key \
  --fullchain-file /etc/nginx/ssl/example.com.pem \
  --reloadcmd "systemctl reload nginx"
```

**Docker (the `neilpang/acme.sh` image):** mount the `/acme.sh` data directory as a volume so accounts and certificates survive restarts; otherwise every run is a new order and may hit [rate limits](https://ssl.sh/errors/lets-encrypt-rate-limit/).

**Wildcard certificates:** require DNS-01 (`--dns dns_cf` etc.); HTTP-01 cannot validate `*.example.com`.

**Upgrading:** `acme.sh --upgrade` fixes many errors from changed CA APIs on old installations.

## Don't do this

- **Keep `--force` in cron jobs.** It renews every run regardless of expiry and burns through limits.
- **Post `--debug` logs publicly without redacting.** They contain API keys and account URLs.
- **Disable the firewall or WAF entirely to make validation pass.** Exempt only the challenge path, or move to DNS-01.
