API OpenAI com Python e FastAPI: crie um endpoint seguro

API OpenAI com Python e FastAPI é uma combinação adequada para criar um backend que recebe mensagens, valida os dados, chama um modelo da OpenAI e devolve uma resposta em JSON. A chave permanece no servidor, sem ser exposta ao navegador ou ao aplicativo do usuário.

Neste tutorial, você criará um endpoint POST /api/responder usando o SDK oficial assíncrono da OpenAI e a Responses API. O exemplo também inclui validação com Pydantic, CORS restrito, tratamento de erros, teste com curl e uma opção de streaming por Server-Sent Events.

Documentação e exemplos revisados em 10 de outubro de 2026. O código não foi executado neste artigo; valide dependências, modelo, limites e comportamento no seu próprio ambiente antes de usar em produção.

O que será construído

A arquitetura terá três partes:

  1. um cliente envia uma mensagem ao seu endpoint FastAPI;
  2. o servidor valida o corpo, acrescenta instruções e chama a OpenAI API;
  3. o servidor devolve apenas os campos necessários ao cliente.
Navegador ou aplicativo
          |
          | POST /api/responder
          v
Backend FastAPI
  - autenticação do seu usuário
  - validação
  - limites
  - chave da OpenAI
          |
          | Responses API
          v
OpenAI API

O frontend nunca deve receber OPENAI_API_KEY. Ele chama seu backend, e o backend chama a OpenAI.

Pré-requisitos

ItemRecomendação
PythonPython 3.10 ou mais recente, conforme o SDK oficial atual
ContaConta na OpenAI Platform com projeto e faturamento configurados
ChaveChave de projeto armazenada em variável de ambiente
FrameworkFastAPI com servidor ASGI
SDKBiblioteca oficial openai
ModeloModel ID atual escolhido conforme tarefa e orçamento

O SDK Python oficial oferece clientes síncrono e assíncrono. Como os endpoints FastAPI normalmente são assíncronos, este exemplo usa AsyncOpenAI para não bloquear desnecessariamente o event loop enquanto espera a resposta HTTP.

1. Crie o projeto e instale as dependências

mkdir openai-fastapi
cd openai-fastapi

python -m venv .venv

Ative o ambiente no macOS ou Linux

source .venv/bin/activate

Ative no Windows PowerShell

.venv\Scripts\Activate.ps1

Instale FastAPI, o servidor e o SDK:

python -m pip install "fastapi[standard]" openai python-dotenv

fastapi[standard] instala dependências recomendadas, incluindo o servidor necessário para executar a aplicação. O pacote python-dotenv é usado apenas para carregar um arquivo local durante o desenvolvimento.

Em produção, registre versões testadas em um arquivo de lock. Não presuma que uma atualização automática de dependência manterá o mesmo comportamento.

2. Configure as variáveis de ambiente

Crie um arquivo chamado .env:

OPENAI_API_KEY=sua_chave_aqui
OPENAI_MODEL=gpt-6-luna
FRONTEND_ORIGINS=http://localhost:3000

Adicione o arquivo ao .gitignore:

.env
.venv/
__pycache__/
*.pyc

O modelo padrão do tutorial é gpt-6-luna, opção atual voltada a tarefas focadas e de alto volume. Para trabalho mais complexo, avalie modelos como gpt-6.1-sol ou gpt-6-astra no catálogo oficial.

3. Crie o arquivo main.py

Este exemplo:

  • falha na inicialização se a chave não estiver configurada;
  • aceita apenas mensagens entre 1 e 8 mil caracteres;
  • permite uma instrução opcional limitada;
  • não expõe mensagens internas de erro ao usuário;
  • não armazena o objeto Response por padrão;
  • restringe CORS a origens configuradas.
import os
from typing import Final

import openai
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from openai import AsyncOpenAI
from pydantic import BaseModel, Field

load_dotenv()

api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
    raise RuntimeError(
        "Defina OPENAI_API_KEY como variável de ambiente."
    )

MODEL: Final[str] = os.getenv("OPENAI_MODEL", "gpt-6-luna")

client = AsyncOpenAI(api_key=api_key)

app = FastAPI(
    title="API OpenAI com FastAPI",
    version="1.0.0",
)

