# The password.es API is live: how to use it

> One call, no key and no sign-up, that returns passwords and the very same numbers you see on the site: bits, crack time and level. Including what it does not do yet, and why calling it is not always a good idea.

2026-08-31 · David Carrero · password.es
Original: https://password.es/en/blog/password-generator-api/

---

This blog has spent months repeating the same thing: [your password should never
leave your browser](/en/blog/why-password-es-never-sends-your-password/). The
[generator](/en/) draws its randomness from `crypto.getRandomValues()` on your
own machine, and the [checker](/en/checker/) analyses what you type without
sending it anywhere.

And today we are publishing an API that generates passwords on a server.

**The contradiction is obvious and we are not going to gloss over it**: every
API response carries a notice saying exactly that. But there are places where
there is no browser — a script provisioning a hundred accounts, a server minting
a temporary credential, an assistant that someone asks for a password and that
makes one up — and there the alternative is not "the browser": it is a badly
chosen `random()` or a string typed by hand. That is what this is for.

## The whole call

No key, no sign-up, no headers. This is all of it:

`curl -X POST https://api.password.es/v1/generate`

It returns a 16-character password and its analysis. Run while this paragraph
was being written: `x*7$9,BUy9PPvOI?`, drawn from an 89-character alphabet,
**103.6 bits** of entropy, and a crack time of 2.5 × 10¹¹ years. Level 4 out of 4.

None of those numbers is new. They come from the same engine that draws the
meter on the home page: the same [bits](/en/blog/what-are-entropy-bits/)
formula, the same attack model — 10¹² guesses per second, offline, fast hash —
and the same level scale as the checker. If the site and the API returned
different numbers for the same password, one of the two would be lying.

## What comes back, field by field

The response carries `passwords` — always an array, even when you ask for one —
and an `analysis` block with `length`, `pool`, `bits`, `log10_guesses`,
`crack_time_log10_seconds`, `crack_time`, `level`, `level_scale` and `ceiling`.

Two of those deserve a paragraph. **`level_scale` says where the level came
from** (today always `"time"`), so that a future divergence between the site and
the API shows up in the field instead of having to be inferred by comparing
numbers. And **`ceiling` says whether the figure is exact or an upper bound**:
in `generate` it is always `false`, because the server made the password and
knows which alphabet it used; in the web checker it cannot always be, because
there the password is one you brought.

Then comes `notice`, the warning, and a `_meta` block with the plan, the
language that was applied, the limits and a link back to the documentation.

## The options

All optional, and named in English because whoever integrates an API types the
field names verbatim: `length` (4–64, default 16), `count` (1–20), `lower`,
`upper`, `digits`, `symbols`, `exclude_ambiguous`, `no_repeats` and `lang`.

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

Two details the documentation spells out and that are worth knowing before you
integrate. **`exclude_ambiguous` drops six characters, not seven**: it removes
`0 O 1 I l | o`, but the vertical bar was never in the symbol set to begin with,
so the alphabet goes from 89 down to 83 and not to 82. That is why the response
reads `pool` 83. And **`no_repeats` is not an absolute guarantee**: it retries
ten times, exactly as the web generator does, and at 64 characters an adjacent
repeat slips through about 0.1% of the time. Saying so is more useful than
promising otherwise.

## The language

By default it answers in English, which is what someone integrating without
saying anything expects. You change it with `?lang=` in the URL, with `"lang"`
in the body or with the `Accept-Language` header, and if they disagree the first
one wins. An unknown language is not an error: it falls back to English.

There is an asymmetry here that you cannot guess, which is why the API declares
it: **messages exist in English and Spanish; links, in all eighteen of the
site's languages**. Asking for German gives you German links and messages still
in English. You do not have to assume: `_meta.lang` carries
`{ "messages", "links" }` with what was applied to each half.

## The MCP server

MCP is the protocol an assistant — Claude, ChatGPT — uses to reach external
tools. `https://api.password.es/mcp` is an MCP server with no sign-up and no
key, and it publishes a single tool: `generate_password`, with the same
parameters as above.

This is the part we care about most, and not for technical reasons: **an
assistant with nowhere to get a password from will make one up**, and what comes
out of that is not random, it is whatever the model considers password-shaped.
With the server connected it stops improvising and returns one made with
`crypto.getRandomValues()`, with its analysis and with the warning included in
the tool description and in every result. That warning is exactly what the
assistant ends up reading back to whoever asked for the password, which is why
it is there.

If you are coming from other MCP servers, one fact: **this one is stateless.**
The POST is answered with JSON and opens no stream, `Mcp-Session-Id` is neither
issued nor expected, a notification is answered with 202 and no body, and GET
returns 405. The 2025-06-18 specification allows all of that explicitly, but
anyone expecting sessions and SSE will debug blind if nobody tells them.

## What it does not do

**`/v1/check` returns 501.** That is not an oversight or a half-finished
endpoint: it is deliberate, and its own response explains why. Returning the
same numbers as the checker requires the very same pattern engine that runs on
the site, and that costs between 11 ms and 3.6 s of CPU per request depending on
what you send it. On a service with no sign-up and no key, that spread is a
product decision — where to put the length cap — that has not been made yet.
Meanwhile the 501 carries a `checker_url` pointing at the web checker in the
language you asked for, which does exactly this and sends nothing.

There are no accounts either, no keys and no plans. And because they do not
exist, there is not a single link to a sign-up anywhere in the API: not even in
the rate-limit error, which is where everyone puts one.

## 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 inside every
response, in `_meta.limits.burst`. Go over it and you get a 429 with
`Retry-After` and `RateLimit-*` headers.

In `_meta` you will also see a `quota` block with its values at `null`. It is
the slot reserved for when accounts exist, and it goes empty on purpose: **a
limit announced and not enforced is worse than announcing none**, because
whoever integrates respects it and codes against a number nobody is counting.

## And even so, think twice

An API that generates passwords is, deep down, an antipattern: the password
travels over the network and passes through a machine that is not yours. Us
storing nothing does not change the shape of the problem, only our part of it —
and you already know what [a promise you cannot verify is
worth](/en/blog/why-password-es-never-sends-your-password/).

That is why the warning does not live on this page alone: it travels in every
response, in the language you ask for. And that is why this paragraph is here
and not tucked away at the end of the documentation. **For a password you are
going to use yourself, this site's [generator](/en/) runs entirely in your
browser and sends nothing.** The API is for the other thing: what happens with
nobody in front of a screen.

The details live on two pages. [What the API is](/en/api/) covers who it is for
and what answers today and what does not. The [reference](/en/api/docs/) has the
nine parameters, every field explained one by one, the error codes and the
limits; it is the one you open with your editor beside it. And if what you want
is for a machine to read it, there is `openapi.json`.

---

*Sources: the API itself, verified against production on 31 August 2026 —
`POST /v1/generate`, `POST /v1/check` (501), `POST /mcp` and `GET /openapi.json`
· the analysis figures come from the same engine as the password.es generator
and checker · the attack model is 10¹² guesses per second, offline, fast hash,
the same one as the rest of the site · the MCP server implements the 2025-06-18
specification with stateless Streamable HTTP transport.*
