{
  "openapi": "3.1.0",
  "info": {
    "title": "API Asender",
    "version": "2026-08-28",
    "summary": "A API pública da plataforma Asender.",
    "description": "Documentação completa em https://docs.asender.net. Para agentes: o contexto inteiro em um arquivo está em https://docs.asender.net/llms-full.txt."
  },
  "servers": [
    {
      "url": "https://api.asender.net",
      "description": "API pública (BFF)"
    },
    {
      "url": "https://auth.asender.net",
      "description": "Identidade (OIDC / OAuth 2.1)"
    }
  ],
  "tags": [
    {
      "name": "api",
      "description": "A superfície que integradores chamam. Autenticação por Bearer, tudo escopado por tenant."
    },
    {
      "name": "auth",
      "description": "Authorization Server. É daqui que sai o token que a API pública exige."
    }
  ],
  "paths": {
    "/": {
      "get": {
        "operationId": "AuthRoot",
        "x-asender-authz": "public",
        "tags": [
          "meta"
        ],
        "summary": "Identidade do serviço.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "service": {
                      "type": "string",
                      "const": "asender-auth"
                    },
                    "version": {
                      "type": "string"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "responde `{service, version}` em `GET /`.\n\nOnde é usada: rota raiz.\n\nEfeitos: escreve a resposta. Serve para saber QUAL binário está rodando quando o comportamento diverge do esperado — a primeira pergunta de todo diagnóstico de deploy.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.handleRoot",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/healthz": {
      "get": {
        "operationId": "AuthHealthz",
        "x-asender-authz": "public",
        "tags": [
          "meta"
        ],
        "summary": "Liveness.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "description": "`200 {\"status\":\"ok\"}`, sem tocar em dependência.\n\nOnde é usada: liveness probe.\n\nEfeitos: escreve a resposta. Não consulta o banco de propósito: liveness que falha por causa do Postgres faz o orquestrador REINICIAR um serviço saudável durante uma instabilidade do banco — e reiniciar o serviço de login em massa transforma degradação em queda de autenticação para todo mundo.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.handleHealth",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/version": {
      "get": {
        "operationId": "AuthVersion",
        "x-asender-authz": "public",
        "tags": [
          "meta"
        ],
        "summary": "Versão e commit do binário em execução.",
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "description": "nome do serviço, versão e commit.\n\nOnde é usada: rota pública, usada por ops e por quem investiga incidente. Pública porque a primeira pergunta de todo incidente é \"que versão está no ar?\", e ela tem de ser respondível sem credencial — quem investiga muitas vezes ainda não tem uma. O que sai é só isso: nunca configuração, nunca endereço interno.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.handleVersion",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/readyz": {
      "get": {
        "operationId": "AuthReadyz",
        "x-asender-authz": "public",
        "tags": [
          "meta"
        ],
        "summary": "Readiness — ping no Postgres.",
        "security": [],
        "responses": {
          "200": {
            "description": "Banco respondeu.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ready"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "`NotReady` — o erro do ping vai no `Message`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "faz ping no banco com prazo de 2s; 503 `NotReady` quando falha.\n\nOnde é usada: readiness probe.\n\nEfeitos: uma consulta trivial ao Postgres. É aqui que a dependência entra — o oposto do `/healthz`. O prazo curto é deliberado: uma sonda que espera indefinidamente nunca reporta \"não pronto\", e a instância continua recebendo login que vai falhar.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.handleReady",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/metrics": {
      "get": {
        "operationId": "AuthMetrics",
        "x-asender-authz": "public",
        "tags": [
          "meta"
        ],
        "summary": "Métricas Prometheus.",
        "x-asender-divergence": "STUB. Só um comentário. Sem OpenTelemetry.",
        "security": [],
        "responses": {
          "200": {
            "description": "Texto.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.tel.MetricsHandler",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/emails/send": {
      "post": {
        "operationId": "PublicSendEmail",
        "x-asender-authz": "apikey",
        "tags": [
          "public-email"
        ],
        "summary": "Enfileira um email (payload compatível com SES SendEmail).",
        "description": "Persiste no `asender_messages` (mensagem + outbox no MESMO commit) e o\npublisher de outbox entrega ao NATS. **Não publica direto no broker** —\nseria dual-write.\n\nCc e Bcc entram como destinatários independentes: o serviço materializa\n**uma mensagem por destinatário**, que é o comportamento correto de\ncópia oculta.\n",
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendEmailRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Enqueued"
          },
          "400": {
            "$ref": "#/components/responses/ValidationDetails"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TenantSuspended"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "emailH.Send",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/emails/batch": {
      "post": {
        "operationId": "PublicBatchEmail",
        "x-asender-authz": "apikey",
        "tags": [
          "public-email"
        ],
        "summary": "Até 500 emails independentes numa chamada.",
        "description": "**Resultado parcial é legítimo e explícito.** Falha de um item não\nderruba os outros: cada entrada de `Results` traz `MessageId` ou\n`Error`, sempre com o `Index` de origem.\n",
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "Messages"
                ],
                "properties": {
                  "Messages": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 500,
                    "items": {
                      "$ref": "#/components/schemas/SendEmailRequest"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Lote processado (com ou sem itens rejeitados).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Count": {
                          "type": "integer"
                        },
                        "Results": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/BatchResult"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TenantSuspended"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "emailH.Batch",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/emails": {
      "get": {
        "operationId": "PublicListEmails",
        "x-asender-authz": "apikey",
        "tags": [
          "public-email"
        ],
        "summary": "Histórico de emails do tenant da API key.",
        "description": "`channel` é FIXO em `email` nesta rota — mandar `?channel=sms` é **422**\n(parâmetro fora da allowlist), não filtro silencioso.\n",
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/MessageStatus"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Busca. `%` e `_` são escapados no repositório.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Página de mensagens.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessagePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TenantSuspended"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "emailH.List",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/emails/{id}": {
      "get": {
        "operationId": "PublicGetEmail",
        "x-asender-authz": "apikey",
        "tags": [
          "public-email"
        ],
        "summary": "Um email com corpo, metadata e timeline de eventos.",
        "description": "Mensagem de outro tenant → **404** (não confirma existência). Mensagem\ndeste tenant em outro canal também → 404: nesta rota só email existe.\n",
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/MessageId"
          }
        ],
        "responses": {
          "200": {
            "description": "Detalhe.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageDetail"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TenantSuspended"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "emailH.Get",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/sms/send": {
      "post": {
        "operationId": "PublicSendSMS",
        "x-asender-authz": "apikey",
        "tags": [
          "public-sms"
        ],
        "summary": "Enfileira um SMS.",
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "To",
                  "Body"
                ],
                "properties": {
                  "From": {
                    "type": "string",
                    "description": "E.164, opcional."
                  },
                  "To": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string",
                      "description": "E.164"
                    }
                  },
                  "Body": {
                    "type": "string",
                    "maxLength": 1600
                  },
                  "IdempotencyKey": {
                    "type": "string"
                  },
                  "Tags": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Enqueued"
          },
          "400": {
            "$ref": "#/components/responses/ValidationDetails"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TenantSuspended"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "description": "valida o payload SES-shaped e persiste o envio no asender_messages, que enfileira para o worker de email.\n\nOnde é usada: API pública do cliente (autenticada por API key). Fluxo do dado: API key → APIKeyAuth → Principal.TenantID → POST /v1/messages (header X-Asender-Tenant) → messages + outbox no mesmo commit → NATS → worker.\n\nSaídas: 202 com `{MessageId, MessageIds, Status, ReusedIdempotency}`; 400 em validação local; 422 quando o asender_messages recusa; 502 se ele está fora.\n\nEfeitos: escrita no serviço de mensageria. Idempotente por IdempotencyKey.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "smsH.Send",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/push/send": {
      "post": {
        "operationId": "PublicSendPush",
        "x-asender-authz": "apikey",
        "tags": [
          "public-push"
        ],
        "summary": "Enfileira um push para tokens ou para um tópico.",
        "description": "Envio só por tópico entra como destinatário sintético `topic:<nome>` —\no `asender_messages` exige ao menos um destinatário e não modela tópico.\n",
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKeyHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "Notification"
                ],
                "properties": {
                  "DeviceTokens": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "Topic": {
                    "type": "string"
                  },
                  "Notification": {
                    "type": "object",
                    "required": [
                      "Title",
                      "Body"
                    ],
                    "properties": {
                      "Title": {
                        "type": "string"
                      },
                      "Body": {
                        "type": "string"
                      }
                    }
                  },
                  "Data": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "IdempotencyKey": {
                    "type": "string"
                  },
                  "Tags": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "$ref": "#/components/responses/Enqueued"
          },
          "400": {
            "$ref": "#/components/responses/ValidationDetails"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TenantSuspended"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "pushH.Send",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/push/devices": {
      "post": {
        "operationId": "PublicRegisterDevice",
        "x-asender-authz": "apikey",
        "tags": [
          "public-push"
        ],
        "summary": "Registra (upsert por token) um device de push.",
        "description": "Idempotente pelo par (tenant, token). Responde **200** sempre, mesmo\nquando o `asender_messages` respondeu 201 no primeiro registro.\n",
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "Token",
                  "Platform"
                ],
                "properties": {
                  "Token": {
                    "type": "string"
                  },
                  "Platform": {
                    "type": "string",
                    "enum": [
                      "ios",
                      "android",
                      "web"
                    ]
                  },
                  "UserRef": {
                    "type": "string",
                    "description": "ACEITO E NÃO PERSISTIDO — sem campo no contrato interno; a perda é logada (Warn), não silenciosa."
                  },
                  "Metadata": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "ACEITO E NÃO PERSISTIDO — idem UserRef."
                  }
                }
              }
            }
          }
        },
        "x-asender-divergence": "`UserRef` e `Metadata` são aceitos e descartados (logados em Warn). O\ncontrato de devices do asender_messages não tem esses campos.\n",
        "responses": {
          "200": {
            "description": "Device registrado ou reativado.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Device": {
                          "$ref": "#/components/schemas/Device"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationDetails"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TenantSuspended"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "pushH.RegisterDevice",
        "x-asender-fonte": "spec+codigo"
      },
      "get": {
        "operationId": "PublicListDevices",
        "x-asender-authz": "apikey",
        "tags": [
          "public-push"
        ],
        "summary": "Lista os devices de push da conta da API key.",
        "description": "Contraparte de leitura do registro: quem envia token precisa poder\nconferir o que está registrado. O tenant vem da API key, NUNCA da query —\naceitar `tenant_id` do cliente aqui seria vazamento cross-tenant.\n",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Página de devices, em PascalCase.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "Devices",
                    "NextCursor",
                    "HasMore"
                  ],
                  "properties": {
                    "Devices": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "NextCursor": {
                      "type": "integer"
                    },
                    "HasMore": {
                      "type": "boolean"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "pushH.ListDevices",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/account": {
      "get": {
        "operationId": "PublicGetAccount",
        "x-asender-authz": "apikey",
        "tags": [
          "public-account"
        ],
        "summary": "Conta da API key usada, mais o contexto da própria chave.",
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "TenantId": {
                          "type": "string",
                          "examples": [
                            "acc_demo_sp"
                          ]
                        },
                        "Name": {
                          "type": "string"
                        },
                        "Plan": {
                          "type": "string",
                          "description": "Vem vazio hoje: o core não expõe plano no payload de tenant."
                        },
                        "Status": {
                          "type": "string",
                          "enum": [
                            "active"
                          ]
                        },
                        "ApiKeyId": {
                          "type": "string"
                        },
                        "ApiKeyScope": {
                          "type": "string",
                          "description": "Escopos da chave unidos por vírgula (`emails:send,emails:read`)."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TenantSuspended"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "accountH.Get",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/account/usage": {
      "get": {
        "operationId": "PublicGetUsage",
        "x-asender-authz": "apikey",
        "tags": [
          "public-account"
        ],
        "summary": "Contadores de consumo do tenant no período corrente.",
        "security": [
          {
            "ApiKeyBearer": []
          }
        ],
        "x-asender-divergence": "**Histórico — CORRIGIDO durante esta rodada, mantido por rastreabilidade.**\nO `asender-core` respondia `404 NotFound` quando o tenant não tinha\nlinha em `core.usage_counters` (o caso de TODO tenant do seed), e este\nhandler — que só trata 404 em `GET /v1/account`, não aqui — traduzia\nisso em **502 \"Could not load usage.\"**, como se o core estivesse fora.\nO core passou a devolver contadores ZERADOS nesse caso, e a rota\nresponde 200. O `502` abaixo continua documentado porque segue sendo a\nresposta quando o core está de fato indisponível.\n",
        "responses": {
          "200": {
            "description": "Consumo do período. Tenant sem nenhum envio devolve os contadores em\nzero, com `UpdatedAt: null` — ausência de linha é consumo zero, não\nausência de recurso.\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Usage": {
                          "$ref": "#/components/schemas/Usage"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TenantSuspended"
          },
          "429": {
            "$ref": "#/components/responses/Throttled"
          },
          "502": {
            "description": "Core fora **ou** tenant sem contador de uso (ver `x-asender-divergence`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "description": "contadores de consumo do tenant da API key (mês, enviados, quota).\n\nOnde é usada: superfície pública `/v1`, consumida por integrações de cliente. Fluxo do dado: API key verificada → Principal.TenantID → asender-core GET /v1/tenants/{id}/usage (snake_case) → PascalMap → resposta pública.\n\nSaídas: 200 com `{Usage:{Period,EmailsSent,SmsSent,PushSent,ApiCalls}}`; 404 se o tenant da API key não existe mais no core; 502 se o core está fora. DEFEITO QUE ISTO TAMBÉM FECHA (item 7 do briefing Y2, metade da borda): o core respondia 404 quando não havia linha em `core.usage_counters` — e o seed não cria nenhuma —, e este handler traduzia QUALQUER erro do upstream em 502. Ou seja: a rota respondia \"serviço indisponível\" para todo tenant que ainda não enviou nada, mandando o alerta para o time errado. O 404 do core agora só significa \"tenant inexistente\" (a correção principal está em asender-core/internal/service/usage.Get) e é traduzido como 404, não 502. DEFEITO QUE ISTO FECHA (T3 §3.6): o payload do core saía CRU sob `Usage` (`{\"Usage\":{\"sent\":42}}`), fora do contrato PascalCase da API pública (API_DESIGN.md §51). O cliente que segue o contrato lê `Usage.Sent` e recebe `undefined` — mesma classe de divergência de fronteira do GET /api/tenants.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "accountH.Usage",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/login": {
      "post": {
        "operationId": "BffLogin",
        "x-asender-authz": "public",
        "tags": [
          "bff-auth"
        ],
        "summary": "Autentica e devolve o token de sessão.",
        "description": "Corpo aceito em snake_case (`{\"email\",\"password\"}`) porque é o que o\n`asender-auth` define e o proxy repassa; a RESPOSTA é PascalCase.\n\nCom 2FA habilitado a resposta é 200 com `Token:\"\"` e\n`TwoFactorRequired:true` — explícito, para o chamador não confundir com\nupstream quebrado.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "examples": [
                      "demo@asender.local"
                    ]
                  },
                  "password": {
                    "type": "string",
                    "format": "password"
                  }
                }
              }
            }
          }
        },
        "x-asender-divergence": "Oráculo de tempo: login de usuário existente leva ~200 ms (bcrypt) e de\ninexistente ~1 ms. Permite enumerar contas (RELATORIO-FINAL §3 item 9).\n",
        "responses": {
          "200": {
            "description": "Sessão criada, ou 2FA pendente.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Token": {
                          "type": "string",
                          "description": "JWT. Vazio quando `TwoFactorRequired`."
                        },
                        "UserId": {
                          "type": "string"
                        },
                        "UserEmail": {
                          "type": "string"
                        },
                        "ExpiresAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "TwoFactorRequired": {
                          "type": "boolean"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "description": "Conta bloqueada ou desabilitada.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.Login",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/register": {
      "post": {
        "operationId": "BffRegister",
        "x-asender-authz": "public",
        "tags": [
          "bff-auth"
        ],
        "summary": "Cria usuário e (best-effort) o tenant raiz dele.",
        "description": "A criação do tenant é best-effort: se falhar, a resposta ainda é 201\ncom `Tenant: null` e um campo `Note` dizendo para repetir via\n`POST /api/tenants`. Falha silenciosa não é opção.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "Email",
                  "Password"
                ],
                "properties": {
                  "Email": {
                    "type": "string",
                    "format": "email"
                  },
                  "Password": {
                    "type": "string",
                    "minLength": 8
                  },
                  "Name": {
                    "type": "string"
                  },
                  "TenantName": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Usuário criado.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "UserId": {
                          "type": "string"
                        },
                        "UserEmail": {
                          "type": "string"
                        },
                        "Tenant": {
                          "oneOf": [
                            {
                              "type": "null"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "Tenant": {
                                  "$ref": "#/components/schemas/Tenant"
                                }
                              }
                            }
                          ]
                        },
                        "Note": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.Register",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/2fa": {
      "post": {
        "operationId": "BffTwoFactorLogin",
        "x-asender-authz": "public",
        "tags": [
          "bff-auth"
        ],
        "summary": "Segunda etapa do login — confirma o código do segundo fator.",
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "description": "troca o código do autenticador pela sessão.\n\nOnde é usada: tela `/2fa`, para onde o login manda quem tem segundo fator.\n\nEfeitos: cria sessão no asender-auth. # Por que esta rota é PÚBLICA Quem chega aqui ainda não tem sessão — é justamente o que ela está tentando obter. Exigir sessão tornaria o 2FA impossível de completar: a armadilha de aplicar a mesma guarda em toda rota \"porque é mais seguro\". O que protege é o par (user_id, código): o `user_id` sozinho não abre nada, e o código vale 30 segundos. Sem esta rota, o login de quem tem 2FA ficava sem passo seguinte — a conta ficava inacessível pelo painel.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.TwoFactor",
        "x-asender-fonte": "spec+codigo"
      },
      "get": {
        "operationId": "BffTwoFactorState",
        "x-asender-authz": "session",
        "tags": [
          "bff-auth"
        ],
        "summary": "Estado do segundo fator do usuário da sessão.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "diz se a conta da sessão tem TOTP CONFIRMADO.\n\nOnde é usada: tela de segurança do painel.\n\nEfeitos: uma leitura no asender-auth. Responde o estado CONFIRMADO, e não \"existe segredo\": um setup interrompido deixa segredo gravado sem confirmação, e mostrar \"2FA ligado\" aí faria a pessoa acreditar numa proteção que o login não exige.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.EstadoDoSegundoFator",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/verify": {
      "post": {
        "operationId": "BffVerifyEmail",
        "x-asender-authz": "public",
        "tags": [
          "bff-auth"
        ],
        "summary": "Confirma o e-mail a partir do token do link.",
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "description": "repassa o token do link ao asender-auth.\n\nOnde é usada: tela `/verify`, com o token da URL.\n\nEfeitos: carimba `email_verified_at` no auth. # Também é PÚBLICA Quem abre o link do e-mail pode estar em outro navegador, ou nem ter sessão. Exigir login para verificar o e-mail cria a dependência circular clássica — e a pessoa que mais precisa verificar é justamente a que ainda não conseguiu entrar.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.Verify",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/logout": {
      "post": {
        "operationId": "BffLogout",
        "x-asender-authz": "bearer-unverified",
        "tags": [
          "bff-auth"
        ],
        "summary": "Revoga a sessão do Bearer apresentado. IDEMPOTENTE.",
        "description": "Revoga no `asender-auth`; a validação de sessão passou a consultar o\nestado, então o token deixa de valer (401 no `/api/auth/me` seguinte).\n\n**Esta rota fica FORA do grupo `SessionAuth`, de propósito** — é o único\ncaso em `/api/*`. Dentro do grupo, o segundo logout (retry, aba\nduplicada, refresh) morria em 401 no middleware, quebrando a\nidempotência de uma operação que descreve um ESTADO desejado (\"estar\nfora\"), não uma transição.\n\nConsequência observável, e é por isso que a classe declarada é\n`bearer-unverified` e não `session`: o handler exige o header\n`Authorization` (sem ele, 401) mas **não valida a sessão**. Qualquer\nstring não vazia como Bearer — inclusive uma API key ou lixo — recebe\n**200**. Não abre nada (revogar um token que não existe é no-op no\n`asender-auth`), mas o 401 desta rota vem do HANDLER, não do middleware:\nquem for auditar a superfície não pode contá-la como autenticada.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revogado, ou já estava — inclusive para um Bearer que nunca foi sessão.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Status": {
                          "type": "string",
                          "const": "ok"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.Logout",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/me": {
      "get": {
        "operationId": "BffMe",
        "x-asender-authz": "session",
        "tags": [
          "bff-auth"
        ],
        "summary": "Principal da sessão.",
        "description": "`TenantId` vem do `asender-auth` e **costuma vir vazio** — a resolução\nreal do tenant é feita por rota (`resolveSessionTenant`), a partir dos\nvínculos no core. Não use este campo como escopo.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "UserId": {
                          "type": "string"
                        },
                        "UserEmail": {
                          "type": "string"
                        },
                        "TenantId": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.Me",
        "x-asender-fonte": "spec+codigo"
      },
      "patch": {
        "operationId": "BffUpdateMe",
        "x-asender-authz": "session",
        "tags": [
          "bff-auth"
        ],
        "summary": "Altera o perfil do usuário da sessão.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "altera nome e/ou e-mail do usuário DA SESSÃO.\n\nOnde é usada: tela de perfil do painel.\n\nEfeitos: escreve no asender-auth; e-mail novo desverifica a conta e dispara a verificação do endereço novo (lá).",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.UpdateMe",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/me/verify/resend": {
      "post": {
        "operationId": "BffResendVerification",
        "x-asender-authz": "session",
        "tags": [
          "bff-auth"
        ],
        "summary": "Reenvia o e-mail de verificação do usuário da sessão.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "pede ao asender-auth um link de verificação novo para o usuário da sessão.\n\nOnde é usada: aviso \"confirme seu e-mail\" do painel.\n\nEfeitos: um e-mail (o guard do auth decide se ele sai fora de prd). O 429 do upstream é REPASSADO como 429: quem pediu demais precisa saber que basta esperar, e um 502 aqui mandaria a pessoa procurar defeito onde não há.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.ResendVerification",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/me/emails": {
      "get": {
        "tags": [
          "api"
        ],
        "summary": "histórico paginado dos emails do tenant. Era 501; agora lê do asender_messages com o canal fixado em `email`.",
        "description": "histórico paginado dos emails do tenant. Era 501; agora lê do asender_messages com o canal fixado em `email`.\n\nOnde é usada: API pública do cliente. Fluxo do dado: API key → Principal.TenantID → GET /v1/messages?channel=email.\n\nEntradas: `status`, `q`, `cursor`, `limit` (allowlist). `channel` do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.\n\nSaídas: 200 com `{Messages, NextCursor, HasMore}`; 422 em query malformada.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "emailsH.List",
        "x-asender-fonte": "codigo"
      },
      "post": {
        "tags": [
          "api"
        ],
        "summary": "Add: POST /api/auth/me/emails.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "emailsH.Add",
        "x-asender-fonte": "codigo"
      }
    },
    "/api/auth/me/emails/{id}": {
      "delete": {
        "tags": [
          "api"
        ],
        "summary": "Remove: DELETE /api/auth/me/emails/{id}.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "emailsH.Remove",
        "x-asender-fonte": "codigo"
      }
    },
    "/api/auth/me/emails/{id}/primary": {
      "post": {
        "tags": [
          "api"
        ],
        "summary": "Primary: POST /api/auth/me/emails/{id}/primary.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "emailsH.Primary",
        "x-asender-fonte": "codigo"
      }
    },
    "/api/auth/me/emails/{id}/verify/resend": {
      "post": {
        "tags": [
          "api"
        ],
        "summary": "Resend: POST /api/auth/me/emails/{id}/verify/resend.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "emailsH.Resend",
        "x-asender-fonte": "codigo"
      }
    },
    "/api/auth/2fa/setup": {
      "post": {
        "operationId": "BffTwoFactorSetup",
        "x-asender-authz": "session",
        "tags": [
          "bff-auth"
        ],
        "summary": "Inicia a ativação do segundo fator.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "começa a ativação e devolve o segredo e a URL do QR.\n\nOnde é usada: tela de segurança.\n\nEfeitos: grava o segredo PENDENTE de confirmação. # A senha é a invariante do piso 17, e ela para AQUI se faltar Para mexer num fator é preciso apresentar um fator diferente dele. O serviço recusa senha vazia, e o gateway recusa antes — não por desconfiança do serviço, mas porque um corpo sem senha é pedido malformado, e mandá-lo adiante gastaria uma viagem para receber a mesma recusa.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.LigarSegundoFator",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/2fa/verify": {
      "post": {
        "operationId": "BffTwoFactorConfirm",
        "x-asender-authz": "session",
        "tags": [
          "bff-auth"
        ],
        "summary": "Confirma a ativação do segundo fator.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "conclui a ativação com um código do aplicativo.\n\nOnde é usada: tela de segurança, depois de ler o QR.\n\nEfeitos: o login passa a EXIGIR o segundo fator.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.ConfirmarSegundoFator",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/2fa/disable": {
      "post": {
        "operationId": "BffTwoFactorDisable",
        "x-asender-authz": "session",
        "tags": [
          "bff-auth"
        ],
        "summary": "Desliga o segundo fator.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "remove o TOTP da conta da sessão.\n\nOnde é usada: tela de segurança.\n\nEfeitos: o login deixa de exigir o segundo fator. Exige a SENHA, e não o código: aceitar o próprio TOTP para removê-lo faria o fator se autorizar sozinho.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.DesligarSegundoFator",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/sessions": {
      "get": {
        "operationId": "BffListSessions",
        "x-asender-authz": "session",
        "tags": [
          "bff-auth"
        ],
        "summary": "Dispositivos e sessões ativas do usuário.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "lista onde a conta do usuário da sessão está aberta.\n\nOnde é usada: tela de segurança do painel.\n\nEfeitos: uma leitura no asender-auth. O token repassado é o da REQUISIÇÃO, e é ele que define de quem é a lista. Não existe parâmetro de usuário: um `?user_id=` seria a rota mais barata do sistema para descobrir de onde outra pessoa se conecta.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.Sessoes",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/auth/sessions/{id}": {
      "delete": {
        "operationId": "BffRevokeSession",
        "x-asender-authz": "session",
        "tags": [
          "bff-auth"
        ],
        "summary": "Revoga uma sessão específica.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "derruba uma sessão do próprio usuário.\n\nOnde é usada: botão \"remover\" da tela de segurança.\n\nEfeitos: a sessão para de autenticar imediatamente. # Por que a impersonação NÃO passa por aqui A sessão de suporte é somente leitura (ADR-0017), e o middleware de impersonação já recusa escrita. Isto é uma escrita — e derrubar o dispositivo de um cliente durante uma sessão de suporte é exatamente o poder que o ADR tira de quem entra na conta alheia.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "authH.RevogarSessao",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants": {
      "get": {
        "operationId": "BffListTenants",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Vínculos de tenant do usuário logado.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Tenants": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Tenant"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "description": "histórico paginado dos emails do tenant. Era 501; agora lê do asender_messages com o canal fixado em `email`.\n\nOnde é usada: API pública do cliente. Fluxo do dado: API key → Principal.TenantID → GET /v1/messages?channel=email.\n\nEntradas: `status`, `q`, `cursor`, `limit` (allowlist). `channel` do cliente é IGNORADO: nesta rota o canal é do contrato, não do parâmetro.\n\nSaídas: 200 com `{Messages, NextCursor, HasMore}`; 422 em query malformada.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "tenantsH.List",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "BffCreateTenant",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Cria um tenant RAIZ pertencente ao usuário logado.",
        "description": "**Allowlist tipada de dois campos.** `parent_id`, `monthly_quota`,\n`status`, `plan`, `owner_user_id` e `id` são IGNORADOS e registrados —\nem nível ERROR, como tentativa de escalonamento de privilégio. O\n`owner_user_id` vem sempre do principal verificado.\n\nIgnorar (em vez de 422) preserva chamadores antigos que mandam\n`owner_user_id`; ignorar aqui **não é engolir**: fica no log com o\nusuário que enviou.\n\nPara pendurar um tenant sob outro use `POST /api/tenants/{id}/children`\n(autoriza o pai) ou `PATCH /api/tenants/{id}/parent` (autoriza os dois\nlados). É o único caminho autorizado para mexer na árvore.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "Name"
                ],
                "properties": {
                  "Name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "Slug": {
                    "type": "string"
                  }
                },
                "additionalProperties": {
                  "description": "Aceito no wire e DESCARTADO; ver descrição."
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Tenant criado. **O objeto vem NO TOPO, sem a chave `Tenant`** — o\n`asender-core` responde o tenant sem chave de recurso nesta rota e o\nBFF só pascaliza o que recebeu. É inconsistente com\n`POST /api/tenants/{id}/children` e com `POST /api/auth/register`,\nque devolvem `{\"Tenant\":{...}}`. Documentado como está no ar.\n",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "$ref": "#/components/schemas/Tenant"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "tenantsH.Create",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants/tree": {
      "get": {
        "operationId": "BffTenantTree",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Subárvore do tenant corrente, em pré-ordem.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "name": "depth",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "TenantId": {
                          "type": "string"
                        },
                        "Tree": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/TenantNode"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "description": "a subárvore do tenant corrente, para o seletor de tenant e a tela de hierarquia.\n\nOnde é usada: dashboard (settings/tenants) e backoffice. Fluxo do dado: sessão → tenant resolvido → asender-core GET /v1/tenants/{id}/tree → PascalCase.\n\nEntradas: `depth` — inteiro positivo; valor não numérico é 422, não \"sem limite\" silencioso.\n\nSaídas: 200 com `{TenantId, Tree:[...]}`; 404 se o tenant pedido não é acessível.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "treeH.Tree",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants/{id}/children": {
      "post": {
        "operationId": "BffCreateChildTenant",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Cria uma sub-conta sob um tenant administrado pelo usuário.",
        "description": "Autoriza `{id}` por vínculo direto **ou herdado** (ser membro de um\nancestral manda na subárvore). `MonthlyQuota` ausente/nulo = herda do\nancestral mais próximo com valor.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "Name"
                ],
                "additionalProperties": false,
                "properties": {
                  "Name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "Slug": {
                    "type": "string"
                  },
                  "MonthlyQuota": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Sub-conta criada.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Tenant": {
                          "$ref": "#/components/schemas/TenantNode"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "treeH.CreateChild",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants/{id}/parent": {
      "patch": {
        "operationId": "BffMoveTenant",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Move o tenant para outro pai (ou para a raiz).",
        "description": "**Autoriza os DOIS lados**: `{id}` e o `ParentId` de destino. Autorizar\nsó a origem deixaria pendurar um tenant na árvore de outro cliente, que\npassaria a vê-lo em rollup de relatório.\n\nCorpo com EXATAMENTE um campo `ParentId`. `null` promove a raiz; campo\nausente é 422 (um `{}` acidental não pode promover uma sub-conta).\nCiclo → 422 (o trigger de closure do core levanta `check_violation`),\nnunca 500.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ParentId"
                ],
                "additionalProperties": false,
                "properties": {
                  "ParentId": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Movido; a closure foi reescrita.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Tenant": {
                          "$ref": "#/components/schemas/TenantNode"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "treeH.MoveParent",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants/{id}/quota": {
      "patch": {
        "operationId": "BffSetTenantQuota",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Define ou limpa a quota mensal do tenant.",
        "description": "Corpo com EXATAMENTE um campo `MonthlyQuota`. `null` volta a herdar do\nancestral mais próximo. Campo ausente é 422 — um nome digitado errado\nnão pode limpar a quota em silêncio.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "MonthlyQuota"
                ],
                "additionalProperties": false,
                "properties": {
                  "MonthlyQuota": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "minimum": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Quota gravada; `EffectiveQuota` já recalculada.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Tenant": {
                          "$ref": "#/components/schemas/TenantNode"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "treeH.SetQuota",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/messages/send": {
      "post": {
        "operationId": "BffSendMessage",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Composer do dashboard — envia em qualquer canal.",
        "description": "Corpo em PascalCase, campo a campo espelhando o contrato interno.\nCampo desconhecido é **400** (`DisallowUnknownFields`), não silêncio.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "Channel",
                  "To"
                ],
                "properties": {
                  "Channel": {
                    "$ref": "#/components/schemas/Channel"
                  },
                  "From": {
                    "type": "string"
                  },
                  "To": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    }
                  },
                  "Subject": {
                    "type": "string"
                  },
                  "BodyText": {
                    "type": "string"
                  },
                  "BodyHtml": {
                    "type": "string"
                  },
                  "Template": {
                    "type": "string",
                    "description": "Slug do template. Variável declarada e não informada é 422."
                  },
                  "Variables": {
                    "type": "object",
                    "additionalProperties": true
                  },
                  "IdempotencyKey": {
                    "type": "string"
                  },
                  "Metadata": {
                    "type": "object",
                    "additionalProperties": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Enfileirado.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Messages": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Message"
                          }
                        },
                        "ReusedIdempotency": {
                          "type": "boolean"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.SendMessage",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/messages": {
      "get": {
        "operationId": "BffListMessages",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Histórico de envios do tenant corrente.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "name": "channel",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Channel"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/MessageStatus"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessagePage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "description": "página de mensagens do tenant, com filtros de canal/status/busca.\n\nOnde é usada: tela de histórico do dashboard.\n\nEntradas: Channel/Status/Q/Cursor/Limit são aceitos em snake_case (`channel`, `status`, `q`, `cursor`, `limit`) e repassados por allowlist — nenhum outro parâmetro atravessa.\n\nSaídas: 200 com `{Messages, NextCursor, HasMore}`; 422 em query malformada.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.ListMessages",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/messages/{id}": {
      "get": {
        "operationId": "BffGetMessage",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Detalhe do envio com timeline.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "$ref": "#/components/parameters/MessageId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MessageDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "description": "uma mensagem com corpo, metadata e a trilha de eventos.\n\nOnde é usada: tela de detalhe do envio.\n\nSaídas: 200 com `{Message, Events}`; 404 quando o id não existe NO TENANT — mensagem de outro tenant também é 404, para não confirmar existência (§22.8).",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.GetMessage",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/templates": {
      "get": {
        "operationId": "BffListTemplates",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Templates do tenant.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Templates": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Template"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "description": "templates do tenant, para a tela de templates e o seletor do composer.\n\nOnde é usada: dashboard.\n\nSaídas: 200 com `{Templates}`.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.ListTemplates",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "BffUpsertTemplate",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Cria ou atualiza um template pelo par (tenant, slug).",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "Slug",
                  "Name",
                  "Channel"
                ],
                "properties": {
                  "Slug": {
                    "type": "string"
                  },
                  "Name": {
                    "type": "string"
                  },
                  "Channel": {
                    "$ref": "#/components/schemas/Channel"
                  },
                  "Subject": {
                    "type": "string"
                  },
                  "BodyHtml": {
                    "type": "string"
                  },
                  "BodyText": {
                    "type": "string"
                  },
                  "Variables": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Gravado (upsert — 200 tanto na criação quanto na atualização).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Template": {
                          "$ref": "#/components/schemas/Template"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "description": "gravação idempotente do template pelo par (tenant, slug).\n\nOnde é usada: editor de templates do dashboard.\n\nSaídas: 200 com `{Template}`; 422 em validação do asender_messages.\n\nEfeitos: escreve em messages.templates.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.UpsertTemplate",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/push/devices": {
      "get": {
        "operationId": "BffListDevices",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Devices de push do tenant, para o seletor da tela de disparo.",
        "description": "Fonte do seletor de dispositivos em `/t/{conta}/send/push`. Antes de\nexistir, o caminho respondia 404 e o seletor ficava vazio mesmo com\ndevices no banco — a tela levava o usuário a concluir que a conta não\ntinha dispositivo nenhum.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Página de devices, em PascalCase.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "Devices",
                    "NextCursor",
                    "HasMore"
                  ],
                  "properties": {
                    "Devices": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "NextCursor": {
                      "type": "integer"
                    },
                    "HasMore": {
                      "type": "boolean"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.ListDevices",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/push/site-config": {
      "get": {
        "operationId": "BffPushSiteConfig",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Pacote de integração de Web Push para o site do cliente.",
        "description": "Devolve, num JSON só, tudo que o site precisa para receber Web Push:\nchave pública VAPID do tenant, `manifest.json`, o service worker e o\nsnippet de inscrição — os três já preenchidos com os valores desta\ninstalação. Gerar no servidor evita o chamado clássico do integrador que\nesqueceu de substituir um `<SUA_CHAVE_AQUI>`.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          }
        ],
        "responses": {
          "200": {
            "description": "Artefatos prontos para o site.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "PublicKey",
                    "ApiBase",
                    "Manifest",
                    "ServiceWorker",
                    "Snippet"
                  ],
                  "properties": {
                    "PublicKey": {
                      "type": "string"
                    },
                    "ApiBase": {
                      "type": "string"
                    },
                    "TenantId": {
                      "type": "string"
                    },
                    "Manifest": {
                      "type": "object"
                    },
                    "ServiceWorker": {
                      "type": "string"
                    },
                    "Snippet": {
                      "type": "string"
                    },
                    "Instructions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "siteCfgH.SiteConfig",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/contacts": {
      "get": {
        "operationId": "BffListContacts",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Audiência do tenant.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "list_id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "$ref": "#/components/schemas/CursorPage"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Contacts": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Contact"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "description": "página de contatos do tenant, com busca e filtro por lista.\n\nOnde é usada: tela de audiência do dashboard.\n\nEntradas: `q`, `list_id`, `cursor`, `limit` (allowlist).\n\nSaídas: 200 com `{Contacts, NextCursor, HasMore}`.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.ListContacts",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "BffCreateContact",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Cria um contato.",
        "description": "`Subscribed` ausente vira `true` — opt-in é o default do cadastro manual.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Criado.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Contact": {
                          "$ref": "#/components/schemas/Contact"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.CreateContact",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/contacts/bulk": {
      "post": {
        "operationId": "BffBulkContacts",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Importa até 5000 contatos, com upsert pela identidade natural.",
        "description": "Import parcial é resultado legítimo: itens ruins voltam em `Errors` com\no índice de origem, e o lote não é invalidado por causa deles. Lote\nvazio ou acima do teto é 422.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "Contacts"
                ],
                "properties": {
                  "Contacts": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 5000,
                    "items": {
                      "$ref": "#/components/schemas/ContactInput"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Lote processado.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Created": {
                          "type": "integer"
                        },
                        "Updated": {
                          "type": "integer"
                        },
                        "Errors": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "Index": {
                                "type": "integer"
                              },
                              "Email": {
                                "type": "string"
                              },
                              "Phone": {
                                "type": "string"
                              },
                              "Error": {
                                "type": "string"
                              }
                            }
                          }
                        },
                        "Contacts": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/Contact"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.BulkContacts",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/lists": {
      "get": {
        "operationId": "BffListLists",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Listas de contatos com contagem de membros.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Lists": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ContactList"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "description": "listas de contatos do tenant, com contagem de membros.\n\nOnde é usada: tela de audiência.\n\nSaídas: 200 com `{Lists}`.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.ListLists",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "BffCreateList",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Cria uma lista de contatos.",
        "description": "O upstream faz upsert por slug; este BFF responde sempre 201.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "Name"
                ],
                "properties": {
                  "Name": {
                    "type": "string",
                    "minLength": 1
                  },
                  "Slug": {
                    "type": "string"
                  },
                  "Description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Criada (ou atualizada pelo slug).",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "List": {
                          "$ref": "#/components/schemas/ContactList"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.CreateList",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/lists/{id}/members": {
      "post": {
        "operationId": "BffAddListMembers",
        "x-asender-authz": "session",
        "tags": [
          "bff-messaging"
        ],
        "summary": "Anexa contatos a uma lista.",
        "description": "`NotFound` traz os ids que não existem no tenant — o servidor diz o que\nNÃO pôde fazer, em vez de descartar em silêncio. Idempotente.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public id da lista (`lst_<hex>`)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "ContactIds"
                ],
                "properties": {
                  "ContactIds": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Added": {
                          "type": "integer"
                        },
                        "NotFound": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Validation"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "msgBFF.AddListMembers",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/reports/overview": {
      "get": {
        "operationId": "BffReportOverview",
        "x-asender-authz": "session",
        "tags": [
          "bff-reports"
        ],
        "summary": "Funil do período com quebra por canal.",
        "description": "Os três canais SEMPRE aparecem em `ByChannel`, zerados quando não houve\ntráfego. As chaves de `ByChannel` (`email`/`sms`/`push`) ficam em\nminúsculas porque são DADO, não nome de campo.\n\nTaxas em `[0,1]`, arredondadas a 4 casas, grampeadas no teto (a base\npode ter `bounced > sent`, porque bounce não exige `sent_at`).\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Totals": {
                          "$ref": "#/components/schemas/ReportCounters"
                        },
                        "Rates": {
                          "$ref": "#/components/schemas/ReportRates"
                        },
                        "ByChannel": {
                          "type": "object",
                          "propertyNames": {
                            "enum": [
                              "email",
                              "sms",
                              "push"
                            ]
                          },
                          "additionalProperties": {
                            "type": "object",
                            "properties": {
                              "Totals": {
                                "$ref": "#/components/schemas/ReportCounters"
                              },
                              "Rates": {
                                "$ref": "#/components/schemas/ReportRates"
                              }
                            }
                          }
                        },
                        "Period": {
                          "$ref": "#/components/schemas/ReportPeriod"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "reportsBFF.Overview",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/reports/timeseries": {
      "get": {
        "operationId": "BffReportTimeseries",
        "x-asender-authz": "session",
        "tags": [
          "bff-reports"
        ],
        "summary": "Série temporal por bucket e canal.",
        "description": "Um ponto por (bucket, canal), buckets vazios inclusos. O campo continua\nse chamando `Day` mesmo com `interval=hour` — é o início do bucket; a\ngranularidade vem em `Period.Interval`.\n\nEste BFF aceita `hour|day|week|month`, mas o `asender_messages` só\nimplementa `day|hour`: `week`/`month` passam a validação daqui e são\nrecusados com 422 pelo upstream.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "name": "channel",
            "in": "query",
            "schema": {
              "$ref": "#/components/schemas/Channel"
            }
          },
          {
            "name": "interval",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "hour",
                "day",
                "week",
                "month"
              ],
              "default": "day"
            },
            "description": "`week` e `month` são aceitos aqui e recusados pelo upstream (422)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Points": {
                          "type": "array",
                          "items": {
                            "allOf": [
                              {
                                "$ref": "#/components/schemas/ReportCounters"
                              },
                              {
                                "type": "object",
                                "properties": {
                                  "Day": {
                                    "type": "string",
                                    "format": "date-time"
                                  },
                                  "Channel": {
                                    "$ref": "#/components/schemas/Channel"
                                  }
                                }
                              }
                            ]
                          }
                        },
                        "Period": {
                          "allOf": [
                            {
                              "$ref": "#/components/schemas/ReportPeriod"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "Interval": {
                                  "type": "string"
                                }
                              }
                            }
                          ]
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "reportsBFF.Timeseries",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/reports/top-templates": {
      "get": {
        "operationId": "BffReportTopTemplates",
        "x-asender-authz": "session",
        "tags": [
          "bff-reports"
        ],
        "summary": "Ranking de templates por volume, com taxas.",
        "description": "`limit` aceito de 1 a 100 nesta borda; o teto REAL aplicado pelo\nrepositório do `asender_messages` é 50.\n",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantHeader"
          },
          {
            "$ref": "#/components/parameters/From"
          },
          {
            "$ref": "#/components/parameters/To"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/RequestIdEnvelope"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "Templates": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "Slug": {
                                "type": "string"
                              },
                              "Name": {
                                "type": "string"
                              },
                              "Total": {
                                "type": "integer"
                              },
                              "Sent": {
                                "type": "integer"
                              },
                              "Delivered": {
                                "type": "integer"
                              },
                              "Opened": {
                                "type": "integer"
                              },
                              "Clicked": {
                                "type": "integer"
                              },
                              "DeliveryRate": {
                                "type": "number"
                              },
                              "OpenRate": {
                                "type": "number"
                              },
                              "ClickRate": {
                                "type": "number"
                              }
                            }
                          }
                        },
                        "Period": {
                          "$ref": "#/components/schemas/ReportPeriod"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/NoTenant"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrDenied"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "reportsBFF.TopTemplates",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants/{id}/api-keys": {
      "get": {
        "tags": [
          "api"
        ],
        "summary": "ListKeys lista as chaves da conta.",
        "description": "Onde é usada: GET /api/tenants/{id}/api-keys.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "apiKeysH.ListKeys",
        "x-asender-fonte": "codigo"
      },
      "post": {
        "tags": [
          "api"
        ],
        "summary": "CreateKey emite uma chave. O texto puro volta UMA vez, no corpo do core.",
        "description": "Onde é usada: POST /api/tenants/{id}/api-keys.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "apiKeysH.CreateKey",
        "x-asender-fonte": "codigo"
      }
    },
    "/api/tenants/{id}/api-keys/{keyId}": {
      "delete": {
        "tags": [
          "api"
        ],
        "summary": "RevokeKey revoga uma chave.",
        "description": "Onde é usada: DELETE /api/tenants/{id}/api-keys/{keyId}.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "apiKeysH.RevokeKey",
        "x-asender-fonte": "codigo"
      }
    },
    "/api/tenants/{id}/members": {
      "get": {
        "operationId": "BffListMembers",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Membros da conta.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "quem tem acesso à conta.\n\nOnde é usada: tela de equipe.\n\nEfeitos: uma leitura no core.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "membersH.ListMembers",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "BffAddMember",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Vincula um usuário à conta.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "dá acesso direto a um usuário que já existe.\n\nOnde é usada: tela de equipe.\n\nEfeitos: uma escrita no core.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "membersH.AddMember",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants/{id}/members/{userId}": {
      "delete": {
        "operationId": "BffRemoveMember",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Desvincula um usuário da conta.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "tira o acesso de alguém.\n\nOnde é usada: tela de equipe.\n\nEfeitos: uma escrita no core.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "membersH.RemoveMember",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants/{id}/invitations": {
      "get": {
        "operationId": "BffListInvitations",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Convites pendentes da conta.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "membersH.ListInvitations",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "BffCreateInvitation",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Convida alguém para a conta.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "convida um e-mail para a conta.\n\nOnde é usada: tela de equipe.\n\nEfeitos: uma escrita no core (e, quando houver envio, um e-mail).",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "membersH.CreateInvitation",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants/{id}/invitations/{token}": {
      "delete": {
        "operationId": "BffRevokeInvitation",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Cancela um convite pendente.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          },
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "cancela um convite que ainda não foi aceito.\n\nOnde é usada: tela de equipe.\n\nEfeitos: uma escrita no core.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "membersH.RevokeInvitation",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/invitations/accept": {
      "post": {
        "operationId": "BffAcceptInvitation",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Aceita um convite e vincula o usuário à conta.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "transforma o convite em associação, para quem está logado.\n\nOnde é usada: tela de convite.\n\nEfeitos: uma escrita no core. # Esta rota NÃO passa por contaAutorizada E não pode: quem aceita ainda NÃO pertence à conta — exigir pertinência aqui tornaria o convite impossível de aceitar. Quem autoriza é o TOKEN, que o core valida (existente, não expirado, não usado), e o usuário vem da sessão.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "membersH.AcceptInvitation",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/tenants": {
      "get": {
        "operationId": "PlatformListTenants",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Lista as contas, na visão da plataforma.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "todas as contas, para o suporte achar a do chamado.\n\nOnde é usada: tela `/impersonate`.\n\nEfeitos: uma leitura no core.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.ListarContas",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/tenants/{id}/enter": {
      "post": {
        "operationId": "PlatformEnterTenant",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Abre sessão de suporte na conta (somente leitura, ADR-0017).",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "abre a entrada de suporte e grava o cookie com o ID dela.\n\nOnde é usada: tela `/impersonate`.\n\nEfeitos: uma escrita no core — e, a partir dela, LEITURA da conta alheia. O motivo é obrigatório: uma trilha sem motivo responde \"alguém entrou\", que é a metade inútil da pergunta que ela existe para responder.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.Entrar",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/impersonation/end": {
      "post": {
        "operationId": "PlatformEndImpersonation",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Encerra a sessão de suporte em andamento.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "encerra a entrada em curso e apaga o cookie.\n\nOnde é usada: botão \"sair da conta\".\n\nEfeitos: a sessão de suporte para NA HORA.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.Sair",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/tenants/{id}/impersonations": {
      "get": {
        "operationId": "BffListImpersonations",
        "x-asender-authz": "session",
        "tags": [
          "bff-tenants"
        ],
        "summary": "Trilha de sessões de suporte na conta.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/TenantPathId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "quem entrou naquela conta, quando e por quê.\n\nOnde é usada: tela de segurança da conta — do CLIENTE.\n\nEfeitos: uma leitura no core. # Esta rota NÃO exige plataforma Ela exige PERTINÊNCIA, como qualquer leitura de conta. Uma trilha que só a plataforma consegue ler serve para a plataforma se defender, não para o cliente se proteger (ADR-0017).",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.Trilha",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/me": {
      "get": {
        "operationId": "PlatformWhoAmI",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Diz se o usuário da sessão é da plataforma.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "responde quem é o operador e confirma que ele é da plataforma.\n\nOnde é usada: o console, ao abrir — é o que decide entre mostrar o console e mostrar \"não disponível\".\n\nEfeitos: uma leitura na allowlist. Existe em vez de o console deduzir de outra rota: sem ela, o front descobriria que não é plataforma pelo 404 da PRIMEIRA rota que chamasse — e mostraria um erro de carregamento onde a resposta certa é \"esta área não é sua\".",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.EuNaPlataforma",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/tenants/{id}": {
      "get": {
        "operationId": "PlatformGetTenant",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Detalhe de uma conta, na visão da plataforma.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "a conta inteira, vista pela plataforma.\n\nOnde é usada: console, ao abrir um cliente.\n\nEfeitos: uma leitura no core. # Por que não reusar `GET /api/tenants/{id}` Aquela rota exige PERTINÊNCIA: o usuário tem de ser membro da conta. Quem opera a plataforma não é membro de nenhuma conta de cliente — e não deve virar, porque virar membro para poder ver é exatamente o atalho que o ADR-0017 existe para impedir. A autorização aqui é a allowlist de plataforma, e a rota é outra.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.ContaDaPlataforma",
        "x-asender-fonte": "spec+codigo"
      },
      "patch": {
        "operationId": "PlatformPatchTenant",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Altera uma conta, na visão da plataforma.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "muda status e metadados da conta, pela plataforma.\n\nOnde é usada: console.\n\nEfeitos: uma escrita no core. # Isto NÃO é a sessão de suporte A impersonação é somente leitura (ADR-0017) porque entrar na conta é ver o que o cliente vê. Isto é outra coisa: é a plataforma agindo COMO plataforma — suspender uma conta abusiva, por exemplo — e a ação fica no log com o ator. A distinção importa: se a sessão de suporte pudesse escrever, \"entrar para ajudar\" viraria \"entrar para consertar\", e o limite que o ADR desenhou desapareceria na prática.",
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.AtualizarContaDaPlataforma",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/metrics": {
      "get": {
        "operationId": "PlatformMetrics",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Métricas agregadas da plataforma.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.MetricasDaPlataforma",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/blacklist": {
      "get": {
        "operationId": "PlatformListBlacklist",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Lista os bloqueios da plataforma.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.ListarBlacklist",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "PlatformAddBlacklist",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Acrescenta uma entrada à lista de bloqueio.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.Bloquear",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/blacklist/{id}": {
      "delete": {
        "operationId": "PlatformRemoveBlacklist",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Remove uma entrada da lista de bloqueio.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.Desbloquear",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/blacklist/suggestions": {
      "get": {
        "operationId": "PlatformListBlacklistSuggestions",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Sugestões de bloqueio ainda não decididas.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.ListarSugestoes",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "PlatformCreateBlacklistSuggestion",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Registra uma sugestão de bloqueio.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.Sugerir",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/blacklist/suggestions/{id}/apply": {
      "post": {
        "operationId": "PlatformApplyBlacklistSuggestion",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Aceita a sugestão e a promove a bloqueio.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.AplicarSugestao",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/blacklist/suggestions/{id}/dismiss": {
      "post": {
        "operationId": "PlatformDismissBlacklistSuggestion",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Descarta a sugestão.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.DescartarSugestao",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/datacenter-asn": {
      "get": {
        "operationId": "PlatformListDatacenterASN",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "ASNs classificados como datacenter.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.ListarASNs",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "PlatformAddDatacenterASN",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Classifica um ASN como datacenter.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.AcrescentarASN",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/datacenter-asn/{asn}": {
      "delete": {
        "operationId": "PlatformRemoveDatacenterASN",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Remove a classificação de um ASN.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "asn",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.RemoverASN",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/platform-alerts": {
      "get": {
        "operationId": "PlatformListAlerts",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Regras de alerta da plataforma.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.ListarAlertasDePlataforma",
        "x-asender-fonte": "spec+codigo"
      },
      "post": {
        "operationId": "PlatformCreateAlert",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Cria uma regra de alerta de plataforma.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.CriarAlertaDePlataforma",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/platform-alerts/{id}": {
      "put": {
        "operationId": "PlatformUpdateAlert",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Substitui uma regra de alerta.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.AtualizarAlertaDePlataforma",
        "x-asender-fonte": "spec+codigo"
      },
      "delete": {
        "operationId": "PlatformDeleteAlert",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Remove uma regra de alerta.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.ApagarAlertaDePlataforma",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/api/admin/platform-alerts/events": {
      "get": {
        "operationId": "PlatformListAlertEvents",
        "x-asender-authz": "session",
        "tags": [
          "bff-plataforma"
        ],
        "summary": "Disparos de alerta da plataforma.",
        "security": [
          {
            "SessionBearer": []
          },
          {
            "SessionCookie": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "x-asender-servico": "asender-api",
        "x-asender-handler": "plataformaH.EventosDePlataforma",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/.well-known/openid-configuration": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "anuncia os endpoints e as capacidades (só `code`, só S256, só RS256 — o perfil OAuth2.1 da casa).",
        "description": "anuncia os endpoints e as capacidades (só `code`, só S256, só RS256 — o perfil OAuth2.1 da casa).\n\nOnde é usada: os clients leem isto no bootstrap para se autoconfigurar. Cacheável (muda só em deploy).",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.oidcH.Discovery",
        "x-asender-fonte": "codigo"
      }
    },
    "/.well-known/jwks.json": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "JWKS serve /.well-known/jwks.json — a(s) chave(s) pública(s) RS256. Os clients",
        "description": "Onde é usada: verificação no client.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.oidcH.JWKS",
        "x-asender-fonte": "codigo"
      }
    },
    "/authorize": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "Authorize implementa GET /authorize (OAuth2.1 authorization code + PKCE).",
        "description": "Onde é usada: o browser é redirecionado para cá pelo client (app) que quer logar.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.oidcH.Authorize",
        "x-asender-fonte": "codigo"
      }
    },
    "/token": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Token implementa POST /token. Despacha por grant_type.",
        "description": "Onde é usada: o client troca aqui, servidor-a-servidor (ou pelo BFF), o code/refresh por tokens.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.oidcH.Token",
        "x-asender-fonte": "codigo"
      }
    },
    "/userinfo": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "valida o Bearer (RS256, iss, exp) e devolve sub/email/name.",
        "description": "valida o Bearer (RS256, iss, exp) e devolve sub/email/name.\n\nOnde é usada: o client chama para hidratar o perfil. Sem token válido -> 401 invalid_token.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.oidcH.UserInfo",
        "x-asender-fonte": "codigo"
      }
    },
    "/logout": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "Expects Authorization: Bearer <token>.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.oidcH.Logout",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/users": {
      "post": {
        "operationId": "AuthRegisterUser",
        "x-asender-authz": "service",
        "tags": [
          "users"
        ],
        "summary": "Cria um usuário.",
        "description": "A rota é declarada como `r.Post(\"/\")` dentro de `r.Route(\"/users\")`, ou\nseja o padrão registrado é `/v1/users/`. **Ambas as formas respondem\n201** (verificado na stack): `POST /v1/users` e `POST /v1/users/`.\n\nSenha é hasheada com bcrypt no custo configurado (`BCRYPT_COST`, ≥ 12).\nO hash NUNCA aparece em resposta alguma.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "name": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string",
                    "format": "password"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Criado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ServiceUnauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.usersH.Register",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/users/{id}": {
      "get": {
        "operationId": "AuthGetUser",
        "x-asender-authz": "service",
        "tags": [
          "users"
        ],
        "summary": "Um usuário pelo public id.",
        "parameters": [
          {
            "$ref": "#/components/parameters/UserId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/ServiceUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.usersH.Get",
        "x-asender-fonte": "spec+codigo"
      },
      "patch": {
        "operationId": "AuthPatchUser",
        "x-asender-authz": "service",
        "tags": [
          "users"
        ],
        "summary": "Altera nome e/ou email.",
        "description": "Senha NÃO é alterável por aqui — o caminho é `POST /v1/auth/password-reset/confirm`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/UserId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "$ref": "#/components/schemas/User"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ServiceUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.usersH.Patch",
        "x-asender-fonte": "spec+codigo"
      },
      "delete": {
        "operationId": "AuthDeleteUser",
        "x-asender-authz": "service",
        "tags": [
          "users"
        ],
        "summary": "Remove o usuário.",
        "parameters": [
          {
            "$ref": "#/components/parameters/UserId"
          }
        ],
        "responses": {
          "204": {
            "description": "Removido."
          },
          "401": {
            "$ref": "#/components/responses/ServiceUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.usersH.Delete",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/users/{id}/verify/resend": {
      "post": {
        "operationId": "AuthResendVerification",
        "x-asender-authz": "service",
        "tags": [
          "users"
        ],
        "summary": "Reenvia o e-mail de verificação de um usuário.",
        "security": [
          {
            "ServiceToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "Onde é usada:  POST /v1/users/{id}/emails/{emailId}/verify/resend.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.verifH.Reenviar",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/users/{id}/emails": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "Listar devolve os e-mails do usuário.",
        "description": "Onde é usada: GET /v1/users/{id}/emails.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.emailsH.Listar",
        "x-asender-fonte": "codigo"
      },
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Adicionar cria um e-mail secundário (não-verificado) e dispara a verificação.",
        "description": "Onde é usada: POST /v1/users/{id}/emails {email}.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.emailsH.Adicionar",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/users/{id}/emails/{emailId}": {
      "delete": {
        "tags": [
          "auth"
        ],
        "summary": "Remover apaga um e-mail secundário.",
        "description": "Onde é usada: DELETE /v1/users/{id}/emails/{emailId}.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.emailsH.Remover",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/users/{id}/emails/{emailId}/primary": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "DefinirPrimario promove um e-mail verificado a primário.",
        "description": "Onde é usada:  POST /v1/users/{id}/emails/{emailId}/primary.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.emailsH.DefinirPrimario",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/users/{id}/emails/{emailId}/verify/resend": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Reenviar redispara a verificação de um e-mail.",
        "description": "Onde é usada:  POST /v1/users/{id}/emails/{emailId}/verify/resend.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.emailsH.Reenviar",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/auth/login": {
      "post": {
        "operationId": "AuthLogin",
        "x-asender-authz": "service",
        "tags": [
          "auth"
        ],
        "summary": "Autentica e emite sessão.",
        "description": "Com 2FA ativo, a resposta traz `two_factor_required:true` e **sem\ntoken**; a sessão só é emitida por `POST /v1/2fa/check`.\n\nUser-Agent e IP do chamador entram no registro da sessão (o IP vem do\n`RealIP` do chi, portanto de `X-Forwarded-For` quando houver proxy).\n",
        "x-asender-divergence": "**Oráculo de tempo** (RELATORIO-FINAL §3 item 9): email existente\ncusta o bcrypt (~200 ms), inexistente retorna em ~1 ms. Dá para\nenumerar contas cronometrando. Corrigir exige hash dummy no caminho\nde \"usuário não existe\".\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "email",
                  "password"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string",
                    "format": "password"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sessão emitida, ou 2FA pendente.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "login": {
                      "$ref": "#/components/schemas/LoginResult"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "`401 InvalidCredentials` — credenciais inválidas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authH.Login",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/auth/logout": {
      "post": {
        "operationId": "AuthLogout",
        "x-asender-authz": "session",
        "tags": [
          "auth"
        ],
        "summary": "Revoga a sessão do Bearer apresentado.",
        "description": "Credencial: `Authorization: Bearer <token de sessão>` — **não** o token\nde serviço. Ausente → 401 `MissingToken`.\n\nCorrigido na onda 7: revoga de fato e o `validate` passa a consultar o\nestado da sessão, então o token deixa de valer imediatamente.\n",
        "security": [
          {
            "SessionBearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Revogada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SessionUnauthorized"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authH.Logout",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/auth/validate": {
      "get": {
        "operationId": "AuthValidateSession",
        "x-asender-authz": "session",
        "tags": [
          "auth"
        ],
        "summary": "Resolve um token de sessão no usuário dono.",
        "description": "É o hop que o middleware `SessionAuth` do `asender-api` faz em toda\nrequisição `/api/*` autenticada.\n\n**A resposta DEVE trazer `user.id`.** Um 200 sem ele é violação de\ncontrato: o cliente do `asender-api` recusa com `ErrContractViolation` e\no middleware barra o principal anônimo (defesa em profundidade). Esta\nrota não devolve tenant — quem sabe de tenant é o `asender-core`.\n",
        "security": [
          {
            "SessionBearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Sessão válida.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "required": [
                        "id"
                      ],
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/SessionUnauthorized"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authH.Validate",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/auth/password-reset": {
      "post": {
        "operationId": "AuthPasswordReset",
        "x-asender-authz": "service",
        "tags": [
          "auth"
        ],
        "summary": "Solicita o token de redefinição de senha.",
        "x-asender-divergence": "**O token de reset volta NO CORPO** (`reset_token`) em vez de ser\nenviado por email — há um `TODO(email)` explícito para enfileirar o\nenvio via NATS. Quem alcança esta rota redefine a senha de qualquer\nusuário. Aceitável só porque o serviço é interno; some junto com a\nexposição das portas.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sempre `ok:true`; `reset_token` presente quando um token foi gerado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "reset_token": {
                      "type": "string"
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ServiceUnauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authH.PasswordReset",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/auth/password-reset/confirm": {
      "post": {
        "operationId": "AuthPasswordResetConfirm",
        "x-asender-authz": "service",
        "tags": [
          "auth"
        ],
        "summary": "Redefine a senha com o token emitido.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "token",
                  "new_password"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "new_password": {
                    "type": "string",
                    "format": "password"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Senha alterada.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "`InvalidToken` ou `ExpiredToken`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorEnvelope"
                }
              }
            }
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authH.PasswordResetConfirm",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/auth/verify": {
      "post": {
        "operationId": "AuthVerifyEmail",
        "x-asender-authz": "service",
        "tags": [
          "auth"
        ],
        "summary": "Confirma o e-mail a partir do token do link.",
        "security": [
          {
            "ServiceToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "consome o token do link e carimba `email_verified_at`.\n\nOnde é usada: tela `/verify` do front, com o token da URL.\n\nEfeitos: duas escritas. Token inválido, expirado e já usado respondem IGUAL: distingui-los diria a quem tem um link velho se ele já foi usado por outra pessoa.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.verifH.Confirmar",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/auth/sessions": {
      "get": {
        "operationId": "AuthListSessions",
        "x-asender-authz": "service",
        "tags": [
          "auth"
        ],
        "summary": "Sessões ativas do usuário.",
        "security": [
          {
            "ServiceToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "lista onde a conta de quem apresenta o token está aberta.\n\nOnde é usada: tela de segurança do painel, através do BFF.\n\nEfeitos: uma leitura. Não recebe usuário por parâmetro: a lista é sempre a do dono do token. Um `?user_id=` seria a rota mais barata do sistema para descobrir de onde outra pessoa se conecta.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authH.Sessoes",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/auth/sessions/{id}": {
      "delete": {
        "operationId": "AuthRevokeSession",
        "x-asender-authz": "service",
        "tags": [
          "auth"
        ],
        "summary": "Revoga uma sessão específica.",
        "security": [
          {
            "ServiceToken": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "401": {
            "description": "credencial ausente ou inválida"
          }
        },
        "description": "derruba UMA sessão do próprio usuário.\n\nOnde é usada: botão \"remover\" da tela de segurança.\n\nEfeitos: a sessão para de autenticar imediatamente. 404 tanto para sessão inexistente quanto para sessão de OUTRA pessoa: a distinção transformaria a rota num oráculo de id de sessão alheia.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authH.RevogarSessao",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/authz/decide": {
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "`{permitido, motivo, papel, nivel}`.",
        "description": "`{permitido, motivo, papel, nivel}`.\n\nOnde é usada: POST /v1/authz/decide.\n\nEfeitos: grava a trilha (dentro do PDP). Erro de banco vira 503, e não `{permitido:false}`: o PEP precisa distinguir \"a política diz não\" de \"não consegui perguntar\" — o primeiro ele mostra ao usuário, o segundo ele resolve com o cache que já tem (piso 10).",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authzH.Decidir",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/authz/eu": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "`{produtos:[{slug,nome,subdominio,papel,nivel}]}`.",
        "description": "`{produtos:[{slug,nome,subdominio,papel,nivel}]}`.\n\nOnde é usada:  GET /v1/authz/eu?sub=&tenant= — alimenta o switcher e a home do hub.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authzH.Meus",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/authz/produtos": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "Catalogo devolve as ferramentas que existem.",
        "description": "Onde é usada: GET /v1/authz/produtos.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authzH.Catalogo",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/tenants/{id}/produtos": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "ListarAssinatura devolve o que a conta assina.",
        "description": "Onde é usada: GET /v1/tenants/{id}/produtos.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authzH.ListarAssinatura",
        "x-asender-fonte": "codigo"
      },
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Assinar liga ou suspende um produto na conta.",
        "description": "Onde é usada: POST /v1/tenants/{id}/produtos {produto, status}. `status` é validado aqui E no CHECK da tabela: a borda dá a mensagem, o banco dá a garantia — a borda pode ser contornada por outro caminho de escrita.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authzH.Assinar",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/tenants/{id}/acessos": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "ListarAcessos devolve quem acessa o quê na conta.",
        "description": "Onde é usada: GET /v1/tenants/{id}/acessos.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authzH.ListarAcessos",
        "x-asender-fonte": "codigo"
      },
      "post": {
        "tags": [
          "auth"
        ],
        "summary": "Conceder dá acesso de uma pessoa a uma ferramenta da conta.",
        "description": "Onde é usada: POST /v1/tenants/{id}/acessos {user_id, produto, papel}. Papel que o produto não declara é recusado pela FK — 422 com a mensagem, e não 500: \"papel inventado\" é erro de quem chamou, não do servidor.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authzH.Conceder",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/tenants/{id}/acessos/{userId}/{produto}": {
      "delete": {
        "tags": [
          "auth"
        ],
        "summary": "Revogar tira o acesso.",
        "description": "Onde é usada: DELETE /v1/tenants/{id}/acessos/{userId}/{produto}. Idempotente: revogar duas vezes responde ok nas duas. O efeito chega às ferramentas em até 60s (TTL do cache do PEP) — contrato escrito no ADR-0025.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authzH.Revogar",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/tenants/{id}/authz/decisoes": {
      "get": {
        "tags": [
          "auth"
        ],
        "summary": "Trilha devolve as decisões recentes da conta.",
        "description": "Onde é usada: GET /v1/tenants/{id}/authz/decisoes?negadas=1&limite=50.",
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.authzH.Trilha",
        "x-asender-fonte": "codigo"
      }
    },
    "/v1/2fa/setup": {
      "post": {
        "operationId": "AuthTwoFASetup",
        "x-asender-authz": "service",
        "tags": [
          "twofa"
        ],
        "summary": "Gera o segredo TOTP e a `otpauth://` URL.",
        "description": "Ainda não confirmado: o 2FA só passa a valer depois de `POST /v1/2fa/verify`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserIdBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Segredo gerado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "setup": {
                      "type": "object",
                      "properties": {
                        "secret": {
                          "type": "string",
                          "description": "Segredo base32. Sai UMA VEZ."
                        },
                        "otpauth_url": {
                          "type": "string"
                        }
                      }
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ServiceUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.twofaH.Setup",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/2fa/verify": {
      "post": {
        "operationId": "AuthTwoFAVerify",
        "x-asender-authz": "service",
        "tags": [
          "twofa"
        ],
        "summary": "Confirma o setup do TOTP.",
        "description": "NÃO emite sessão — isso é o `check`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TwoFACodeBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/TwoFactorFailed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.twofaH.Verify",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/2fa/disable": {
      "post": {
        "operationId": "AuthTwoFADisable",
        "x-asender-authz": "service",
        "tags": [
          "twofa"
        ],
        "summary": "Desliga o TOTP do usuário.",
        "x-asender-divergence": "Aceita só `user_id` — **não exige código TOTP nem senha**. Quem alcança\nesta rota remove o segundo fator de qualquer conta.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserIdBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Desligado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "const": true
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/ServiceUnauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.twofaH.Disable",
        "x-asender-fonte": "spec+codigo"
      }
    },
    "/v1/2fa/check": {
      "post": {
        "operationId": "AuthTwoFACheck",
        "x-asender-authz": "service",
        "tags": [
          "twofa"
        ],
        "summary": "Segunda etapa do login — valida o código e EMITE a sessão.",
        "description": "Sucesso devolve o mesmo `login` de `POST /v1/auth/login`, agora com token.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TwoFACodeBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Código válido; sessão emitida.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "login": {
                      "$ref": "#/components/schemas/LoginResult"
                    },
                    "RequestId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/TwoFactorFailed"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        },
        "x-asender-servico": "asender-auth",
        "x-asender-handler": "s.twofaH.Check",
        "x-asender-fonte": "spec+codigo"
      }
    }
  },
  "components": {
    "schemas": {
      "api_ErrorEnvelope": {
        "type": "object",
        "required": [
          "Error",
          "RequestId"
        ],
        "properties": {
          "Error": {
            "type": "object",
            "required": [
              "Type",
              "Code",
              "Message"
            ],
            "properties": {
              "Type": {
                "type": "string",
                "enum": [
                  "Sender",
                  "Receiver"
                ],
                "description": "Derivado do status: 4xx→Sender, 5xx→Receiver. O SDK decide retry por aqui."
              },
              "Code": {
                "type": "string",
                "examples": [
                  "ValidationError",
                  "AuthorizationError",
                  "NotFound",
                  "TenantSuspended",
                  "NoTenant",
                  "Conflict",
                  "PayloadTooLarge",
                  "Throttling",
                  "InternalError"
                ]
              },
              "Message": {
                "type": "string"
              },
              "Details": {
                "description": "Presente só na validação SES do `/v1/*`: mapa campo → motivo. Omitido quando vazio.",
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          },
          "RequestId": {
            "type": "string"
          }
        }
      },
      "api_RequestIdEnvelope": {
        "type": "object",
        "required": [
          "RequestId"
        ],
        "properties": {
          "RequestId": {
            "type": "string",
            "description": "Presente em TODA resposta de sucesso. Espelha o header\n`X-Request-Id`. **Divergência:** um `X-Request-Id` mandado pelo\ncliente é ecoado cru e vira o correlation id do log\n(RELATORIO-FINAL §3 item 15).\n"
          }
        }
      },
      "api_CursorPage": {
        "type": "object",
        "properties": {
          "NextCursor": {
            "type": "integer",
            "description": "Cursor da próxima página; 0 quando acabou."
          },
          "HasMore": {
            "type": "boolean"
          }
        }
      },
      "api_Channel": {
        "type": "string",
        "enum": [
          "email",
          "sms",
          "push"
        ]
      },
      "api_MessageStatus": {
        "type": "string",
        "enum": [
          "queued",
          "sending",
          "sent",
          "delivered",
          "bounced",
          "failed",
          "opened",
          "clicked"
        ]
      },
      "api_Message": {
        "type": "object",
        "description": "Projeção de listagem: SEM corpo (payload grande em lista é desperdício).",
        "properties": {
          "Id": {
            "type": "string"
          },
          "TenantId": {
            "type": "string"
          },
          "Channel": {
            "$ref": "#/components/schemas/Channel"
          },
          "Status": {
            "$ref": "#/components/schemas/MessageStatus"
          },
          "From": {
            "type": "string"
          },
          "To": {
            "type": "string"
          },
          "Subject": {
            "type": "string"
          },
          "Provider": {
            "type": [
              "string",
              "null"
            ]
          },
          "ProviderMessageId": {
            "type": [
              "string",
              "null"
            ]
          },
          "ErrorCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "ErrorMessage": {
            "type": [
              "string",
              "null"
            ]
          },
          "QueuedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "SentAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "DeliveredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "FailedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "OpenedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "ClickedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "CreatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "api_MessagePage": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RequestIdEnvelope"
          },
          {
            "$ref": "#/components/schemas/CursorPage"
          },
          {
            "type": "object",
            "properties": {
              "Messages": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/Message"
                }
              }
            }
          }
        ]
      },
      "api_MessageDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/RequestIdEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "Message": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/Message"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "BodyText": {
                        "type": "string"
                      },
                      "BodyHtml": {
                        "type": "string"
                      },
                      "Metadata": {
                        "type": "object",
                        "additionalProperties": true,
                        "description": "VALOR OPACO — chaves internas passam intactas (não são pascalizadas)."
                      }
                    }
                  }
                ]
              },
              "Events": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "Type": {
                      "type": "string"
                    },
                    "OccurredAt": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "Payload": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Valor opaco."
                    }
                  }
                }
              }
            }
          }
        ]
      },
      "api_SendEmailRequest": {
        "type": "object",
        "required": [
          "Source",
          "Destination",
          "Message"
        ],
        "properties": {
          "Source": {
            "type": "string",
            "format": "email"
          },
          "Destination": {
            "type": "object",
            "properties": {
              "ToAddresses": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "email"
                }
              },
              "CcAddresses": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "email"
                }
              },
              "BccAddresses": {
                "type": "array",
                "items": {
                  "type": "string",
                  "format": "email"
                }
              }
            },
            "description": "Ao menos um endereço somando To+Cc+Bcc."
          },
          "Message": {
            "type": "object",
            "required": [
              "Subject",
              "Body"
            ],
            "properties": {
              "Subject": {
                "type": "object",
                "properties": {
                  "Data": {
                    "type": "string",
                    "minLength": 1
                  },
                  "Charset": {
                    "type": "string"
                  }
                }
              },
              "Body": {
                "type": "object",
                "description": "Ao menos um de Text.Data ou Html.Data.",
                "properties": {
                  "Text": {
                    "type": "object",
                    "properties": {
                      "Data": {
                        "type": "string"
                      },
                      "Charset": {
                        "type": "string"
                      }
                    }
                  },
                  "Html": {
                    "type": "object",
                    "properties": {
                      "Data": {
                        "type": "string"
                      },
                      "Charset": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "ReplyToAddresses": {
            "type": "array",
            "items": {
              "type": "string",
              "format": "email"
            }
          },
          "ReturnPath": {
            "type": "string",
            "format": "email"
          },
          "IdempotencyKey": {
            "type": "string",
            "description": "É ESTE campo que funciona, não o header."
          },
          "Tags": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "api_BatchResult": {
        "type": "object",
        "properties": {
          "Index": {
            "type": "integer"
          },
          "Status": {
            "type": "string",
            "enum": [
              "queued",
              "rejected",
              "failed"
            ]
          },
          "MessageId": {
            "type": "string"
          },
          "MessageIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "Error": {
            "type": "object",
            "properties": {
              "Code": {
                "type": "string"
              },
              "Message": {
                "type": "string"
              },
              "Details": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "api_Template": {
        "type": "object",
        "properties": {
          "Id": {
            "type": "string"
          },
          "TenantId": {
            "type": "string"
          },
          "Slug": {
            "type": "string"
          },
          "Name": {
            "type": "string"
          },
          "Channel": {
            "$ref": "#/components/schemas/Channel"
          },
          "Subject": {
            "type": "string"
          },
          "BodyHtml": {
            "type": "string"
          },
          "BodyText": {
            "type": "string"
          },
          "Variables": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Valor opaco — não pascalizado."
          },
          "CreatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "UpdatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "api_ContactInput": {
        "type": "object",
        "additionalProperties": false,
        "description": "Ao menos um de Email ou Phone.",
        "properties": {
          "Email": {
            "type": "string"
          },
          "Phone": {
            "type": "string"
          },
          "Name": {
            "type": "string"
          },
          "Attributes": {
            "type": "object",
            "additionalProperties": true,
            "description": "Valor opaco."
          },
          "Subscribed": {
            "type": "boolean",
            "default": true
          }
        }
      },
      "api_Contact": {
        "type": "object",
        "properties": {
          "Id": {
            "type": "string"
          },
          "TenantId": {
            "type": "string"
          },
          "Email": {
            "type": "string"
          },
          "Phone": {
            "type": "string"
          },
          "Name": {
            "type": "string"
          },
          "Attributes": {
            "type": "object",
            "additionalProperties": true
          },
          "Subscribed": {
            "type": "boolean"
          },
          "UnsubscribedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "CreatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "UpdatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "api_ContactList": {
        "type": "object",
        "properties": {
          "Id": {
            "type": "string"
          },
          "TenantId": {
            "type": "string"
          },
          "Name": {
            "type": "string"
          },
          "Slug": {
            "type": "string"
          },
          "Description": {
            "type": "string"
          },
          "MemberCount": {
            "type": "integer"
          },
          "CreatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "UpdatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "api_Device": {
        "type": "object",
        "properties": {
          "Id": {
            "type": "string"
          },
          "TenantId": {
            "type": "string"
          },
          "ContactId": {
            "type": "string"
          },
          "Token": {
            "type": "string"
          },
          "Platform": {
            "type": "string",
            "enum": [
              "ios",
              "android",
              "web"
            ]
          },
          "Active": {
            "type": "boolean"
          },
          "LastSeenAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "CreatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "UpdatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "api_Tenant": {
        "type": "object",
        "description": "Projeção do asender-core (`tenantPayload`), pascalizada.",
        "properties": {
          "Id": {
            "type": "string"
          },
          "Name": {
            "type": "string"
          },
          "Slug": {
            "type": "string"
          },
          "Status": {
            "type": "string",
            "enum": [
              "active",
              "suspended"
            ]
          },
          "TrialEndsAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "Metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Valor opaco."
          },
          "CreatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "UpdatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "api_TenantNode": {
        "type": "object",
        "description": "Nó de árvore (`tenantNodePayload` do core), pascalizado.",
        "properties": {
          "Id": {
            "type": "string"
          },
          "Name": {
            "type": "string"
          },
          "Slug": {
            "type": "string"
          },
          "Status": {
            "type": "string"
          },
          "Kind": {
            "type": "string",
            "enum": [
              "root",
              "leaf"
            ]
          },
          "Depth": {
            "type": "integer",
            "description": "Profundidade absoluta na árvore."
          },
          "RelativeDepth": {
            "type": "integer",
            "description": "Profundidade relativa ao nó consultado — use para indentar."
          },
          "ParentId": {
            "type": [
              "string",
              "null"
            ]
          },
          "MonthlyQuota": {
            "type": [
              "integer",
              "null"
            ],
            "description": "`null` = herda."
          },
          "EffectiveQuota": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Valor que realmente vale; `null` = sem limite em nenhum ancestral."
          },
          "ChildrenCount": {
            "type": "integer"
          },
          "CreatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "UpdatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "api_Usage": {
        "type": "object",
        "properties": {
          "Period": {
            "type": "string",
            "examples": [
              "2026-07"
            ]
          },
          "EmailsSent": {
            "type": "integer"
          },
          "SmsSent": {
            "type": "integer"
          },
          "PushSent": {
            "type": "integer"
          },
          "ApiCalls": {
            "type": "integer"
          },
          "UpdatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "api_ReportCounters": {
        "type": "object",
        "description": "`Total` é o volume do período; `Queued` é BACKLOG (o que ainda não saiu).",
        "properties": {
          "Total": {
            "type": "integer"
          },
          "Queued": {
            "type": "integer"
          },
          "Sent": {
            "type": "integer"
          },
          "Delivered": {
            "type": "integer"
          },
          "Bounced": {
            "type": "integer"
          },
          "Failed": {
            "type": "integer"
          },
          "Opened": {
            "type": "integer"
          },
          "Clicked": {
            "type": "integer"
          }
        }
      },
      "api_ReportRates": {
        "type": "object",
        "description": "`Delivery = Delivered/Sent`, `Bounce = Bounced/Sent`,\n`Open = Opened/Delivered`, `Click = Clicked/Delivered`.\nDenominador zero devolve `0` (nunca `null`, nunca `NaN`).\n",
        "properties": {
          "Delivery": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "Bounce": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "Open": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "Click": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          }
        }
      },
      "api_ReportPeriod": {
        "type": "object",
        "description": "Janela efetivamente usada, já com o default aplicado. `To` é EXCLUSIVO.",
        "properties": {
          "From": {
            "type": "string",
            "format": "date-time"
          },
          "To": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "auth_ErrorEnvelope": {
        "type": "object",
        "required": [
          "Error",
          "RequestId"
        ],
        "properties": {
          "Error": {
            "type": "object",
            "required": [
              "Type",
              "Code",
              "Message"
            ],
            "properties": {
              "Type": {
                "type": "string",
                "enum": [
                  "Sender",
                  "Receiver"
                ]
              },
              "Code": {
                "type": "string",
                "examples": [
                  "InvalidRequest",
                  "InvalidInput",
                  "MissingToken",
                  "InvalidCredentials",
                  "InvalidToken",
                  "ExpiredToken",
                  "TwoFactorFailed",
                  "TwoFactorMissing",
                  "NotFound",
                  "Conflict",
                  "NotReady",
                  "InternalError"
                ]
              },
              "Message": {
                "type": "string"
              },
              "Details": {}
            }
          },
          "RequestId": {
            "type": "string"
          }
        }
      },
      "auth_User": {
        "type": "object",
        "description": "Nunca traz `password_hash` nem o segredo TOTP.",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "usr_demo_owner"
            ]
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "auth_LoginResult": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "description": "JWT de sessão. VAZIO quando `two_factor_required`."
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "user_id": {
            "type": "string"
          },
          "two_factor_required": {
            "type": "boolean"
          }
        }
      },
      "auth_UserIdBody": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "user_id"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          }
        }
      },
      "auth_TwoFACodeBody": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "user_id",
          "code"
        ],
        "properties": {
          "user_id": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "TOTP de 6 dígitos."
          }
        }
      }
    },
    "securitySchemes": {
      "ApiKeyBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer sk_live_...`. Prefixos aceitos: `sk_live_`,\n`sk_test_`, `sk_master_`, `pk_live_`, mínimo 16 caracteres. Validada no\n`asender-core`; tenant não-`active` → 403 `TenantSuspended`.\n\n**Alternativas aceitas e DIVERGENTES do piso da casa:** o header\n`X-Api-Key` e o parâmetro de query `?api_key=`. Credencial em query\nstring vaza para access log, Referer e histórico do browser — é dívida\nregistrada, não recomendação.\n"
      },
      "SessionBearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Token de sessão do usuário final, emitido por `POST /v1/auth/login`."
      },
      "SessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "asender_session",
        "description": "Cookie HttpOnly posto pelo dashboard. Equivalente ao Bearer de sessão."
      },
      "ServiceToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Asender-Svc-Token",
        "description": "`SERVICE_SECRET` **cru** (não é JWT), comparado em tempo constante —\nmesmo contrato do `asender-core` e do `asender_messages`. Exigido em\ntodo ambiente; só `SERVICE_AUTH_DISABLED=true` desliga.\n"
      }
    }
  }
}