allowed_origins = [
    origin.strip()
    for origin in os.getenv(
        "FRONTEND_ORIGINS",
        "http://localhost:3000",
    ).split(",")
    if origin.strip()
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=allowed_origins,
    allow_credentials=False,
    allow_methods=["GET", "POST"],
    allow_headers=["Content-Type", "Authorization"],
)


class ChatRequest(BaseModel):
    mensagem: str = Field(
        min_length=1,
        max_length=8_000,
    )
    instrucao: str | None = Field(
        default=None,
        max_length=2_000,
    )


class ChatResponse(BaseModel):
    resposta: str
    model: str
    response_id: str


@app.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}


@app.post(
    "/api/responder",
    response_model=ChatResponse,
)
async def responder(payload: ChatRequest) -> ChatResponse:
    instructions = (
        payload.instrucao
        or (
            "Responda em português do Brasil. "
            "Se não tiver informação suficiente, diga isso. "
            "Não invente dados, fontes ou ações concluídas."
        )
    )

    try:
        response = await client.responses.create(
            model=MODEL,
            instructions=instructions,
            input=payload.mensagem,
            store=False,
        )
    except openai.RateLimitError as exc:
        raise HTTPException(
            status_code=429,
            detail=(
                "Limite temporário atingido. "
                "Tente novamente depois."
            ),
        ) from exc
    except openai.AuthenticationError as exc:
        raise HTTPException(
            status_code=500,
            detail="Falha na configuração do serviço.",
        ) from exc
    except openai.APIConnectionError as exc:
        raise HTTPException(
            status_code=503,
            detail=(
                "Não foi possível conectar à OpenAI API."
            ),
        ) from exc
    except openai.APIError as exc:
        raise HTTPException(
            status_code=502,
            detail=(
                "A OpenAI API retornou um erro."
            ),
        ) from exc

    text = response.output_text.strip()

    if not text:
        raise HTTPException(
            status_code=502,
            detail="O modelo não retornou texto.",
        )

    return ChatResponse(
        resposta=text,
        model=response.model,
        response_id=response.id,
    )

Os limites de 8 mil e 2 mil caracteres são decisões da aplicação, não limites oficiais do modelo. Eles reduzem abuso e custos inesperados, mas devem ser ajustados à tarefa real.

Como o código funciona

AsyncOpenAI

AsyncOpenAI é o cliente assíncrono do SDK oficial. A chamada utiliza await, permitindo que o servidor atenda outras conexões enquanto aguarda a resposta externa.

Pydantic

FastAPI usa modelos Pydantic para ler o JSON, converter tipos, validar campos e gerar o schema OpenAPI. Se o corpo for inválido, o framework devolve uma resposta de validação antes de chamar a OpenAI.

Responses API

client.responses.create() envia a solicitação ao endpoint atual de respostas. response.output_text reúne o conteúdo textual produzido.

store=False

O parâmetro impede o armazenamento padrão do objeto Response nesse fluxo. Ele não elimina logs ou retenção no restante da arquitetura e não substitui uma política de dados.

4. Inicie o servidor em desenvolvimento

fastapi dev main.py

A aplicação ficará disponível, normalmente, em:

  • http://127.0.0.1:8000;
  • http://127.0.0.1:8000/docs para Swagger UI;
  • http://127.0.0.1:8000/redoc para ReDoc.

Não use modo de recarregamento automático em produção. A documentação de execução do FastAPI alerta que --reload consome mais recursos e é destinado ao desenvolvimento.

5. Teste o endpoint com curl

curl -X POST http://127.0.0.1:8000/api/responder \
  -H "Content-Type: application/json" \
  -d '{
    "mensagem": "Explique o que é FastAPI em quatro frases."
  }'

Uma resposta bem-sucedida terá estrutura semelhante a:

{
  "resposta": "FastAPI é um framework...",
  "model": "gpt-6-luna",
  "response_id": "resp_..."
}

O response_id pode ajudar a investigar falhas e correlacionar logs. Não registre a chave, o corpo completo ou dados pessoais sem necessidade.

Como adicionar autenticação ao seu endpoint

