Integração

Como criar APIs REST no TOTVS Protheus

Expor serviços ADVPL de forma segura e performática — do WS REST aos cuidados de produção.

O TOTVS Protheus permite expor rotinas como serviços web REST. Quando bem construídas, essas APIs viram a porta de entrada de integrações — e-commerce, apps, ERPs parceiros — sem abrir o AppServer para fora sem controle.

Comece pela arquitetura, não pelo código

Antes de escrever o primeiro WSRESTFUL, defina o contrato: recursos, métodos, payloads, erros e versão. Uma API com contrato claro evita refatoração quando o integrador externo já está em produção.

  • Versionamento: use /api/v1/... desde o início; quebrar contrato sem versão derruba integrações.
  • Autenticação: token com expiração, nunca credencial de usuário Protheus direto no cliente.
  • Padrão de erro: corpo JSON com codigo e mensagem, não só HTTP status.

Estrutura de um WS REST no ADVPL

O REST do Protheus usa o framework WSRESTFUL/WSMETHOD (TDN, WSMETHOD / REST): o bloco do serviço declara o recurso e cada WSMETHOD mapeia um verbo HTTP (GET, POST, PUT, DELETE). Dentro do método, o objeto Self dá acesso ao header (::GetHeader()), ao body (::GetContent()) e à resposta (::SetResponse()); erros usam SetRestFault().

RESTPEDIDO.PRW — Exemplo completo de WS REST
// RESTPEDIDO.PRW — API de consulta e criação de pedidos
#Include "protheus.ch"
#Include "restful.ch"
#Include "topconn.ch"

WsRestFul Pedidos Description "API de pedidos v1" Format Application_Json
  WsData page  As Integer
  WsData limit As Integer

  WsMethod GET  Description "Consulta pedidos" WsSyntax "/api/v1/pedidos/{page}/{limit}"
  WsMethod POST Description "Cria pedido"      WsSyntax "/api/v1/pedidos"
End WsRestFul

WsMethod GET WsReceive page, limit WsService Pedidos
  // Header: ::GetHeader("Authorization") | Body (POST): ::GetContent()
  ::SetContentType("application/json")
  ::SetResponse('{"ok":true}')
Return .T.

Consulta com paginação server-side

O erro mais comum é carregar todos os registros de uma vez. Use ROW_NUMBER() para paginar no banco, ChangeQuery() + TCQUERY para executar e TCSetField() para converter tipos. Nunca use SELECT * em produção: liste as colunas explicitamente.

WsMethod GET — paginação com TopConn
WsMethod GET WsReceive page, limit WsService Pedidos
  Local nPage    := 1
  Local nLimit   := 20
  Local cQuery   := ""
  Local aPedidos := 
  Local oItem    := Nil

  // Validação de parâmetros (WsData vem da query string; Nil se ausente)
  If Self:page != Nil
    nPage := Self:page
  EndIf
  If Self:limit != Nil .And. Self:limit >= 1 .And. Self:limit <= 100
    nLimit := Self:limit
  EndIf
  If nPage < 1
    nPage := 1
  EndIf

  // Query paginada — colunas explícitas, sem SELECT *
  cQuery := "SELECT C5_NUM, C5_CLIENTE, C5_LOJACLI, C5_EMISSAO, C5_TIPO,"
  cQuery += " ROW_NUMBER() OVER(ORDER BY C5_NUM) AS RN"
  cQuery += " FROM " + RetSqlName("SC5") + " SC5"
  cQuery += " WHERE SC5.C5_FILIAL = '" + xFilial("SC5") + "'"
  cQuery += " AND SC5.D_E_L_E_T_ = ' '"
  cQuery += " AND SC5.C5_EMISSAO BETWEEN '20260801' AND '20260831'"
  cQuery += " %nolock%"

  cQuery := ChangeQuery(cQuery)
  TCQUERY cQuery NEW ALIAS "QPED"

  // C5_EMISSAO chega como CHAR(8) — converte para data
  TCSetField("QPED", "C5_EMISSAO", "D")

  QPED->(dbGoTop())
  While QPED->(!Eof())
    oItem := JsonObject():New()
    oItem["numero"]  := QPED->C5_NUM
    oItem["cliente"] := QPED->C5_CLIENTE + "-" + QPED->C5_LOJACLI
    oItem["emissao"] := DToS(QPED->C5_EMISSAO)
    oItem["tipo"]    := QPED->C5_TIPO
    aAdd(aPedidos, oItem)
    QPED->(dbSkip())
  End

  QPED->(dbCloseArea())

  ::SetContentType("application/json")
  ::SetResponse(FWJsonSerialize(aPedidos))
