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
codigoemensagem, 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 — 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 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.
// 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:
; 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
SQLsó, eviteDbSeekem loop. - Serialização: monte o JSON com
FWJsonSerialize()ouJsonObject()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-Keyou 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