{
    "openapi": "3.1.0",
    "info": {
        "title": "Conectar con Sistema Facturación Perú · OAuth 2.0",
        "version": "1.0.0",
        "summary": "Que el cliente de tu sistema, que ya tiene su cuenta en Sistema Facturación Perú, la conecte con un botón.",
        "description": "Servidor OAuth 2.0 con **authorization code + PKCE (S256)**, ambos obligatorios, y `state` obligatorio\n(RFC 6749, RFC 7636, RFC 7009 y RFC 9207 para `iss`). Solo inicia sesión y conecta una cuenta que **ya existe**:\nnunca registra empresas ni usuarios.\n\n1. Registramos tu aplicación (Superadmin › Integraciones › Aplicaciones): `client_id` (`sfp_app_…`),\n   `client_secret` (`sfp_secret_…`, se muestra una vez), direcciones de retorno exactas y permisos que puede pedir.\n2. Tu botón abre una ventana emergente en `GET /conectar`. El usuario escribe su RUC, entra con su usuario de siempre\n   en el subdominio de su empresa y pulsa «Permitir».\n3. Volvemos a tu `redirect_uri` con `code` (un solo uso, 5 minutos), `state` e `iss`.\n4. Tu servidor canjea el código en `POST /oauth/token` y recibe `access_token` (1 hora), `refresh_token` (cambia en cada\n   uso) y la dirección de la API v1 de esa empresa (`empresa.api_url`).\n5. Llamas a la API v1 (su referencia está en el mismo portal) con `Authorization: Bearer sfp_oat_…`.\n\nErrores del token: `{ \"error\": \"...\", \"error_description\": \"...\" }` (RFC 6749 §5.2). Guía: https://sistemafacturacionperu.com/desarrolladores/conectar/\n",
        "contact": {
            "name": "Sistema Facturación Perú",
            "url": "https://sistemafacturacionperu.com/desarrolladores"
        }
    },
    "servers": [
        {
            "url": "https://api.sistemafacturacionperu.com",
            "description": "Servidor de autorización"
        }
    ],
    "tags": [
        {
            "name": "Autorización"
        },
        {
            "name": "Tokens"
        }
    ],
    "paths": {
        "/conectar": {
            "get": {
                "tags": [
                    "Autorización"
                ],
                "summary": "Abre la ventana «Conectar con Sistema Facturación Perú»",
                "operationId": "conectar",
                "description": "Se abre en una ventana emergente (o redirección), nunca en un iframe: responde con `X-Frame-Options: DENY`.\nPrimero se validan `client_id` y `redirect_uri` (coincidencia exacta): si fallan, el error se muestra en la\npantalla y **no** se redirige. Con la dirección ya válida, los demás errores vuelven a tu `redirect_uri` como\n`?error=…&error_description=…&state=…&iss=…`.\n",
                "parameters": [
                    {
                        "name": "response_type",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "enum": [
                                "code"
                            ]
                        }
                    },
                    {
                        "name": "client_id",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "example": "sfp_app_k3j9x0m2q8w1z7c4v6b5n0a1"
                        }
                    },
                    {
                        "name": "redirect_uri",
                        "in": "query",
                        "required": true,
                        "description": "Una de las registradas, idéntica.",
                        "schema": {
                            "type": "string",
                            "format": "uri"
                        }
                    },
                    {
                        "name": "state",
                        "in": "query",
                        "required": true,
                        "description": "Al azar, hasta 500 caracteres. Compruébalo a la vuelta (CSRF).",
                        "schema": {
                            "type": "string",
                            "maxLength": 500
                        }
                    },
                    {
                        "name": "code_challenge",
                        "in": "query",
                        "required": true,
                        "description": "BASE64URL(SHA256(code_verifier)) sin «=», 43 caracteres.",
                        "schema": {
                            "type": "string",
                            "minLength": 43,
                            "maxLength": 43
                        }
                    },
                    {
                        "name": "code_challenge_method",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "enum": [
                                "S256"
                            ]
                        }
                    },
                    {
                        "name": "scope",
                        "in": "query",
                        "description": "Permisos separados por espacios (de los que tu aplicación puede pedir). Sin él, todos los registrados.",
                        "schema": {
                            "type": "string",
                            "example": "comprobantes.emitir comprobantes.leer consultas"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "La pantalla del RUC (HTML)."
                    },
                    "302": {
                        "description": "Vuelta a tu `redirect_uri`. Con permiso: `code`, `state`, `iss`. Si no: `error` (`invalid_request`,\n`unsupported_response_type`, `invalid_scope`, `access_denied`), `error_description`, `state`, `iss`.\n",
                        "headers": {
                            "Location": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "https://tusistema.com/sfp/vuelta?code=sfp_oac_…&state=abc&iss=https%3A%2F%2Fapi.sistemafacturacionperu.com"
                            }
                        }
                    },
                    "400": {
                        "description": "Aplicación o redirect_uri no registradas (en pantalla, sin redirección)."
                    },
                    "403": {
                        "description": "Aplicación suspendida (en pantalla)."
                    }
                }
            }
        },
        "/oauth/token": {
            "post": {
                "tags": [
                    "Tokens"
                ],
                "summary": "Canjea el código o renueva los tokens",
                "operationId": "oauthToken",
                "description": "De servidor a servidor. El cliente se autentica con `Authorization: Basic base64(client_id:client_secret)` **o**\ncon `client_id` y `client_secret` en el cuerpo (no las dos). Límite: 30 por minuto por IP y 120 por `client_id`.\n- `authorization_code`: `code`, `redirect_uri` (la misma) y `code_verifier`. Un código canjeado dos veces\n  revoca todo lo que se emitió con él.\n- `refresh_token`: rotatorio. Guarda siempre el nuevo: si reusas uno ya usado, revocamos toda la cadena.\n",
                "security": [
                    {
                        "clientBasic": []
                    },
                    {}
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "oneOf": [
                                    {
                                        "$ref": "#/components/schemas/PedidoCodigo"
                                    },
                                    {
                                        "$ref": "#/components/schemas/PedidoRenovacion"
                                    }
                                ]
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Tokens y datos de la empresa.",
                        "headers": {
                            "Cache-Control": {
                                "schema": {
                                    "type": "string",
                                    "example": "no-store"
                                }
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/RespuestaToken"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "`invalid_request`, `invalid_grant`, `invalid_scope` o `unsupported_grant_type`.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorOAuth"
                                },
                                "example": {
                                    "error": "invalid_grant",
                                    "error_description": "code_verifier no corresponde al code_challenge (PKCE)."
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "`invalid_client` (con `WWW-Authenticate: Basic` si usaste Basic).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorOAuth"
                                }
                            }
                        }
                    },
                    "429": {
                        "description": "`temporarily_unavailable`, con `Retry-After`.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorOAuth"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/oauth/revoke": {
            "post": {
                "tags": [
                    "Tokens"
                ],
                "summary": "Revoca un token (RFC 7009)",
                "operationId": "oauthRevoke",
                "description": "Un `access_token` deja de servir; un `refresh_token` **desconecta la empresa** (como si pulsara «Desconectar»,\nsin aviso: lo pediste tú). Un token desconocido o de otra aplicación responde 200 igual.\n",
                "security": [
                    {
                        "clientBasic": []
                    },
                    {}
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/x-www-form-urlencoded": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "token"
                                ],
                                "properties": {
                                    "token": {
                                        "type": "string"
                                    },
                                    "token_type_hint": {
                                        "type": "string",
                                        "enum": [
                                            "access_token",
                                            "refresh_token"
                                        ]
                                    },
                                    "client_id": {
                                        "type": "string"
                                    },
                                    "client_secret": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Revocado (o no existía).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Falta `token`.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorOAuth"
                                }
                            }
                        }
                    },
                    "401": {
                        "description": "`invalid_client`.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ErrorOAuth"
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "webhooks": {
        "conexion.revocada": {
            "post": {
                "summary": "La empresa pulsó «Desconectar»",
                "description": "A la URL de webhook de tu aplicación, firmado como los webhooks de la API v1:\n`X-SFP-Signature: t=<unix>,v1=<hex HMAC-SHA256(whsec_…, \"t.cuerpo\")>`, más `X-SFP-Event` y `X-SFP-Delivery`.\nResponde 2xx; reintentamos a 1 m, 5 m, 30 m, 2 h, 6 h y 24 h. Sus tokens ya no sirven.\n",
                "requestBody": {
                    "content": {
                        "application/json": {
                            "example": {
                                "id": "7b0e3c8a-5d1f-4f0e-9a51-2a8c7c9e1f30",
                                "evento": "conexion.revocada",
                                "creado": "2026-12-26T10:15:00-05:00",
                                "data": {
                                    "conexion_id": 42,
                                    "empresa": {
                                        "ruc": "20601030013",
                                        "razon_social": "EMPRESA CONECTADA S.A.C."
                                    },
                                    "motivo": "empresa",
                                    "revocada": "2026-12-26T10:15:00-05:00"
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "Recibido"
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "clientBasic": {
                "type": "http",
                "scheme": "basic",
                "description": "client_id y client_secret (cada uno con application/x-www-form-urlencoded, RFC 6749 §2.3.1)."
            }
        },
        "schemas": {
            "PedidoCodigo": {
                "type": "object",
                "required": [
                    "grant_type",
                    "code",
                    "redirect_uri",
                    "code_verifier"
                ],
                "properties": {
                    "grant_type": {
                        "type": "string",
                        "const": "authorization_code"
                    },
                    "code": {
                        "type": "string",
                        "example": "sfp_oac_Qm3x…"
                    },
                    "redirect_uri": {
                        "type": "string",
                        "format": "uri"
                    },
                    "code_verifier": {
                        "type": "string",
                        "minLength": 43,
                        "maxLength": 128
                    },
                    "client_id": {
                        "type": "string"
                    },
                    "client_secret": {
                        "type": "string"
                    }
                }
            },
            "PedidoRenovacion": {
                "type": "object",
                "required": [
                    "grant_type",
                    "refresh_token"
                ],
                "properties": {
                    "grant_type": {
                        "type": "string",
                        "const": "refresh_token"
                    },
                    "refresh_token": {
                        "type": "string",
                        "example": "sfp_ort_Lk2…"
                    },
                    "scope": {
                        "type": "string",
                        "description": "Opcional: solo permisos ya concedidos."
                    },
                    "client_id": {
                        "type": "string"
                    },
                    "client_secret": {
                        "type": "string"
                    }
                }
            },
            "RespuestaToken": {
                "type": "object",
                "properties": {
                    "access_token": {
                        "type": "string",
                        "example": "sfp_oat_9fK2…"
                    },
                    "token_type": {
                        "type": "string",
                        "const": "Bearer"
                    },
                    "expires_in": {
                        "type": "integer",
                        "example": 3600
                    },
                    "refresh_token": {
                        "type": "string",
                        "example": "sfp_ort_Lk2…"
                    },
                    "scope": {
                        "type": "string",
                        "description": "Los permisos que sirven HOY con el plan de la empresa (en el Básico, solo los de emitir).",
                        "example": "comprobantes.emitir comprobantes.leer"
                    },
                    "conexion_id": {
                        "type": "integer",
                        "example": 42
                    },
                    "empresa": {
                        "type": "object",
                        "properties": {
                            "ruc": {
                                "type": "string",
                                "example": "20601030013"
                            },
                            "razon_social": {
                                "type": "string",
                                "example": "EMPRESA CONECTADA S.A.C."
                            },
                            "api_url": {
                                "type": "string",
                                "format": "uri",
                                "example": "https://miempresa.sistemafacturacionperu.com/api/v1"
                            }
                        }
                    }
                }
            },
            "ErrorOAuth": {
                "type": "object",
                "required": [
                    "error"
                ],
                "properties": {
                    "error": {
                        "type": "string",
                        "enum": [
                            "invalid_request",
                            "invalid_client",
                            "invalid_grant",
                            "invalid_scope",
                            "unsupported_grant_type",
                            "temporarily_unavailable",
                            "server_error"
                        ]
                    },
                    "error_description": {
                        "type": "string"
                    }
                }
            }
        }
    }
}