Return .T.

Atenção aos campos: os campos C5_TOTAL e C5_SITUAC usados na versão anterior deste artigo não existem no SC5 padrão — o valor do pedido é calculado nos itens (SC6, C6_VALOR). Antes de publicar uma API, valide os campos físicos no dicionário (SX3) do seu ambiente; nunca presuma campo "padrão" sem conferir.

Autenticação: validação de token

O token Bearer deve ser validado antes de cada operação. Falha de autenticação usa SetRestFault(401, ...) com retorno .F. — o framework monta a resposta HTTP correta. O log de auditoria vai por FWLogMsg() (padrão CA1004), nunca no console. E MsSeek() exige DbSelectArea + DbSetOrder corretos antes da busca.

ValidarToken — Autenticação Bearer
// No início do WsMethod GET:
// If !ValToken(::GetHeader("Authorization"))
//   Return .F.  // framework responde 401 via SetRestFault
// EndIf

Static Function ValToken(cAuth)
  Local lRet   := .F.
  Local cToken := ""

  // Extrai o token do header "Bearer xxx"
  If Upper(Left(cAuth, 7)) == "BEARER "
    cToken := SubStr(cAuth, 8)
  EndIf

  If !Empty(cToken)
    // Valida token na tabela customizada ZA0 — área e ordem corretas
    DbSelectArea("ZA0")
    ZA0->(DbSetOrder(1)) // ZA0_FILIAL + ZA0_TOKEN

    If ZA0->(MsSeek(xFilial("ZA0") + cToken))
      // Verifica expiração
      If ZA0->ZA0_EXP >= Date()
        lRet := .T.
        // Auditoria estruturada via FWLogMsg (CA1004).
        // Adapte a assinatura à sua release — ver TDN.
        FWLogMsg("INFO", "", "APIPedidos", "", "", "", "Token validado: " + ZA0->ZA0_USER)
      EndIf
    EndIf
  EndIf

  If !lRet
    SetRestFault(401, "Token ausente, invalido ou expirado")
  EndIf

Return lRet

Expiração com hora: o exemplo compara por data (ZA0_EXP >= Date()), o que faz o token valer até o fim do dia. Para expiração com hora, use campo datetime e compare com FWTimeStamp.

CORS: o problema que ninguém avisa

Se o frontend (Angular/PO-UI) roda em outro domínio ou porta, o navegador bloqueia as requisições por política de CORS. No Protheus, o CORS é configurado no AppServer, na seção [HTTPURI] do AppServer.ini:

AppServer.ini — Configuração CORS
; No AppServer.ini, na seção [HTTPURI]
[HTTPURI]
URL=/rest10
CORSEnable=1
AllowOrigin=*

Em produção, restrinja AllowOrigin ao domínio do front. Se houver Apache/Nginx na frente do AppServer como proxy reverso, o CORS passa a ser configurado no proxy — não no AppServer sozinho.

Se não configurar CORS, o PO-UI no navegador recebe erro 403 ou é bloqueado por "blocked by CORS policy". É o erro nº 1 em integrações Protheus + Angular.

Performance: o vilão silencioso

  • N+1 de consultas: busque o conjunto em um SQL só, evite DbSeek em loop.
  • Serialização: monte o JSON com FWJsonSerialize() ou JsonObject() sem objetos desnecessários.
  • Paginação server-side: use ROW_NUMBER() — nunca carregue 10k registros no AppServer.
  • Timeout: defina tempo máximo de resposta (ex: 30s) e retorne erro 504 se exceder.
  • Teste sob carga: uma API lenta num pico de e-commerce derruba o AppServer inteiro.

Em produção

Além de CORS e autenticação, três itens são obrigatórios:

  • Idempotência para POST: o mesmo pedido enviado duas vezes não pode gerar duplicidade. Use header X-Idempotency-Key ou campo único na tabela.
  • Logs estruturados: registre timestamp, método, URL, status HTTP, tempo de resposta e usuário. Exemplo: [2026-09-07 14:32:10] GET /api/v1/pedidos 200 230ms user=edu
  • Monitoramento: alertas quando taxa de erro ultrapassa 5% ou latência média passa de 2s.

Esses três itens separam uma integração confiável de uma bomba-relógio.

Precisa de uma API Protheus bem construída?

Falar com a ELP