O exemplo não autentica usuários finais. Sem proteção, qualquer pessoa que alcance o endpoint poderá consumir seus créditos.

Em produção, adicione pelo menos:

  • sessão, JWT ou outra autenticação adequada;
  • limite por usuário, IP, organização ou plano;
  • limite diário ou mensal de custo;
  • bloqueio de contas abusivas;
  • identificador de segurança pseudonimizado;
  • logs de auditoria para ações importantes.

Não confie apenas em CORS. CORS controla o que navegadores permitem, mas não impede que outro cliente HTTP chame publicamente seu servidor.

Como configurar CORS corretamente

CORS é necessário quando o frontend e o backend usam origens diferentes. Uma origem é formada por protocolo, domínio e porta.

A documentação oficial do FastAPI sobre CORS recomenda listar origens explicitamente, especialmente quando a aplicação usa cookies ou cabeçalhos de autorização.

Evite em produção:

allow_origins=["*"]

Prefira:

allow_origins=[
    "https://app.exemplo.com",
    "https://admin.exemplo.com",
]

Se usar credenciais, teste preflight, cookies, cabeçalhos e domínios reais. Não autorize origens desconhecidas apenas para eliminar um erro no navegador.

Como criar streaming com FastAPI

Streaming permite entregar trechos de texto enquanto o modelo continua gerando. A Responses API envia eventos semânticos; o evento response.output_text.delta contém os novos trechos.

Adicione estes imports ao início de main.py:

import json

from fastapi.responses import StreamingResponse

Depois, acrescente o endpoint:

@app.post("/api/stream")
async def stream_response(
    payload: ChatRequest,
) -> StreamingResponse:
    async def event_generator():
        try:
            stream = await client.responses.create(
                model=MODEL,
                instructions=(
                    "Responda em português do Brasil. "
                    "Não invente fatos ausentes."
                ),
                input=payload.mensagem,
                store=False,
                stream=True,
            )

            async for event in stream:
                if event.type == "response.output_text.delta":
                    data = json.dumps(
                        {"delta": event.delta},
                        ensure_ascii=False,
                    )
                    yield f"data: {data}\n\n"

                elif event.type == "response.completed":
                    yield "event: done\ndata: {}\n\n"

        except openai.APIError:
            data = json.dumps(
                {"mensagem": "Falha ao gerar a resposta."},
                ensure_ascii=False,
            )
            yield f"event: error\ndata: {data}\n\n"

    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no",
        },
    )

Depois que o streaming começa, o servidor não consegue substituir facilmente a resposta por um erro HTTP comum. Por isso, falhas ocorridas durante o fluxo são enviadas como eventos SSE.

Streaming pode exibir conteúdo antes da revisão completa. Avalie moderação, interrupção, reconexão e tratamento de respostas incompletas.

Como manter uma conversa

Para continuar uma conversa simples, envie previous_response_id na próxima chamada:

primeira = await client.responses.create(
    model=MODEL,
    input="Meu produto é uma plataforma de cursos.",
    store=True,
)

segunda = await client.responses.create(
    model=MODEL,
    previous_response_id=primeira.id,
    input="Crie três mensagens de onboarding.",
)

Se precisar de uma conversa durável entre sessões ou dispositivos, avalie a Conversations API. Antes disso, defina autenticação, propriedade da conversa, exclusão, retenção e autorização de acesso.

Não aceite um previous_response_id fornecido pelo cliente sem verificar se ele pertence ao usuário autenticado. Caso contrário, uma pessoa pode tentar acessar contexto de outra conversa.

Como escolher o modelo no backend

Evite permitir que qualquer usuário envie livremente um model ID. Isso pode causar custos inesperados ou acesso a modelos inadequados.

Uma estratégia simples é mapear funções internas:

MODELS = {
    "economico": "gpt-6-luna",
    "equilibrado": "gpt-6.1-sol",
    "avancado": "gpt-6-astra",
}

O backend escolhe o perfil autorizado e não confia em um model ID arbitrário vindo do navegador.

PerfilUso provável
EconômicoClassificação, extração e transformação simples
EquilibradoAnálise profissional, texto e automações com maior exigência
AvançadoRaciocínio complexo, programação e tarefas difíceis

