Deze blog herhaalt al maanden hetzelfde: je wachtwoord zou je browser niet
moeten verlaten. De
generator haalt zijn toeval uit crypto.getRandomValues() op je eigen
machine, en de wachtwoord-checker analyseert wat je
typt zonder het ergens naartoe te sturen.
En vandaag publiceren we een API die wachtwoorden op een server maakt.
De tegenstrijdigheid is overduidelijk en we gaan hem niet wegpoetsen: elk
antwoord van de API draagt een waarschuwing die precies dat zegt. Maar er zijn
plekken waar geen browser is — een script dat honderd accounts aanmaakt, een
server die een tijdelijke credential maakt, een assistent aan wie iemand een
wachtwoord vraagt en die er een verzint — en daar is het alternatief niet “de
browser”: het is een slecht gekozen random() of een met de hand getypte reeks.
Daarvoor is dit.
De hele aanroep
Zonder sleutel, zonder registratie, zonder headers. Dit is alles:
curl -X POST https://api.password.es/v1/generate
Hij geeft een wachtwoord van 16 tekens terug en de analyse ervan. Uitgevoerd
terwijl deze alinea werd geschreven: x*7$9,BUy9PPvOI?, getrokken uit een
alfabet van 89 tekens, 103,6 bits entropie en een kraaktijd van
2,5 × 10¹¹ jaar. Niveau 4 van 4.
Geen van die cijfers is nieuw. Ze komen uit dezelfde motor die de meter op de startpagina tekent: dezelfde bits-formule, hetzelfde aanvalsmodel — 10¹² pogingen per seconde, offline, snelle hash — en dezelfde niveauschaal als de checker. Als de site en de API verschillende cijfers gaven voor hetzelfde wachtwoord, zou een van beide liegen.
Wat er terugkomt, veld voor veld
Het antwoord bevat passwords — altijd een array, ook als je er één vraagt — en
een blok analysis met length, pool, bits, log10_guesses,
crack_time_log10_seconds, crack_time, level, level_scale en ceiling.
Twee daarvan verdienen een alinea. level_scale zegt waar het niveau vandaan
komt (vandaag altijd "time"), zodat een toekomstig verschil tussen site en
API in het veld zichtbaar wordt in plaats van te moeten worden afgeleid uit het
vergelijken van cijfers. En ceiling zegt of het cijfer exact is of een
bovengrens: bij generate staat er altijd false, want de server heeft het
wachtwoord gemaakt en kent zijn alfabet; in de checker van de site kan dat niet
altijd, want daar breng jij het wachtwoord mee.
Daarna komt notice, de waarschuwing, en een blok _meta met het plan, de
toegepaste taal, de limieten en een link terug naar de documentatie.
De opties
Allemaal optioneel, met Engelse namen omdat wie een API integreert de veldnamen
letterlijk overtypt: length (4–64, standaard 16), count (1–20), lower,
upper, digits, symbols, exclude_ambiguous, no_repeats en lang.
curl -X POST https://api.password.es/v1/generate -H 'content-type: application/json' -d '{"length":20,"exclude_ambiguous":true}'
Twee details die de documentatie noemt en die je maar beter kunt weten voordat
je integreert. exclude_ambiguous haalt zes tekens weg, geen zeven: het
schrapt 0 O 1 I l | o, maar de verticale streep zat sowieso niet in de
symbolenset, dus het alfabet zakt van 89 naar 83 en niet naar 82. Daarom leest
pool in het antwoord 83. En no_repeats is geen absolute garantie: het
probeert het tien keer opnieuw, precies zoals de generator op de site, en bij 64
tekens glipt er in ongeveer 0,1 % van de gevallen een aangrenzende herhaling
doorheen. Dat zeggen is nuttiger dan het tegendeel beloven.
De taal
Standaard antwoordt hij in het Engels, wat verwacht wordt door wie integreert
zonder iets te zeggen. Je verandert het met ?lang= in de URL, met "lang" in
de body of met de header Accept-Language, en als ze elkaar tegenspreken wint
de eerste. Een onbekende taal is geen fout: hij valt terug op het Engels.
Hier zit een asymmetrie die je niet kunt raden, en daarom verklaart de API hem:
de berichten bestaan in het Engels en Spaans; de links in alle achttien talen
van de site. Duits vragen geeft je Duitse links en berichten nog steeds in het
Engels. Je hoeft niets aan te nemen: _meta.lang bevat
{ "messages", "links" } met wat op elke helft is toegepast.
De MCP-server
MCP is het protocol waarmee een assistent — Claude, ChatGPT — externe tools
gebruikt. https://api.password.es/mcp is een MCP-server zonder registratie en
zonder sleutel, en publiceert één tool: generate_password, met dezelfde
parameters als hierboven.
Dit is het deel dat ons het meest interesseert, en niet om technische redenen:
een assistent die nergens een wachtwoord vandaan kan halen, verzint er een,
en wat daaruit komt is niet willekeurig, het is wat het model wachtwoordachtig
vindt. Met de server aangesloten stopt hij met improviseren en geeft hij er een
terug die met crypto.getRandomValues() is gemaakt, met analyse en met de
waarschuwing in de toolbeschrijving en in elk resultaat. Die waarschuwing is
precies wat de assistent uiteindelijk voorleest aan wie om het wachtwoord vroeg,
en daarom staat hij er.
Kom je van andere MCP-servers, dan één ding: deze is stateless. De POST
wordt met JSON beantwoord en opent geen stream, Mcp-Session-Id wordt niet
uitgegeven en niet verwacht, een notificatie krijgt 202 zonder body, en GET
geeft 405. De specificatie 2025-06-18 staat dat expliciet toe, maar wie sessies
en SSE verwacht, debugt blind als niemand het vertelt.
Wat hij niet doet
/v1/check geeft 501. Dat is geen vergissing en geen half afgemaakt
endpoint: het is opzet, en het antwoord zelf legt uit waarom. Dezelfde cijfers
teruggeven als de checker vraagt om precies dezelfde patroonmotor die op de site
draait, en die kost tussen 11 ms en 3,6 s CPU per verzoek, afhankelijk van wat
je stuurt. Bij een dienst zonder registratie en zonder sleutel is die bandbreedte
een productbeslissing — waar de lengtelimiet komt te liggen — die nog niet
genomen is. Ondertussen draagt de 501 een checker_url die naar de checker van
de site wijst in de gevraagde taal, die precies dit doet en niets verstuurt.
Accounts bestaan evenmin, en sleutels en abonnementen ook niet. En omdat ze niet bestaan, staat er in de hele API geen enkele link naar een registratie: zelfs niet in de fout bij overschreden limiet, waar iedereen er een zet.
De limieten
Eén, en het is degene die echt wordt afgedwongen: 60 verzoeken per minuut per
IP. Je hoeft deze pagina niet te geloven: het getal reist mee in elk antwoord,
in _meta.limits.burst. Ga je eroverheen, dan krijg je een 429 met
Retry-After en RateLimit-*-headers.
In _meta zie je ook een blok quota met de waarden op null. Dat is de plek
die gereserveerd is voor wanneer er accounts zijn, en hij blijft met opzet leeg:
een aangekondigde en niet afgedwongen limiet is erger dan helemaal geen
limiet, want wie integreert houdt zich eraan en programmeert tegen een getal
dat niemand telt.
En toch: denk er twee keer over na
Een API die wachtwoorden maakt is in de kern een antipatroon: het wachtwoord reist over het netwerk en gaat door een machine die niet van jou is. Dat wij niets bewaren verandert de vorm van het probleem niet, alleen ons deel ervan — en je weet al wat een belofte waard is die je niet kunt controleren.
Daarom leeft de waarschuwing niet alleen op deze pagina: hij reist mee in elk antwoord, in de taal die je vraagt. En daarom staat deze alinea hier en niet weggestopt achterin de documentatie. Voor een wachtwoord dat je zelf gaat gebruiken draait de generator van deze site volledig in je browser en stuurt hij niets. De API is voor het andere: wat er gebeurt zonder iemand voor een scherm.
De details staan op twee pagina’s. Wat de API is vertelt voor wie
hij is, wat vandaag antwoordt en wat niet. De referentie heeft
de negen parameters, alle velden stuk voor stuk uitgelegd, de foutcodes en de
limieten; dat is de pagina die je naast je editor openhoudt. En wil je dat een
machine het leest, dan is er openapi.json.
Bronnen: de API zelf, op 31 augustus 2026 tegen productie geverifieerd —
POST /v1/generate, POST /v1/check (501), POST /mcp en GET /openapi.json
· de cijfers van de analyse komen uit dezelfde motor als de generator en de
checker van password.es · het aanvalsmodel is 10¹² pogingen per seconde,
offline en met snelle hash, hetzelfde als op de rest van de site · de MCP-server
implementeert de specificatie 2025-06-18 met stateless Streamable
HTTP-transport.
Foto van Stanislav Kondratiev · Pexels