{
  "openapi": "3.1.0",
  "info": {
    "title": "api.password.es",
    "version": "1.0.0",
    "summary": "Genera y comprueba contraseñas con los mismos números que password.es.",
    "description": "Los valores que devuelve esta API salen del mismo motor que la web, y un test de paridad lo verifica contra el código en producción.\n\nNo se registra ninguna contraseña: ni en logs, ni en almacenamiento, ni en analítica. Aun así, generar contraseñas por red es un antipatrón de seguridad: la contraseña viaja y pasa por una máquina ajena. Para uso real, el generador de https://password.es/en/ corre entero en el navegador y no envía nada.",
    "license": {
      "name": "Documentación en password.es",
      "url": "https://password.es/api/"
    }
  },
  "servers": [
    {
      "url": "https://api.password.es",
      "description": "Producción"
    }
  ],
  "paths": {
    "/v1/generate": {
      "post": {
        "summary": "Genera una o varias contraseñas y su análisis exacto",
        "description": "El análisis es exacto, no una estimación: el servidor ha generado la contraseña, así que sabe que es aleatoria. El nivel 0-4 sale del tiempo de crackeo, la misma escala que usa el comprobador de password.es. El campo level_scale lo declara en cada respuesta, para que una divergencia futura se vea en vez de deducirse.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "length": {
                    "type": "integer",
                    "minimum": 4,
                    "maximum": 64,
                    "default": 16
                  },
                  "count": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 20,
                    "default": 1
                  },
                  "lower": {
                    "type": "boolean",
                    "default": true
                  },
                  "upper": {
                    "type": "boolean",
                    "default": true
                  },
                  "digits": {
                    "type": "boolean",
                    "default": true
                  },
                  "symbols": {
                    "type": "boolean",
                    "default": true,
                    "description": "Alfabeto: ~!@#$%^&*()_+-=[]{};:,./<>? (27 caracteres)"
                  },
                  "exclude_ambiguous": {
                    "type": "boolean",
                    "default": false,
                    "description": "Quita 0 O 1 I l | o"
                  },
                  "lang": {
                    "type": "string",
                    "enum": [
                      "en",
                      "es",
                      "nl",
                      "de",
                      "fr",
                      "it",
                      "pt",
                      "ca",
                      "gl",
                      "eu",
                      "pl",
                      "tr",
                      "id",
                      "vi",
                      "ja",
                      "ko",
                      "hi",
                      "ar"
                    ],
                    "description": "En qué idioma se responde. Por defecto inglés. También se acepta como ?lang= en la URL y, si no se pide, se mira Accept-Language. Los mensajes existen en inglés y español; los enlaces, en los dieciocho idiomas del sitio. Un idioma desconocido no es un error: se cae a inglés y _meta.lang lo declara."
                  },
                  "no_repeats": {
                    "type": "boolean",
                    "default": false,
                    "description": "Evita caracteres repetidos adyacentes. No es una garantía absoluta: se intenta diez veces, igual que en la web. A 64 caracteres eso deja pasar un repetido en torno al 0,1 % de las veces."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Contraseñas y análisis",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenerateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Parámetros inválidos",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Límite de peticiones superado",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RateLimited"
                }
              }
            }
          }
        }
      }
    },
    "/v1/check": {
      "post": {
        "summary": "Analiza la fuerza de una contraseña — todavía no implementado",
        "description": "Devuelve 501. Dar los mismos números que el comprobador de la web exige el mismo motor de patrones, y eso cuesta de 11 ms a 3,6 s de CPU por petición según la entrada. Está pendiente de dos decisiones que no son de código: el plan de Cloudflare y un tope de longitud acordado con la web. Mientras tanto, el comprobador de password.es hace exactamente esto en tu navegador, sin enviar nada.",
        "responses": {
          "501": {
            "description": "No implementado todavía",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "summary": "Este documento",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Meta": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "anonymous"
            ],
            "description": "Los demás carriles llegan con las cuentas."
          },
          "limits": {
            "type": "object",
            "description": "Lo que de verdad se aplica ahora mismo.",
            "properties": {
              "burst": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer",
                    "description": "Peticiones por ventana: 60."
                  },
                  "window_seconds": {
                    "type": "integer",
                    "description": "Ventana en segundos: 60."
                  }
                }
              }
            }
          },
          "quota": {
            "type": "object",
            "description": "El hueco de la cuota diaria. Sus dos campos van en null porque todavía no la cuenta nadie: el plan prevé 1000 al día para el carril anónimo, pero el binding de rate limiting de Cloudflare frena ráfagas y no cuenta, así que anunciar ese número sería afirmar un límite que no se hace cumplir. Se rellena cuando existan las cuentas, en D1.",
            "properties": {
              "limit": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "remaining": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "reset": {
                "type": "string",
                "format": "date-time",
                "description": "Medianoche UTC."
              }
            }
          },
          "docs": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Analysis": {
        "type": "object",
        "properties": {
          "length": {
            "type": "integer"
          },
          "pool": {
            "type": "integer",
            "description": "Tamaño del alfabeto."
          },
          "bits": {
            "type": "number",
            "description": "H = L·log2(N), o log2(N)+(L-1)·log2(N-1) con no_repeats."
          },
          "log10_guesses": {
            "type": "number",
            "description": "Trabajo esperado: medio espacio de claves."
          },
          "crack_time_log10_seconds": {
            "type": "number",
            "description": "A 10^12 intentos/s, offline, hash rápido."
          },
          "crack_time": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string"
              },
              "unit": {
                "type": "string"
              }
            }
          },
          "level": {
            "type": "integer",
            "minimum": 0,
            "maximum": 4
          },
          "level_scale": {
            "type": "string",
            "enum": [
              "time"
            ],
            "description": "De dónde sale el nivel: del tiempo de crackeo, la misma escala que usa el comprobador de password.es desde que el sitio unificó las suyas."
          },
          "ceiling": {
            "type": "boolean",
            "description": "False aquí: el número es exacto, no un techo."
          }
        }
      },
      "GenerateResponse": {
        "type": "object",
        "properties": {
          "passwords": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Siempre un array, también con count 1."
          },
          "analysis": {
            "$ref": "#/components/schemas/Analysis"
          },
          "notice": {
            "type": "string"
          },
          "_meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "En prosa y en el idioma de Accept-Language. Es lo que un agente de IA le lee a su usuario."
          },
          "field": {
            "type": "string"
          },
          "docs": {
            "type": "string",
            "format": "uri"
          },
          "_meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "RateLimited": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          },
          {
            "type": "object",
            "properties": {
              "limit": {
                "type": "integer"
              },
              "window_seconds": {
                "type": "integer"
              },
              "_futuros": {
                "type": "string",
                "description": "Cuando existan el registro, los planes y el paquete local, este error llevará además signup_url, pricing_url y local_package. No están todavía, y anunciar una URL que devuelve 404 sería peor que no anunciar ninguna."
              }
            }
          }
        ]
      }
    }
  }
}