Faça testes com dados reais anonimizados. Um modelo mais caro pode reduzir correções, mas um modelo menor pode atender perfeitamente uma tarefa restrita.

Como controlar custos

  • limite tamanho das mensagens;
  • limite o número de chamadas por usuário;
  • não envie históricos irrelevantes;
  • use modelos menores em tarefas simples;
  • aproveite prompt caching em prefixos estáveis;
  • registre tokens e ferramentas por chamada;
  • defina limites financeiros no projeto;
  • interrompa loops de novas tentativas;
  • revise respostas longas sem necessidade.
custo estimado =
tokens de entrada
+ gravação e leitura de cache
+ tokens de saída
+ ferramentas
+ novas tentativas
+ revisão humana

Use a tabela oficial de preços e não fixe valores no código. Tarifas podem variar por modelo, contexto e modo de processamento.

Tratamento de erros

ExceçãoResposta recomendada do backend
AuthenticationErrorErro interno de configuração; não exponha detalhes ao usuário
RateLimitErrorHTTP 429 e orientação para tentar depois
APIConnectionErrorHTTP 503 para indisponibilidade temporária
APIErrorHTTP 502 genérico, com log interno controlado
Validação FastAPIHTTP 422 gerado automaticamente

Em produção, respeite Retry-After e use backoff com jitter para erros temporários. Não tente novamente erros de chave, créditos esgotados ou limites financeiros sem corrigir a causa.

Logs e observabilidade

Registre o suficiente para investigar falhas, sem copiar todo o conteúdo do usuário por padrão.

  • ID interno da requisição;
  • response_id da OpenAI;
  • model ID;
  • status e classe do erro;
  • latência;
  • tokens de entrada, cache e saída;
  • usuário ou cliente pseudonimizado;
  • ferramentas chamadas;
  • resultado da validação.

Não registre a chave, cabeçalhos de autorização, senhas, documentos completos ou dados pessoais sem finalidade e controle de acesso.

Como executar em produção

Para iniciar com o comando de produção do FastAPI:

fastapi run main.py

Também é possível usar um servidor ASGI diretamente:

uvicorn main:app --host 0.0.0.0 --port 8000

Um deploy real também precisa considerar:

  • HTTPS;
  • proxy reverso ou plataforma gerenciada;
  • reinício automático;
  • múltiplos processos ou réplicas;
  • timeouts;
  • health checks;
  • segredos gerenciados;
  • autenticação e autorização;
  • limites e proteção contra abuso;
  • monitoramento de custo e erros.

Erros frequentes nesta integração

A chave não foi encontrada

Confirme o nome OPENAI_API_KEY, reinicie o terminal e verifique se o arquivo .env está no diretório correto.

O navegador mostra erro de CORS

Adicione a origem exata do frontend em FRONTEND_ORIGINS. Protocolo, domínio e porta fazem parte da origem.

Erro 401

A chave pode estar inválida, revogada, ligada a outro projeto ou sem permissão para o recurso.

Erro 429

Pode indicar rate limit, saldo esgotado, limite financeiro ou limite de uso. Leia o código específico do erro; repetir a chamada não resolve todos esses casos.

A resposta demora

Compare modelo, reasoning effort, tamanho da entrada, ferramentas e volume. Use streaming quando a experiência exigir feedback imediato, mas não o trate como redução do tempo total de processamento.

Checklist antes de colocar o endpoint no ar

  • chave fora do código e do navegador;
  • dependências registradas em lockfile;
  • autenticação dos usuários;
  • CORS restrito;
  • limites de entrada e saída;
  • rate limit por usuário;
  • tratamento de 401, 429, 500 e 503;
  • timeouts e número máximo de tentativas;
  • logs sem segredos;
  • monitoramento de tokens e custos;
  • testes de prompt injection;
  • revisão humana nas tarefas de risco;
  • processo para revogar chaves;
  • política de retenção e exclusão.

A integração básica exige poucas linhas, mas um serviço confiável precisa proteger credenciais, usuários, custos e dados. Use o endpoint simples para validar a ideia, depois acrescente autenticação, avaliações, observabilidade e controles antes de ampliar o tráfego.

Fontes oficiais verificadas