# Password API reference · password.es

> Every parameter of POST /v1/generate, every field of the response, the error codes and the limits of api.password.es. With a curl example that works.

password.es · Original: https://password.es/en/api/docs/

---

# Password API reference

The reference for `api.password.es`: every parameter, every response field, every error code and the limits. The examples paste into a terminal as they are.

If what you want is what this is, who it is for and when *not* to use it, start at [the API page](https://password.es/en/api/).

## The endpoints

Everything hangs off `https://api.password.es`. Two answer and one does not yet — and the one that does not says so in its own response.

- POST/v1/generate Generates one or several passwords and returns their analysis.
- GET/openapi.json The description of the API, in OpenAPI 3.1.
- POST/mcp MCP server for assistants. One tool: `generate_password`.
- POST/v1/check 501 Does not exist yet. The error says what is missing and where to go meanwhile.

## Generating a password

The body is optional: without it you get 16 characters with all four types on. With it, whatever you ask for.

The request

```
curl -X POST https://api.password.es/v1/generate \
  -H 'content-type: application/json' \
  -d '{"length":20,"exclude_ambiguous":true}'
```

The response

```
{
  "passwords": [
    "P#f.aK+w4pcsx]}Gx;*>"
  ],
  "analysis": {
    "length": 20,
    "pool": 83,
    "bits": 127.50078862693852,
    "log10_guesses": 38.08053185185735,
    "crack_time_log10_seconds": 26.08053185185735,
    "crack_time": {
      "value": "3.8 × 10¹⁸",
      "unit": "years"
    },
    "level": 4,
    "level_scale": "time",
    "ceiling": false
  },
  "notice": "Generated on someone else's machine, which is a security antipattern even though we store nothing. For a password you will actually use, the generator at https://password.es/en/ runs entirely in your browser and sends nothing.",
  "_meta": {
    "plan": "anonymous",
    "lang": {
      "messages": "en",
      "links": "en"
    },
    "limits": {
      "burst": {
        "limit": 60,
        "window_seconds": 60
      }
    },
    "quota": {
      "limit": null,
      "remaining": null,
      "reset": "2026-09-01T00:00:00.000Z"
    },
    "docs": "https://password.es/api/"
  }
}
```

## The parameters

All of them optional. The table is in English and identical across all eighteen languages, on purpose: whoever integrates an API types the field names exactly as written, and eighteen translations of `exclude_ambiguous` would be debt, not scope.

- Field
- Type
- Default
- Notes
- length
- integer 4–64
- 16
- How many characters. The same range as the generator on this site.
- count
- integer 1–20
- 1
- How many passwords to return. `passwords` is always an array, including with `count` 1.
- lower
- boolean
- true
- Include a–z (26 characters).
- upper
- boolean
- true
- Include A–Z (26 characters).
- digits
- boolean
- true
- Include 0–9 (10 characters).
- symbols
- boolean
- true
- Include `~!@#$%^&*()_+-=[]{};:,./<>?` — the same 27 as the slider on the home page, no more.
- exclude_ambiguous
- boolean
- false
- Drops `0 O 1 I l | o`. Six of them in practice, not seven: `|` is not in the symbol set to begin with. That is why `pool` reads 83 above instead of 89.
- no_repeats
- boolean
- false
- Avoids adjacent repeated characters. Not an absolute guarantee: it retries ten times, exactly as the web generator does. At length 64 that lets a repeat through about 0.1% of the time.
- lang
- string
- `en`
- Which language to answer in. One of the site's eighteen. Also accepted as `?lang=` in the URL, which wins over this field; without either, `Accept-Language` is read. An unknown value is not an error — it falls back to English. See the section below: messages exist in English and Spanish, links in all eighteen.

## What each number means

Same idea: the reference in English, the explanation beside it. None of these numbers is new — they all come from the same engine that draws the meter on the home page.

- Field
- Notes
- passwords
- An array of strings, always — including with `count` 1.
- analysis.length
- How many characters came back.
- analysis.pool
- The size of the alphabet the password was drawn from.
- analysis.bits
- `H = L·log2(N)`, the same formula as the home page. With `no_repeats` it becomes `log2(N)+(L-1)·log2(N-1)`.
- analysis.log10_guesses
- The expected work, as a base-10 logarithm: half the keyspace.
- analysis.crack_time_log10_seconds
- At 10^12 guesses/s, offline, fast hash. The same attack model as the rest of the site.
- analysis.crack_time
- The same figure in words. `unit` follows the answer language: `years`, `años`…
- analysis.level
- 0–4. The same scale as the checker, ever since the site unified the two it used to have.
- analysis.level_scale
- `"time"`. Says where the level came from, so that a future divergence is visible instead of having to be inferred by comparing numbers.
- analysis.ceiling
- `false`: the server generated the password, so the figure is exact and not a ceiling. The same flag the checker uses.
- notice
- The antipattern warning, in every single response.
- _meta.plan
- `"anonymous"`. The only lane there is; the others arrive with accounts.
- _meta.lang
- `{ "messages", "links" }` — which language each half actually came back in. They can differ, and that is why the API says so instead of leaving you to guess.
- _meta.limits.burst
- The rate limit actually enforced: `limit` requests per `window_seconds`.
- _meta.quota
- The reserved daily-quota slot. `limit` and `remaining` are `null` because nobody counts daily requests yet.
- _meta.docs
- A link back to the documentation.

## The answer language

It answers in English by default, which is what someone integrating without saying anything expects. Three ways to change it, and the first one wins if they disagree: `?lang=` in the URL, `"lang"` in the body, and the `Accept-Language` header.

There is an asymmetry worth knowing here: **messages exist in English and Spanish; links, in all eighteen languages of the site.** Asking for German gets you German links and messages still in English.

The request

```
curl -X POST 'https://api.password.es/v1/generate?lang=de' \
  -H 'content-type: application/json' \
  -d '{"length":20}'
```

The response

```
"_meta": {
  "lang": { "messages": "en", "links": "de" }
}
```

You do not have to guess: every response declares in `_meta.lang` what was applied to each half. And a language that does not exist is not an error — it falls back to English, and `_meta.lang` says so.

## Checking a password: not yet

`/v1/check` returns `501`. It is neither a bug nor an oversight: it is deliberate, and the response explains what is missing and where to go meanwhile. `checker_url` points at the site's checker in the language you asked for.

The request

```
curl -X POST https://api.password.es/v1/check \
  -H 'content-type: application/json' \
  -d '{"password":"x"}'
```

The response

```
{
  "error": "not_implemented",
  "message": "/v1/check does not exist yet. Returning the same numbers as the password.es checker requires its very same pattern engine, and that costs between 11 ms and 3.6 s of CPU per request depending on the input: it is waiting on a plan cap decision and on a length cap. Meanwhile the web checker does exactly this in your browser, sending nothing: https://password.es/en/checker/",
  "checker_url": "https://password.es/en/checker/",
  "docs": "https://password.es/api/",
  "_meta": { "…": "igual que arriba" }
}
```

## The errors

They all share one shape: a short `error` for the code, a `message` in prose —which is what an AI assistant reads out to its user—, sometimes the `field` that caused it, a `docs` link and the same `_meta` as always.

- HTTP
- error
- Notes
- 400
- `invalid_length`
- `length` outside 4–64. `field` names it.
- 400
- `invalid_count`
- `count` outside 1–20. `field` names it.
- 400
- `empty_alphabet`
- All four character types turned off, so there is no alphabet to draw from. No `field`: it is the combination, not one parameter.
- 400
- `unknown_parameter`
- A parameter this endpoint does not accept. `field` gives the offending name.
- 429
- `rate_limited`
- Over 60 requests in a minute. Carries `Retry-After` and `RateLimit-*` headers, and `limit` / `window_seconds` in the body.
- 501
- `not_implemented`
- Only from `/v1/check`. Carries `checker_url`, pointing at the web checker in the answer language.

## The limits

One, and it is the one actually enforced: **60 requests per minute per IP**. You do not have to take this page's word for it — the number travels in every response, inside `_meta.limits.burst`.

Go over it and the answer is a `429` with `Retry-After` and `RateLimit-*` headers, plus a message in prose telling you what to do. It carries no link to sign-up or pricing, because there is no sign-up and no pricing.

In `_meta` you will also find a `quota` block with both values at `null`. That is deliberate: it is the slot reserved for when accounts exist, and it is empty because nobody counts daily requests today. A limit announced and not enforced is worse than announcing none.

## The MCP server

MCP is the protocol assistants like Claude or ChatGPT use to reach external tools. Point your assistant at this address and it will generate passwords with these very numbers instead of making them up. No sign-up, no key, and the same limit of 60 requests per minute.

It is a **stateless** server, which is worth knowing if you come from other MCP servers: the POST is answered with JSON and no stream is opened, `Mcp-Session-Id` is neither issued nor expected, a notification is answered with `202` and no body, and the GET returns `405`. The 2025-06-18 specification allows all of this explicitly.

```
curl -X POST https://api.password.es/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"generate_password",
                 "arguments":{"length":20,"lang":"es"}}}'
```

It publishes **a single tool**, `generate_password`, taking the same parameters as the table above plus `lang`. There is no `check_password_strength`, and there will not be one while `/v1/check` does not exist: a tool that always returns an error is not a tool, it is a broken promise inside an assistant's catalogue.

The warning that this is an antipattern travels in the tool description and in every result. That is deliberate: it is what the assistant ends up reading out to whoever asked for the password.

## The documentation machines read

Besides this page there is an **OpenAPI 3.1** description, and that one is published: [api.password.es/openapi.json](https://api.password.es/openapi.json). It is what a client generator, an editor with autocompletion or an agent reads to learn which fields exist without anyone telling it. It describes the same parameters as the table above, the error codes, and why `quota` is empty.
