PHIZCHAT← RFCsRFC PHZ-0001Em revisãoIn review评审中
v1.4

Histórico de versõesVersion history版本历史

  • v1.6atualcurrent当前
    Modelo de identidades conforme o time técnico da PHIZ: Merchant ID gera o Developer ID; sob ele ficam as aplicações (App ID) e, em cada aplicação, os Business Accounts (BA ID). O sender_id passa a ser mapeado para o BA ID, e não mais o merchant_id; canais continuam como remetente opcional. Envio pelo Open ID do usuário por aplicação (telefone convertido em Open ID). Definição dos tipos de conversa (conta pública › serviço empresarial › Business Account). Mídia: formatos e tamanhos definidos pela RFC, upload na primeira fase e URLs de terceiros depois, por cliente. Q2, Q3 e Q5 respondidas; Q4, Q8 e Q10 respondidas em parte; Q6 reescrita. OpenAPI, exemplos e telas da Central atualizados (nova tela Business Accounts).Identity model from the PHIZ technical team: the Merchant ID creates the Developer ID; under it live the applications (App ID) and, in each application, the Business Accounts (BA ID). The sender_id is now mapped to the BA ID instead of the merchant_id; channels remain an optional sender. Sending by the user’s per-application Open ID (phone numbers converted to Open ID). Conversation type definitions (public account › business service › Business Account). Media: formats and sizes set by the RFC, upload in the first phase and third-party URLs later, per customer. Q2, Q3 and Q5 answered; Q4, Q8 and Q10 partly answered; Q6 rewritten. OpenAPI, examples and Developer Center screens updated (new Business Accounts screen).按 PHIZ 技术团队建议调整身份模型:Merchant ID 同步生成 Developer ID;其下为应用(App ID),每个应用下为 Business Account(BA ID)。sender_id 改为映射 BA ID,不再使用 merchant_id;频道仍为可选发送方。按应用的用户 Open ID 发送(手机号换取 Open ID)。明确会话类型(公众号会话 › 企业服务会话 › Business Account)。媒体:格式和大小由 RFC 定义,第一阶段上传,之后按客户开放第三方 URL。Q2、Q3、Q5 已答复;Q4、Q8、Q10 部分答复;重写 Q6。更新 OpenAPI、示例和开发者中心页面(新增 Business Accounts 页面)。
  • v1.5
    Seção 12 renomeada para “Central do Desenvolvedor PHIZ” e migrada para a ferramenta existente: novas telas (painel, nova aplicação Business Messaging, detalhes com scopes, remetentes e segredos, segredo gerado, assinaturas de eventos, webhook, remetentes e aplicações legadas da migração) e o que muda em cada menu, no visual da Central e em PT/EN/ZH. Integração vira aplicação Business Messaging e as permissões viram os papéis da organização. Webhook: a Fase 1 passa a usar Assinaturas de eventos em autoatendimento, no lugar do cadastro manual, e as retentativas, dead letters e reenvio da plataforma são reaproveitados (seção 10). Ajustes nas seções 01, 06, 08, 13 e 15. Q9 reescrita e nova Q10.Section 12 renamed “PHIZ Developer Center” and moved into the existing tool: new screens (dashboard, new Business Messaging application, details with scopes, senders and secrets, generated secret, event subscriptions, webhook, senders and legacy applications from the migration) and what changes in each menu, in the Developer Center look and in PT/EN/ZH. An integration becomes a Business Messaging application and permissions become the organization roles. Webhook: Phase 1 now uses self-service Event subscriptions instead of manual registration, and the platform’s retries, dead letters and replay are reused (section 10). Adjustments to sections 01, 06, 08, 13 and 15. Q9 rewritten and new Q10.第 12 节更名为“PHIZ开发者中心”并迁移至现有工具:按开发者中心的视觉风格重新绘制页面(仪表盘、新建 Business Messaging 应用、含范围/发送方/密钥的应用详情、新密钥、事件订阅、Webhook、发送方及迁移生成的 Legacy 应用),并说明各菜单的变化,提供 PT/EN/ZH 三种语言。集成改为 Business Messaging 应用,权限改为组织角色。Webhook:第 1 阶段改用自助的事件订阅,取代手工登记,并沿用平台的重试、死信和重放(第 10 节)。调整第 01、06、08、13、15 节。重写 Q9,新增 Q10。
  • v1.4
    Webhook na Fase 1: a URL é cadastrada manualmente pela PHIZ, a partir da relação comercial; a configuração pela Área do Desenvolvedor e pela API fica para a Fase 2. Retentativas com backoff, reenvio manual e status failing deixam de ser requisitos da Fase 1 e passam para a Fase 2, conforme comentários de Henry Wu na seção 10. Nova Q9.Webhook in Phase 1: the URL is registered manually by PHIZ, based on the business relationship; setup through the Developer area and the API moves to Phase 2. Retries with backoff, manual replay and the failing status are no longer Phase 1 requirements and move to Phase 2, following Henry Wu's comments on section 10. New Q9.第 1 阶段的 Webhook:URL 由 PHIZ 依托商务关系手工登记;通过开发者中心和 API 配置移至第 2 阶段。带退避的重试、手动重放和 failing 状态不再是第 1 阶段的要求,移至第 2 阶段,依据 Henry Wu 在第 10 节的评论。新增 Q9。
  • v1.3
    Mídia só por upload na PHIZ, com armazenamento temporário: novo endpoint POST /v1/{sender_id}/media (e GET/DELETE /v1/media/{media_id}), envio por id ou link PHIZ e links de terceiros recusados na Fase 1, conforme comentário de Henry Wu na seção 09. Novos erros 2302–2305 e Q8 atualizada.Media only by upload to PHIZ, with temporary storage: new POST /v1/{sender_id}/media endpoint (plus GET/DELETE /v1/media/{media_id}), sending by id or PHIZ link, and third-party links rejected in Phase 1, following Henry Wu's comment on section 09. New errors 2302–2305 and updated Q8.媒体只能上传至 PHIZ 并临时存储:新增 POST /v1/{sender_id}/media 接口(以及 GET/DELETE /v1/media/{media_id}),发送时使用 id 或 PHIZ 链接,第 1 阶段拒绝第三方链接,依据 Henry Wu 在第 09 节的评论。新增错误码 2302–2305,并更新 Q8。
  • v1.2
    Nova seção 04 com o opt-in no PhizChat: aceite dentro do app (fluxo P) e opt-in coletado pela empresa fora do PhizChat (fluxo Q), com as telas, a comparação entre os dois e a proposta de registro de consentimento (modelo A × B).New section 04 on opt-in in PhizChat: acceptance inside the app (flow P) and opt-in collected by the business outside PhizChat (flow Q), with screens, a comparison of both and the consent-registry proposal (model A vs B).新增第 04 节:PhizChat 中的 opt-in——应用内接受(流程 P)以及企业在 PhizChat 之外收集的 opt-in(流程 Q),包含界面、两者对比以及同意登记方案(模式 A 与 B)。
  • v1.1
    Nova seção de referência com a experiência do usuário no WhatsApp (cadastro, opt-in, recebimento e opções do usuário) e política de consentimento (opt-in/opt-out, LGPD).New reference section on the WhatsApp user experience (sign-up, opt-in, receiving and user options) and a consent policy (opt-in/opt-out, LGPD).新增 WhatsApp 用户体验参考章节(注册、opt-in、接收消息与用户选项),以及同意政策(opt-in/opt-out、LGPD)。
  • v1.0
    Primeira versão.First version.首个版本。
Você está lendo a versão 1.4, arquivada e somente leitura. A versão atual é a 1.6.You are reading version 1.4, archived and read-only. The current version is 1.6.你正在阅读已存档的只读版本 1.4。当前版本为 1.6。 Ver versão atualView current version查看当前版本
RFC PHZ-0001 · Rascunho para revisão

PHIZ Business Messaging API

Autenticação por conta de comerciante, envio de mensagens para telefone ou PHIZ ID e webhooks de mensagens e status. Inspirada na Cloud API da Meta e pensada como base da futura Marketing API.

Status
Rascunho
Autor
Gleison Silva - Soluções de Produto
Data
15/10/2026
Revisores
Pedro Bettani (Brasil)
Vitor Mozer (Brasil)
Richard Liang (China)
Versão da API
v1 (proposta)

01Resumo

Este documento propõe uma API pública para que sistemas externos (Blip, Salesforce, CRMs ou sistemas próprios do comerciante) conversem com usuários PHIZ em nome de um comerciante. A proposta tem quatro partes:

  • Autenticação OAuth 2.0 (client credentials) com credenciais emitidas para a conta de comerciante, não mais para um canal.
  • Envio de mensagens para um número de telefone (E.164) ou um PHIZ ID, diretamente em nome do comerciante. Usar um canal como remetente é opcional.
  • Webhooks assinados que entregam as mensagens recebidas dos usuários e cada mudança de status das mensagens enviadas.
  • Uma Área do Desenvolvedor no Open Platform, no nível do comerciante, onde ele cadastra cada sistema externo que vai usar a API (por exemplo, Blip ou Salesforce) como uma integração. Cada integração tem client_id e secret próprios, webhook e histórico de entregas. Integrações não têm relação com Miniapps.

Os contratos de mensagem e webhook reproduzem a estrutura da Cloud API da Meta, trocando apenas os nomes do domínio. Quem já integra com o WhatsApp Business Platform adapta a integração mudando URLs, credenciais e alguns nomes de campo.

02Contexto: como funciona hoje

Levantamento feito na documentação atual (Apifox, módulo Authentication) e no Open Platform (open.phiz.chat).

Autenticação atual

O token é obtido em POST /gateway/openapi/auth com app_id, app_secret, description (obrigatório) e grant_type: "user_credentials". A resposta traz um access token de cerca de 2 horas e um refresh token de cerca de 90 dias, renovado em POST /gateway/openapi/token/refresh.

HTTP
POST /gateway/openapi/auth
Content-Type: application/json

{
  "app_id": "69c37cb1821121af34201f48",
  "app_secret": "94B18B920D19463C7264BD0BDE30C31…",
  "description": "push news",
  "grant_type": "user_credentials",
  "device_fingerprint": ""
}

200 OK
{
  "code": 200,
  "msg": "success",
  "data": {
    "scope": "channel",
    "status": "active",
    "access_token": "at_1770343926041_1Y3iz6XN…",
    "token_type": "Bearer",
    "expires_in": 7199,
    "refresh_token": "rt_1770343926041_mxc7RV9…",
    "access_token_expires_at": "2026-02-06T04:…",
    "refresh_token_expires_at": "2026-05-07T02:…",
    "created_at": "2026-02-05T10:15:23.672+00:00"
  },
  "success": true
}

Onde ficam as credenciais

No Open Platform, cada canal tem a página Canal › Configuração de Desenvolvimento com AppID e AppSecret. O secret não é exibido e só pode ser redefinido. O canal também oferece, em Robô de IA, o modo de resposta “Phiz Business API”, que encaminha as conversas do canal para envio e callbacks de webhook da Business Message API.

No nível do comerciante já existem o ID do Comerciante (UUID), Gerenciamento da equipe com funções, membros e registro de operações. Esses três elementos servem de base para a nova área.

O que muda com esta RFC

Cada linha mostra como funciona hoje, por que isso atrapalha, o que esta proposta coloca no lugar e um caso de uso concreto.

Como é hojePor que atrapalhaCom esta RFCCaso de uso
Para falar com o usuário pela API, o comerciante precisa de um canal.Quem não tem canal, ou quer falar como a própria empresa, não consegue mandar mensagem direta ao usuário.O comerciante manda mensagem direta, como ele mesmo. Usar o canal passa a ser uma opção.Uma farmácia sem canal avisa a cliente Maria, pelo telefone dela, que o pedido está pronto para retirada. A mensagem chega numa conversa direta com a farmácia.
Cada canal tem seu próprio AppID e secret.Um comerciante com 5 canais administra 5 credenciais, e a Blip precisa de um token por canal.Uma credencial por integração, na conta do comerciante, com acesso ao envio direto e aos canais escolhidos.A Loja Exemplo tem 3 canais e usa a Blip. Hoje cadastra 3 AppIDs na Blip; com a RFC, cria uma integração “Blip” e marca envio direto e os 3 canais.
“Redefinir” invalida o secret na hora.A integração fica fora do ar até alguém atualizar o secret no sistema externo.Dois secrets válidos ao mesmo tempo: o novo entra e o antigo continua valendo por um período (padrão 24 h).O secret do Salesforce apareceu num log. O time gera um novo, atualiza o Salesforce à tarde e revoga o antigo no fim do dia, sem nenhuma mensagem perdida.
O login usa grant_type: user_credentials e exige o campo description.Ferramentas que já falam OAuth, como o Salesforce, não conseguem se conectar sem código sob medida.OAuth 2.0 padrão (client_credentials), que essas ferramentas suportam de fábrica.O administrador do Salesforce conecta a PHIZ em Named Credentials preenchendo só a URL do token, o client_id e o secret, sem desenvolvedor.
Existe um refresh token válido por cerca de 90 dias.É mais um segredo de longa duração para guardar, sem ganho numa integração entre servidores.Token de 1 h, sem refresh token; quando expira, o sistema pede outro.A Blip renova o token sozinha a cada hora com o mesmo client_id e secret. Não existe refresh token que possa vencer num fim de semana e derrubar o atendimento.
Não há tela para webhook, logs ou várias integrações.O comerciante não consegue configurar o webhook nem descobrir sozinho por que uma integração parou.Área do Desenvolvedor com integrações separadas, webhook, histórico de entregas e logs.As respostas dos clientes param de chegar na Blip. O comerciante abre o histórico de entregas, vê que o webhook responde erro 500 desde as 10h e, depois da correção, reenvia os eventos.

03Referência: a experiência no WhatsApp

Esta RFC replica no PHIZ o modelo da WhatsApp Business Platform. Para que todos (Brasil e China) tenham a mesma imagem do que estamos copiando, esta seção mostra a jornada de um usuário brasileiro no WhatsApp: como cria a conta, como autoriza uma empresa, como a mensagem chega e o que pode fazer com ela. Exemplo: comerciante PicPay, usuária Ana Souza, em São Paulo.

Sobre as telas

São ilustrações, não a interface oficial do WhatsApp. O texto está em português, como o usuário vê no app. Os códigos (A2, O1, C3…) seguem o canvas de Design que deu origem a esta seção. Clique em uma imagem para abrir em tamanho real.

Caminho da mensagem

A empresa não fala direto com o WhatsApp: o sistema dela (CRM, atendimento) chama um BSP (Blip, Zenvia, Twilio, Infobip), que chama a Cloud API da Meta, que entrega no celular. Respostas, cliques e status voltam por webhook pelo mesmo caminho. No PHIZ, a PHIZ Business Messaging API ocupa o lugar da Cloud API e a conta de comerciante (merchant_id) ocupa o lugar da WABA (ver a equivalência na seção 06).

Empresa → BSP → Meta Cloud API → WhatsApp → usuário, com o opt-in como pré-requisito e o caminho de volta por webhook

B01. Empresa → BSP → Meta Cloud API → WhatsApp → usuário, com o opt-in como pré-requisito e o caminho de volta por webhook

Cadastro do usuário

No WhatsApp, a conta é o número de telefone, verificado por SMS. Não existe pedido de amizade: quem tem o número consegue escrever. Empresas e família ficam na mesma caixa de entrada, por isso o controle sobre mensagens de empresas é rígido. Desde 2026 o WhatsApp oferece nomes de usuário e identificadores por empresa (BSUID) no lugar do telefone, o que conversa com o uso do PHIZ ID nesta RFC.

Número de telefone = identidade da conta

A02. Número de telefone = identidade da conta

Uma única caixa de entrada para tudo

A10. Uma única caixa de entrada para tudo

Autorização do usuário (opt-in)

Antes de uma empresa iniciar uma conversa, o usuário precisa ter autorizado aquela empresa. Há quatro formas comuns: no app da empresa (O1), no cadastro ou checkout (O2), quando o próprio usuário escreve primeiro, o que vale só para aquele atendimento (O3), e respondendo “sim” dentro do chat (O4). A boa prática é separar por finalidade: autorizar alertas da conta não autoriza ofertas.

Ciclo do consentimento: coletar, registrar, enviar dentro do escopo, revogar

O00. Ciclo do consentimento: coletar, registrar, enviar dentro do escopo, revogar

Opt-in no app do comerciante, separado por finalidade

O01. Opt-in no app do comerciante, separado por finalidade

Opt-in no cadastro, com caixas desmarcadas

O02. Opt-in no cadastro, com caixas desmarcadas

O usuário escreve primeiro: autoriza só o atendimento

O03. O usuário escreve primeiro: autoriza só o atendimento

Opt-in pedido dentro do chat, com botões

O04. Opt-in pedido dentro do chat, com botões

A Meta tem a política, mas não valida o opt-in. Não há campo nem endpoint para comprovar a autorização; a Meta confia na empresa e só reage depois, por bloqueios, denúncias e nota de qualidade. Já o opt-out é aplicado pela plataforma: bloqueio, “parar ofertas” e limite de marketing fazem o envio falhar. Mesmo assim, pela LGPD cabe à empresa provar o consentimento e respeitar a revogação, então o registro (quem, comerciante, categoria, origem, data, texto exibido, revogação) é obrigatório na prática.

Registro de consentimento do lado do comerciante

O05. Registro de consentimento do lado do comerciante

A Meta não verifica o opt-in; o opt-out é aplicado pela plataforma; por que registrar mesmo assim

O06. A Meta não verifica o opt-in; o opt-out é aplicado pela plataforma; por que registrar mesmo assim

O que o usuário recebe

Mensagens iniciadas pela empresa usam templates aprovados, em quatro categorias: utilidade (pagamentos, contas), marketing, autenticação (códigos) e serviço. Cada categoria depende do opt-in correspondente.

Notificação igual à de um contato pessoal

C01. Notificação igual à de um contato pessoal

Lista de conversas: conta comercial verificada, não lida e no topo, ao lado de família e amigos

C02. Lista de conversas: conta comercial verificada, não lida e no topo, ao lado de família e amigos

Utilidade: Pix recebido, com botões

C03. Utilidade: Pix recebido, com botões

Marketing: exige opt-in próprio para ofertas

C04. Marketing: exige opt-in próprio para ofertas

Autenticação: código com botão de copiar

C05. Autenticação: código com botão de copiar

Fatura com código Pix copia e cola

C06. Fatura com código Pix copia e cola

O que o usuário pode fazer

O usuário responde (texto, botões, lista, formulário), consulta o perfil da empresa ou retira a autorização. Cada ação gera, ou não, um evento para a empresa.

Todas as opções do usuário e o que a empresa recebe de volta

D00. Todas as opções do usuário e o que a empresa recebe de volta

Resposta rápida por botão

D02. Resposta rápida por botão

Mensagem de lista

D04. Mensagem de lista

Formulário dentro do chat (Flows), próximo de um mini app

D05. Formulário dentro do chat (Flows), próximo de um mini app

Parar ofertas e manter alertas

D07. Parar ofertas e manter alertas

Bloquear e denunciar, com motivo

D08. Bloquear e denunciar, com motivo

Opt-out por palavra-chave, tratado pelo comerciante

D10. Opt-out por palavra-chave, tratado pelo comerciante

Ação do usuárioNo WhatsAppNo PHIZ
Responde com texto ou mídiaWebhook messages; abre janela de 24 hEvento message (seção 10)
Toca em botão ou listabutton_reply / list_replyEvento message com o id escolhido
Para ofertasEnvios de marketing falhamProposta: a plataforma bloqueia a categoria para aquele merchant_id
Bloqueia ou denunciaTodos os envios falham; nota de qualidade caiProposta: erro no envio e sinal de qualidade (ver Q3)
Silencia ou arquivaInvisível para a empresaIgual

O que levamos para o PHIZ

  • A identidade da empresa em todo o fluxo é o ID do Comerciante (merchant_id): é ele que recebe o opt-in, o bloqueio e a nota de qualidade. Não existe um ID de desenvolvedor separado; integrações e apps apenas agem em nome do comerciante.
  • Consentimento por comerciante e por categoria, com opt-out aplicado pela plataforma (ver Política de envio, seção 09).
  • Sinais do usuário (bloquear, denunciar, parar ofertas) alimentando limites de envio do comerciante.
  • Formulários dentro do chat (Flows) como referência para mini apps.

04Opt-in no PhizChat: no app e na empresa

A seção 03 mostrou que no WhatsApp o opt-in quase sempre acontece fora do app e que a Meta não o valida. Esta seção propõe como o usuário autoriza uma conta business no PhizChat, por dois caminhos que terminam nos mesmos controles de opt-out:

  • Fluxo P — dentro do PhizChat: o usuário aceita a conta business no próprio app. A plataforma guarda a prova.
  • Fluxo Q — na empresa, fora do PhizChat: o usuário autorizou no app, site ou loja do comerciante, e o comerciante inicia a conversa.
Sobre as telas

São propostas ilustrativas, com o PicPay como comerciante de exemplo. Os códigos (P0…P8, Q0…Q5) seguem o canvas de Design. Clique em uma imagem para abrir em tamanho real.

Fluxo P · Opt-in dentro do PhizChat

O usuário chega à conta business por link de SMS ou e-mail, QR code, app ou site do comerciante, ou pela busca no app. Se não tem o Phiz, vê uma página H5 só com o perfil e o download; o aceite nunca acontece fora do app. Depois de instalar e entrar, um deep link diferido o leva ao mesmo chat, que abre bloqueado até o aceite. Ao aceitar, o PhizChat registra o consentimento (usuário, merchant_id, data, ponto de entrada), envia o template de boas-vindas, se houver, e libera o chat.

Mapa do fluxo P: entradas, instalação, login, aceite e depois

P00. Mapa do fluxo P: entradas, instalação, login, aceite e depois

Sem o app: página H5 com perfil e download, sem botão Aceitar

P01. Sem o app: página H5 com perfil e download, sem botão Aceitar

Chat da conta business com o cartão de consentimento; campo bloqueado até o aceite

P02. Chat da conta business com o cartão de consentimento; campo bloqueado até o aceite

Depois do aceite: boas-vindas e chat liberado

P03. Depois do aceite: boas-vindas e chat liberado

Perfil da empresa antes do consentimento, também com Aceitar

P04. Perfil da empresa antes do consentimento, também com Aceitar

Janela de confirmação do consentimento

P05. Janela de confirmação do consentimento

Depois do aceite, o perfil mostra o status e a data do consentimento. O usuário pode revogar (o chat volta ao estado bloqueado e pode ser autorizado de novo) ou bloquear (reaproveita o bloqueio atual; a API continua aceitando chamadas, mas devolve o status bloqueado).

Perfil depois do consentimento: status, data e Revogar consentimento

P06. Perfil depois do consentimento: status, data e Revogar consentimento

Confirmação da revogação; o chat volta a ficar bloqueado

P07. Confirmação da revogação; o chat volta a ficar bloqueado

Conta business bloqueada: a API devolve o status bloqueado

P08. Conta business bloqueada: a API devolve o status bloqueado

Fluxo Q · Opt-in coletado pela empresa, fora do PhizChat

A maioria das empresas brasileiras já coleta consentimento nos próprios canais. No fluxo Q, o usuário ativa “mensagens no PhizChat” no app do PicPay; o PicPay guarda a prova e envia o template pela API. A API confere se o destinatário é usuário do Phiz e se não bloqueou nem revogou; no modelo B, confere também se há consentimento registrado para aquela categoria. No app, a primeira mensagem chega com um cartão de origem do consentimento (o que foi autorizado, onde e quando), e o usuário pode continuar recebendo, parar ou denunciar.

Mapa do fluxo Q: lado da empresa, verificações da API e lado do usuário, com a comparação P × Q

Q00. Mapa do fluxo Q: lado da empresa, verificações da API e lado do usuário, com a comparação P × Q

Opt-in no app do PicPay, citando o PhizChat

Q01. Opt-in no app do PicPay, citando o PhizChat

Primeira mensagem na lista, com a etiqueta Nova empresa

Q02. Primeira mensagem na lista, com a etiqueta Nova empresa

Cartão com a origem do consentimento: continuar, parar ou denunciar

Q03. Cartão com a origem do consentimento: continuar, parar ou denunciar

Status do consentimento por categoria, com origem e data

Q04. Status do consentimento por categoria, com origem e data

Modelo B: registro do consentimento na API e erros de envio (proposta ilustrativa)

Q05. Modelo B: registro do consentimento na API e erros de envio (proposta ilustrativa)

P × Q

P · opt-in no PhizChatQ · opt-in na empresa
Quem coletaPhizChat (Aceitar)O comerciante (app, site, loja)
Onde fica a provaPhizChatCRM do comerciante (modelo A) · também no PHIZ (modelo B)
Primeira conversaBloqueada até o aceiteAbre com a mensagem e a origem do consentimento
Opt-outIgual nos dois: revogar, bloquear, denunciar — aplicado pelo PhizChat

Decisões em aberto

  • Modelo A × B no fluxo Q (ver Q1): só a política, como a Meta, ou registro de consentimento no PHIZ conferido a cada envio. O cartão de origem (Q3) e o status por categoria (Q4) completos dependem do modelo B.
  • Entrega antes do aceite no fluxo Q: a proposta entrega a primeira mensagem com o cartão de origem; a alternativa é bloquear o chat até o aceite, como no fluxo P.
  • Categorias de consentimento (alertas da conta, ofertas, códigos) e o que o app mostra ao usuário.
  • Nomes técnicos — /v1/merchants/{merchant_id}/consents, o webhook consent_update e os códigos de erro — são provisórios, para discussão.

05Objetivos e não-objetivos

Objetivos

  • Credenciais ligadas à conta de comerciante, e não mais a cada canal. O comerciante cria uma integração para cada sistema externo (por exemplo, uma para a Blip e outra para o Salesforce). Cada integração recebe seu próprio client_id e secret, e o comerciante define o que ela pode fazer (enviar mensagens, consultar status, configurar webhook) e por quais remetentes pode falar: o próprio comerciante e, se ele quiser, seus canais.
  • OAuth 2.0 padrão (client credentials), compatível com bibliotecas e plataformas de mercado.
  • Enviar mensagens para telefone ou PHIZ ID e receber respostas e status por webhook assinado.
  • Compatibilidade estrutural com a Cloud API da Meta, para que integrações existentes migrem com pouco código.
  • Publicar a especificação da API em formato OpenAPI 3.1 (o padrão do Swagger). É um arquivo que descreve todas as rotas, campos, respostas e webhooks, e funciona como acordo entre o time que constrói a API e quem integra com ela. Com ele, parceiros geram documentação e código e testam antes da entrega.

Fora do escopo desta RFC

  • Templates, campanhas e segmentação (Marketing API, fases futuras).
  • Preço e cobrança por conversa.
  • Grupos, chamadas de voz e vídeo.
  • SDKs oficiais (podem vir depois, gerados a partir do OpenAPI).

06Modelo de domínio

A conta de comerciante passa a ser a raiz de tudo. O comerciante fala com o usuário PhizChat de duas formas:

  • Mensagem direta (padrão). O comerciante envia como ele mesmo, com o nome e a foto de Informações do comerciante. Não precisa ter canal.
  • Pelo canal (opcional). Quem tem canais pode enviar por eles, por exemplo para separar marcas ou áreas.

As integrações pertencem ao comerciante e recebem permissão para usar uma das formas ou as duas.

flowchart LR
  M["Conta de comerciante
merchant_id"] --> I["Integrações
Blip · Salesforce · sistema próprio
client_id + secret cada"] I -- "POST /v1/{sender_id}/messages" --> API["PHIZ Business API"] API -- "Forma 1 · padrão
sender_id = merchant_id" --> D["Mensagem direta
do comerciante"] API -. "Forma 2 · opcional
sender_id = ID do canal" .-> C["Mensagem pelo
canal do comerciante"] D --> U(("Usuário PhizChat
telefone ou PHIZ ID")) C --> U API -- "webhook: respostas e status" --> I

As duas formas usam a mesma rota e o mesmo formato; muda apenas o sender_id. A forma 2 só existe para comerciantes que têm canal.

Fluxo ponta a ponta

Visão para o time técnico: por onde a mensagem passa, da empresa até o usuário, e por onde voltam as respostas e os status.

flowchart LR
  PP["PicPay"] --> B1["BSP
ex.: Blip"] AM["Ambar"] --> B2["BSP
ex.: Salesforce"] B1 --> API["PHIZ Business API
/gateway/business
Auth · Messages · Webhooks"] B2 --> API API -- "1–3 · token, envio e
validação do remetente" --> MA subgraph OP["Open Platform"] MA["Merchant Account"] MA -- "4a · sender_id = merchant_id
padrão" --> DM["Direct Message"] MA -. "4b · sender_id = ID do canal
opcional" .-> CH["Channel"] CH --> CM["Channel Message"] end DM -- "5 · entrega" --> APP["Phiz App"] CM -- "5 · entrega" --> APP APP --> U(("Usuário"))

Ida: da empresa até o usuário. A linha tracejada é o caminho opcional pelo canal.

Retorno: respostas e status

flowchart LR
  U(("Usuário")) -- "responde, recebe, lê" --> APP["Phiz App"]
  APP -- "6 · eventos" --> WH["PHIZ Business API
Webhooks"] WH -- "webhook assinado" --> BSP["BSP da integração
responsável pelo remetente"] BSP --> EMP["Empresa"]

Volta: respostas do usuário e status das mensagens chegam ao BSP pelo webhook.

  1. Integração. A empresa (por exemplo, PicPay ou Ambar) usa um BSP para o atendimento. Na Área do Desenvolvedor, o comerciante cria uma integração para esse BSP e entrega o client_id e o secret.
  2. Token. O BSP chama /oauth/token. O Auth confere a integração na Merchant Account (secret, escopos e remetentes permitidos) e devolve um JWT de 1 h.
  3. Envio. O BSP chama POST /v1/{sender_id}/messages. A API valida token, escopo, remetente, política de opt-in e Idempotency-Key, responde 202 e coloca a mensagem na fila. Para mídia, o BSP primeiro envia o arquivo para POST /v1/{sender_id}/media e usa no envio o id ou o link PHIZ devolvido.
  4. Roteamento. Com sender_id = merchant_id, a mensagem segue como Direct Message (padrão). Com o ID de um canal, segue como Channel Message (opcional).
  5. Entrega. O Phiz App exibe a mensagem ao usuário e baixa a mídia do armazenamento da PHIZ, pelo link gerado no upload.
  6. Retorno. Respostas do usuário e status (sent, delivered, read, failed) viram eventos que o serviço de Webhooks entrega, assinados, à integração responsável pelo remetente.
ComponenteResponsabilidadeSituação
BSP / sistema da empresaChama a API, envia as mídias por upload e recebe o webhook.Externo
PHIZ Business API · AuthEmite e revoga JWT; valida client_id e secret.Novo
PHIZ Business API · MessagesValida e aceita envios (202), aplica idempotência e limites, enfileira e roteia por sender_id.Novo
PHIZ Business API · MediaRecebe o upload, valida formato e tamanho, guarda o arquivo por tempo limitado e gera o id e o link PHIZ usados no envio.Novo
PHIZ Business API · WebhooksConsome eventos do app, assina e entrega com retentativas; guarda histórico de entregas por 72 h.Novo
Open Platform · Merchant AccountGuarda integrações, secrets (hash), escopos, remetentes permitidos e configuração de webhook. Hospeda a Área do Desenvolvedor.Alterado: credenciais saem do canal e vão para o comerciante
Direct MessageConversa direta entre comerciante e usuário no app.Novo (ver Q3)
Channel / Channel MessageEnvio pelo canal do comerciante.Existente
Phiz AppExibe mensagens, baixa mídia pelo link PHIZ e gera eventos de entrega, leitura e resposta.Alterado

Equivalência com a Cloud API da Meta

Meta (WhatsApp Cloud API)PHIZ (esta proposta)
WhatsApp Business Account (WABA)Conta de comerciante · merchant_id
Número comercial · phone_number_idRemetente · sender_id: o próprio comerciante (padrão, igual ao merchant_id) ou, opcionalmente, um canal (igual ao ID do canal)
App da Meta + token de System UserApp de desenvolvedor + OAuth client credentials
wa_idphiz_id
POST /{phone-number-id}/messagesPOST /v1/{sender_id}/messages
POST /{phone-number-id}/media · mídia por id ou link de terceirosPOST /v1/{sender_id}/media · mídia só por id ou link PHIZ; links de terceiros não são aceitos
object: whatsapp_business_accountobject: phiz_merchant_account
X-Hub-Signature-256X-Phiz-Signature-256 + X-Phiz-Timestamp
hub.mode / hub.verify_token / hub.challengeIguais

07Princípios de design

  • Mesmo formato da Cloud API. Corpos de envio, respostas e webhooks seguem a estrutura da Meta. Divergimos só onde há ganho claro de segurança (timestamp na assinatura, mídia só pelo armazenamento da PHIZ) ou de clareza (to_type).
  • Status HTTP, um único envelope de erro e trace_id em toda resposta de erro.
  • O envio é assíncrono e responde 202 Accepted; o resultado chega pelo webhook.
  • Idempotência. O cabeçalho Idempotency-Key evita mensagens duplicadas quando o cliente repete uma requisição.
  • Menor privilégio. Cada integração tem escopos e uma lista de remetentes permitidos.
  • Versionamento /v1. Mudanças compatíveis (campos novos) entram de maneira padronizada; mudanças incompatíveis exigem /v2.

08Autenticação

Usamos o fluxo client credentials do OAuth 2.0 (RFC 6749 §4.4). O app troca client_id e client_secret por um access token de 1 hora e o envia em Authorization: Bearer em todas as chamadas. Não há refresh token: quando o token expira, o app pede outro.

POST/oauth/tokenEmite access token
POST/oauth/revokeRevoga um token (RFC 7009)
GET/v1/meMostra integração, comerciante, escopos e remetentes do token
sequenceDiagram
  autonumber
  participant S as Sistema externo
  participant A as PHIZ Auth
  participant API as PHIZ API
  S->>A: POST /oauth/token (client_id, client_secret)
  A-->>S: 200 access_token (1 h), escopos
  S->>API: POST /v1/{sender_id}/messages · Bearer token
  API-->>S: 202 Accepted

Perto de expirar, o sistema pede outro token. Não há refresh token.

Requisição e resposta

BASH
# Base URL of this API: current PHIZ gateway + /gateway/business prefix (illustrative host)
PHIZ_API_BASE="https://gateway.phiz.chat/gateway/business"

curl -X POST "$PHIZ_API_BASE/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_id=app_2Fk81PzQ \
  -d client_secret="$PHIZ_CLIENT_SECRET" \
  -d "scope=messages:send messages:read"
JSON
{
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InBoei0yMDI2LTEwIn0…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "messages:send messages:read",
  "merchant_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57"
}

Os endpoints /oauth/* usam o formato de erro do OAuth (RFC 6749 §5.2), que as bibliotecas já entendem:

JSON
{
  "error": "invalid_client",
  "error_description": "Client authentication failed."
}

Formato do token

Decisão: o token é um JWT assinado com ES256 e chave publicada em /.well-known/jwks.json. O gateway valida o token localmente, sem consulta ao banco a cada chamada. Revogações e secrets rotacionados entram numa lista de negação de curta duração (até a expiração do token). Para o integrador o token é opaco; o conteúdo abaixo é informativo.

JSON
{
  "iss": "https://gateway.phiz.chat/gateway/business",
  "sub": "app_2Fk81PzQ",
  "aud": "phiz-business-api",
  "mid": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
  "scope": "messages:send messages:read",
  "snd": [
    "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
    "1231"
  ],
  "env": "live",
  "iat": 1760527800,
  "exp": 1760531400,
  "jti": "tok_01JA9VQ4N2"
}

Escopos

EscopoPermite
messages:sendEnviar mensagens, enviar e gerenciar mídias e marcar mensagens como lidas
messages:readConsultar status de mensagens
webhooks:manageConfigurar, testar e reenviar o webhook do próprio app (Fase 2)
account:readLer dados da conta de comerciante e dos remetentes
templates:manageReservado para a Marketing API

Secrets e rotação

  • O secret tem prefixo de ambiente (phzsk_live_, phzsk_test_), o que ajuda scanners de vazamento a reconhecê-lo.
  • É exibido uma única vez, na criação ou rotação, e guardado apenas como hash (Argon2id).
  • Cada app pode ter até dois secrets ativos. Ao gerar um novo, o anterior continua válido pelo período escolhido (padrão 24 h, de 0 a 7 dias), permitindo trocar sem queda.
  • A tela mostra o último uso de cada secret, para saber se o antigo ainda está em uso antes de revogá-lo.
  • Opcional: lista de IPs permitidos por app.

Atual × proposto

AspectoHojeProposta
Dono da credencialCanalConta de comerciante (via app)
Fluxouser_credentials (próprio)client_credentials (RFC 6749)
Campos obrigatórios extrasdescriptionNenhum
Validade~2 h + refresh token ~90 dias1 h, sem refresh token
EscoposchannelPor capacidade + remetentes permitidos
RotaçãoRedefinir invalida na horaDois secrets ativos, com sobreposição
ErrosHTTP 200 + code no corpoStatus HTTP real + envelope padrão

09Envio de mensagens

POST/v1/{sender_id}/messagesEnvia mensagem ou marca como lida
POST/v1/{sender_id}/mediaEnvia um arquivo de mídia e devolve id e link PHIZ
GET/v1/media/{media_id}Dados da mídia: link, tipo, tamanho e validade
DELETE/v1/media/{media_id}Remove a mídia antes do prazo
GET/v1/messages/{message_id}Último status conhecido (contingência)
GET/v1/merchants/{merchant_id}/sendersRemetentes (comerciante e canais) que a integração pode usar

Remetente: o comerciante ou um canal

O sender_id no caminho define em nome de quem a mensagem sai:

  • O próprio comerciante (padrão). Use o merchant_id. A mensagem aparece para o usuário numa conversa Business Account, direta com a empresa, e não depende de o comerciante ter canal.
  • Um canal (opcional). Use o ID do canal, por exemplo 1231. Útil para quem separa marcas ou áreas em canais diferentes. A mensagem aparece na conversa Channel Service do canal, a mesma usada pelo Channel Bot.

Os dois funcionam do mesmo jeito na API: mesmos campos, mesmas respostas e mesmos webhooks. Nos webhooks, metadata.sender_type indica qual foi usado e metadata.conversation_type traz o tipo de conversa (business_account ou channel_service).

Destinatário: telefone ou PHIZ ID

O campo to recebe o destinatário e to_type diz como interpretá-lo:

  • phone (padrão): telefone em E.164, por exemplo +5511987654321. A PHIZ resolve o número para a conta do usuário.
  • phiz_id: o PHIZ ID do usuário, por exemplo 839201447. É o identificador que chega nos webhooks, então respostas podem usá-lo diretamente.

Se o telefone não pertencer a uma conta PHIZ, ou o usuário tiver bloqueado o remetente, a API ainda responde 202 e o webhook informa failed com erro 2104. Assim a API não serve para descobrir quais números têm conta na PHIZ.

Exemplo: texto para telefone

BASH
# sender_id = merchant_id: direct message from the merchant, no channel needed
curl -X POST "$PHIZ_API_BASE/v1/7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57/messages" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-48213-envio" \
  -d '{
    "messaging_product": "phiz",
    "to": "+5511987654321",
    "to_type": "phone",
    "type": "text",
    "text": { "body": "Olá, Maria! Seu pedido #48213 foi enviado." },
    "biz_opaque_callback_data": "pedido=48213"
  }'
JSON
{
  "messaging_product": "phiz",
  "contacts": [
    {
      "input": "+5511987654321",
      "phiz_id": null
    }
  ],
  "messages": [
    {
      "id": "pmid.01JA9VZ6T8R3",
      "message_status": "accepted"
    }
  ]
}

contacts[].phiz_id só vem preenchido se o usuário já conversou com esse remetente. messages[].id é a chave para relacionar os webhooks de status.

Outros exemplos

O mesmo envio, saindo por um canal

BASH
# sender_id = 1231: same request, sent through the merchant's channel
curl -X POST "$PHIZ_API_BASE/v1/1231/messages" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+5511987654321", "type": "text", "text": { "body": "Novidades da semana na Loja Exemplo - Ofertas" } }'

Texto para PHIZ ID

JSON
{
  "to": "839201447",
  "to_type": "phiz_id",
  "type": "text",
  "text": {
    "body": "Seu cupom chegou: PHIZ10"
  }
}

Upload de mídia

BASH
# 1. Upload the file to PHIZ (multipart/form-data). Third-party URLs are not accepted when sending.
curl -X POST "$PHIZ_API_BASE/v1/7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57/media" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -F messaging_product=phiz \
  -F type=application/pdf \
  -F file=@nota-fiscal-48213.pdf
JSON
{
  "id": "pmed.01JB2K7Q9X",
  "link": "https://media.phiz.chat/biz/pmed.01JB2K7Q9X",
  "mime_type": "application/pdf",
  "file_size": 184320,
  "sha256": "9b1f…",
  "expires_at": "2026-11-19T14:00:00Z"
}

Áudio pelo id da mídia

JSON
{
  "to": "+5511987654321",
  "type": "audio",
  "audio": {
    "id": "pmed.01JB2K4M7A"
  }
}

Vídeo pelo link PHIZ, para PHIZ ID

JSON
{
  "to": "839201447",
  "to_type": "phiz_id",
  "type": "video",
  "video": {
    "link": "https://media.phiz.chat/biz/pmed.01JB2K5V1C",
    "caption": "Como montar seu produto"
  }
}

Documento com nome de arquivo

JSON
{
  "to": "+5511987654321",
  "type": "document",
  "document": {
    "id": "pmed.01JB2K7Q9X",
    "filename": "nota-fiscal-48213.pdf",
    "caption": "Nota fiscal do pedido #48213"
  }
}

Botões de resposta

JSON
{
  "to": "+5511987654321",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": {
      "text": "Confirma a entrega amanhã entre 9h e 12h?"
    },
    "action": {
      "buttons": [
        {
          "type": "reply",
          "reply": {
            "id": "confirmar",
            "title": "Confirmar"
          }
        },
        {
          "type": "reply",
          "reply": {
            "id": "reagendar",
            "title": "Reagendar"
          }
        }
      ]
    }
  }
}

Marcar mensagem recebida como lida

JSON
{
  "messaging_product": "phiz",
  "status": "read",
  "message_id": "pmid.IN_01JA9W2C"
}

Tipos suportados

typeConteúdoFase
textAté 4.096 caracteres; preview_url opcionalMVP
imagePor id ou link PHIZ (upload prévio); legenda opcionalMVP
audioPor id ou link PHIZ (upload prévio); sem legendaMVP
videoPor id ou link PHIZ (upload prévio); legenda opcionalMVP
documentPor id ou link PHIZ (upload prévio); legenda e nome do arquivo opcionaisMVP

Mídia: upload na PHIZ e armazenamento temporário

Toda mídia passa pelo armazenamento da PHIZ. O envio acontece em dois passos:

  1. Upload. O BSP envia o arquivo para POST /v1/{sender_id}/media (multipart/form-data). A PHIZ confere formato e tamanho, guarda o arquivo e devolve um id e um link PHIZ, com a data de validade em expires_at.
  2. Envio. Na mensagem, o objeto da mídia traz o id (recomendado) ou esse link. O app do usuário baixa o arquivo do armazenamento da PHIZ.
  • Só links gerados pela PHIZ. Na Fase 1, a API recusa URLs de terceiros (CDN do comerciante ou do parceiro) com erro 2302. Assim a PHIZ garante que o link é seguro e que o arquivo continua acessível até a entrega, e o app não expõe o IP do usuário a servidores externos.
  • Armazenamento temporário. O arquivo fica guardado só pelo tempo necessário para o envio e a entrega: proposta de 30 dias a partir do upload, como o id de mídia da Cloud API (ver Q8). Durante esse prazo, o mesmo id pode ser usado em vários envios, por exemplo o mesmo PDF para muitos clientes. Depois dele, o id e o link deixam de valer e novos envios falham com 2303. O comerciante pode apagar antes com DELETE /v1/media/{media_id}.
  • Validação no upload. Formato e tamanho são checados no upload, e não na entrega: arquivos fora da tabela abaixo são recusados na hora (2304 e 2305).
  • Escopo. Upload, consulta e remoção usam o escopo messages:send, e a mídia só pode ser usada por remetentes da mesma conta de comerciante.
Decisão (v1.3)

Na Fase 1, mídia só por upload na PHIZ, com armazenamento temporário; links de terceiros não são aceitos. Liberar URLs de terceiros para clientes específicos, por exemplo com lista de domínios permitidos, fica para uma fase futura. Combinado com Henry Wu nos comentários desta seção.

TipoFormatos aceitosTamanho máximo
imageJPEG, PNG, WebP5 MB
audioAAC, MP3, M4A, OGG (Opus), AMR16 MB
videoMP4 (H.264 + AAC), 3GP16 MB
documentPDF, DOC/DOCX, XLS/XLSX, PPT/PPTX, TXT, CSV100 MB

Formatos e tamanhos que o app PHIZ deve aceitar, inspirados nos limites da Cloud API e definidos por esta RFC. A conversão e a compressão ficam a cargo de quem envia o arquivo; o upload recusa o que estiver fora da tabela. Precisam ser confirmados com o time responsável pela mídia no app (ver Q8).

Política de envio (consentimento)

Proposta inspirada no WhatsApp (ver seção 03), com os fluxos de opt-in detalhados na seção 04:

  • Iniciar conversa exige opt-in do usuário para aquele comerciante (merchant_id) e categoria: alertas da conta, ofertas ou códigos. Responder em até 24 h depois de uma mensagem do usuário não exige opt-in prévio.
  • O opt-in pode ser coletado fora do PHIZ (app, site ou loja do comerciante) ou dentro do app PHIZ.
  • Opt-out aplicado pela plataforma: se o usuário bloqueia o comerciante ou para as ofertas, os envios daquela categoria falham com erro, sem depender do comerciante.
  • Registro obrigatório: o comerciante guarda a prova de cada consentimento (usuário, merchant_id, categoria, origem, data, texto exibido e revogação). Pela LGPD, cabe a quem trata os dados provar o consentimento e respeitar a revogação.
Decisão de Produto

A Meta tem a política de opt-in mas não a valida: confia no comerciante e reage depois, por denúncias e nota de qualidade. Precisamos escolher entre A, o mesmo modelo (política mais registro no comerciante), e B, um registro de consentimento no PHIZ, verificado antes de cada envio. Ver Q1.

Idempotência

Envie Idempotency-Key em todo envio. A mesma chave em até 24 h devolve a resposta original sem reenviar. A mesma chave com corpo diferente devolve 409. Isso torna seguros os retries após timeout ou erro 5xx.

10Webhooks

Cada app tem uma assinatura de webhook com URL, verify_token e lista de eventos.

Decisão (v1.4)

Fase 1: a URL do webhook é configurada manualmente pela PHIZ, a partir da relação comercial com o comerciante ou parceiro. A configuração pela Área do Desenvolvedor e pela API (os endpoints abaixo) entra na Fase 2, quando o Open Platform for aberto. Combinado com Henry Wu nos comentários desta seção.

Configuração manual na Fase 1

  • O comerciante ou parceiro pede o cadastro pelo contato comercial com a PHIZ e informa a URL do webhook e os eventos que quer receber (messages, message_status).
  • A PHIZ cadastra a URL no servidor e a confere com o mesmo handshake GET descrito abaixo (proposta).
  • O formato dos eventos, a assinatura e o handshake são os mesmos da Fase 2; só muda quem configura.

Configuração por API (Fase 2)

Estes endpoints só serão expostos na Fase 2, junto com a Área do Desenvolvedor.

PUT/v1/apps/{app_id}/webhookDefine URL, verify_token e eventos (Fase 2)
GET/v1/apps/{app_id}/webhookConfiguração e saúde (Fase 2)
POST/v1/apps/{app_id}/webhook/testDispara evento de teste assinado (Fase 2)
GET/v1/apps/{app_id}/webhook/deliveriesHistórico de entregas, 72 h (Fase 2)
POST/v1/apps/{app_id}/webhook/deliveries/{id}/replayReenvia uma entrega (Fase 2)
BASH
curl -X PUT "$PHIZ_API_BASE/v1/apps/app_2Fk81PzQ/webhook" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://integracao.lojaexemplo.com.br/phiz/webhook",
    "verify_token": "um-texto-que-so-voce-conhece",
    "fields": ["messages", "message_status"]
  }'

Exemplo da chamada de configuração da Fase 2.

Eventos

fieldQuando
messagesO usuário enviou mensagem, clicou num botão ou reagiu
message_statusUma mensagem enviada mudou para sent, delivered, read ou failed
sender_updateMudou o status, a qualidade ou o limite de um remetente (comerciante ou canal)
account_updateMudança na conta de comerciante (verificação, restrição)

Verificação da URL

Antes de ativar, a PHIZ faz um GET na URL com hub.mode=subscribe, hub.verify_token e hub.challenge. O servidor confere o token e responde 200 com o valor de hub.challenge. É o mesmo handshake da Meta.

Assinatura

Todo POST traz os cabeçalhos abaixo. A assinatura é um HMAC-SHA256 de {X-Phiz-Timestamp}.{corpo bruto} com o webhook secret do app, separado do client secret. Rejeite eventos com timestamp mais de 5 minutos fora do relógio. Durante a rotação do webhook secret, o cabeçalho traz duas assinaturas separadas por vírgula.

HTTP
POST /phiz/webhook HTTP/1.1
Host: integracao.lojaexemplo.com.br
Content-Type: application/json
User-Agent: PHIZ-Webhooks/1.0
X-Phiz-Delivery-Id: dlv_01JA9W2D7K
X-Phiz-Timestamp: 1760528527
X-Phiz-Signature-256: sha256=6f1c0e…
JAVASCRIPT
// Express: verification handshake + signed events
import express from "express";
import crypto from "node:crypto";

const app = express();
app.use(express.raw({ type: "application/json" })); // keep the raw body for the HMAC

app.get("/phiz/webhook", (req, res) => {
  const ok = req.query["hub.mode"] === "subscribe" &&
             req.query["hub.verify_token"] === process.env.PHIZ_VERIFY_TOKEN;
  return ok ? res.status(200).send(req.query["hub.challenge"]) : res.sendStatus(403);
});

app.post("/phiz/webhook", (req, res) => {
  const ts = Number(req.get("X-Phiz-Timestamp"));
  if (Math.abs(Date.now() / 1000 - ts) > 300) return res.sendStatus(400);

  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.PHIZ_WEBHOOK_SECRET)
    .update(`${ts}.${req.body}`)
    .digest("hex");
  const received = (req.get("X-Phiz-Signature-256") || "").split(",");
  const valid = received.some(sig => sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
  if (!valid) return res.sendStatus(401);

  queue.push({ deliveryId: req.get("X-Phiz-Delivery-Id"), event: JSON.parse(req.body) });
  res.sendStatus(200); // answer fast, process asynchronously
});
PYTHON
# Flask: signature check
import hmac, hashlib, time, os
from flask import request, abort

def verify_phiz_signature() -> None:
    ts = int(request.headers["X-Phiz-Timestamp"])
    if abs(time.time() - ts) > 300:
        abort(400)
    msg = f"{ts}.".encode() + request.get_data()  # raw bytes, before JSON parsing
    expected = "sha256=" + hmac.new(os.environ["PHIZ_WEBHOOK_SECRET"].encode(), msg, hashlib.sha256).hexdigest()
    received = request.headers.get("X-Phiz-Signature-256", "").split(",")
    if not any(hmac.compare_digest(sig, expected) for sig in received):
        abort(401)

Mensagem recebida

JSON
{
  "object": "phiz_merchant_account",
  "entry": [
    {
      "id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
      "changes": [
        {
          "field": "messages",
          "value": {
            "messaging_product": "phiz",
            "metadata": {
              "sender_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
              "sender_type": "merchant",
              "conversation_type": "business_account",
              "display_name": "Loja Exemplo"
            },
            "contacts": [
              {
                "profile": {
                  "name": "Maria Souza"
                },
                "phiz_id": "839201447",
                "phone": "+5511987654321"
              }
            ],
            "messages": [
              {
                "from": "839201447",
                "id": "pmid.IN_01JA9W2C",
                "timestamp": "1760528527",
                "type": "text",
                "text": {
                  "body": "Quero reagendar a entrega"
                },
                "context": {
                  "id": "pmid.01JA9VZ6T8R3"
                }
              }
            ]
          }
        }
      ]
    }
  ]
}

Mensagem recebida com mídia

Quando o usuário envia imagem, áudio, vídeo ou documento, o evento traz um url temporário que aponta para o arquivo no armazenamento atual do chat. A API de mensagens não faz cópia nem guarda a mídia; baixe o arquivo enquanto o link estiver válido. Abaixo, o item messages[] de uma mensagem de voz:

JSON
{
  "from": "839201447",
  "id": "pmid.IN_01JA9W7R",
  "timestamp": "1760528731",
  "type": "audio",
  "audio": {
    "url": "https://media.phiz.chat/in/9xT2…?expires=1760615131&sig=…",
    "mime_type": "audio/ogg; codecs=opus",
    "sha256": "3f9a…",
    "voice": true
  }
}

Status de mensagem enviada

JSON
{
  "object": "phiz_merchant_account",
  "entry": [
    {
      "id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
      "changes": [
        {
          "field": "message_status",
          "value": {
            "messaging_product": "phiz",
            "metadata": {
              "sender_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
              "sender_type": "merchant",
              "conversation_type": "business_account",
              "display_name": "Loja Exemplo"
            },
            "statuses": [
              {
                "id": "pmid.01JA9VZ6T8R3",
                "status": "delivered",
                "timestamp": "1760528410",
                "recipient_id": "839201447",
                "biz_opaque_callback_data": "pedido=48213",
                "conversation": {
                  "id": "conv_01JA9W",
                  "origin": {
                    "type": "service"
                  },
                  "expires_at": "1760614810"
                }
              }
            ]
          }
        }
      ]
    }
  ]
}
JSON
{
  "object": "phiz_merchant_account",
  "entry": [
    {
      "id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
      "changes": [
        {
          "field": "message_status",
          "value": {
            "messaging_product": "phiz",
            "metadata": {
              "sender_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
              "sender_type": "merchant",
              "conversation_type": "business_account",
              "display_name": "Loja Exemplo"
            },
            "statuses": [
              {
                "id": "pmid.01JA9X0QW1",
                "status": "failed",
                "timestamp": "1760528600",
                "recipient_id": "+5511900000000",
                "errors": [
                  {
                    "code": 2104,
                    "title": "recipient_not_reachable",
                    "message": "The phone number is not linked to a PHIZ account or blocked this sender.",
                    "href": "https://developers.phiz.chat/docs/errors#2104"
                  }
                ]
              }
            ]
          }
        }
      ]
    }
  ]
}
stateDiagram-v2
  direction LR
  [*] --> accepted: 202 da API
  accepted --> sent: entregue ao servidor PHIZ
  sent --> delivered: chegou ao aparelho
  delivered --> read: usuário abriu
  accepted --> failed
  sent --> failed
  read --> [*]
  failed --> [*]

Ciclo de vida do status. accepted só aparece na resposta da API e no GET /v1/messages/{id}; os demais chegam por webhook.

Garantias de entrega

Fase 1. O objetivo é colocar no ar e usar os pontos principais; as proteções abaixo entram depois.

  • Duplicados são possíveis. O mesmo evento pode chegar mais de uma vez; use X-Phiz-Delivery-Id para descartar duplicados.
  • Sem ordem garantida. Um read pode chegar antes do delivered; ordene por timestamp e nunca regrida status.
  • Responda em até 10 s com 2xx e processe depois. Qualquer outra resposta, ou timeout, conta como falha.
  • Para status de mensagens enviadas, GET /v1/messages/{message_id} serve de contingência se um webhook se perder.
Risco da Fase 1

Sem a política completa de retentativas, um evento que falhar na entrega pode não ser reenviado, e mensagens recebidas não têm consulta de contingência. O mínimo para a Fase 1 será definido pela revisão técnica (ver Q9).

Fase 2 (proteções). Mecanismos de garantia que ficam para o desenvolvimento futuro:

  • Pelo menos uma vez. Entrega com retentativas e backoff exponencial (10 s, 30 s, 1 min, 5 min, 15 min, 1 h e depois a cada hora) por até 24 h.
  • Reenvio manual. Depois das 24 h, o evento fica disponível para reenvio por 72 h, com o histórico de entregas (/webhook/deliveries).
  • Webhook failing. Se a URL falhar continuamente por 24 h, o webhook passa para failing e os administradores do comerciante recebem um aviso.

11Erros, limites e versões

Fora de /oauth/*, todos os erros usam um envelope único:

JSON
{
  "error": {
    "code": 1003,
    "type": "invalid_parameter",
    "message": "Field 'to' must be E.164 when to_type is 'phone'.",
    "details": [
      {
        "field": "to",
        "issue": "format"
      }
    ],
    "trace_id": "trc_9f2c41ab"
  }
}
codeHTTPtypeSignificado
1001400invalid_requestJSON malformado ou campo obrigatório ausente
1003400invalid_parameterValor inválido (ver details)
1101401token_expiredToken expirado; peça outro
1102401invalid_tokenToken inválido ou revogado
1201403insufficient_scopeO token não tem o escopo necessário
1202403sender_not_allowedA integração não tem permissão para esse remetente
1301404not_foundRecurso inexistente ou de outro comerciante
1401409idempotency_conflictMesma Idempotency-Key com corpo diferente
1501429rate_limitedLimite de requisições; respeite Retry-After
2104webhookrecipient_not_reachableTelefone sem conta PHIZ ou usuário bloqueou o remetente
2201422policy_violationEnvio não permitido pela política de opt-in
2202422unsupported_typeTipo de mensagem não habilitado para o remetente
2301webhookmedia_unavailableO app não conseguiu baixar a mídia do armazenamento da PHIZ (mídia expirada ou removida antes da entrega)
2302400media_link_not_allowedO link não foi gerado pela PHIZ; faça o upload e use o id ou o link devolvido
2303422media_expiredO id ou o link da mídia expirou, foi removido ou pertence a outro comerciante
2304415unsupported_media_typeFormato de arquivo não aceito no upload
2305413media_too_largeArquivo acima do tamanho máximo do tipo
2401403sender_disabledRemetente desativado ou restrito
2501429messaging_limit_reachedLimite diário de destinatários do remetente atingido
5000500internal_errorErro interno; repita com a mesma Idempotency-Key

Limites (proposta inicial)

RecursoLimite
/oauth/token60 req/min por app
Envio de mensagens80 msg/s por remetente (ajustável por contrato)
Demais endpoints600 req/min por app
Destinatários únicos de mensagens iniciadas pela empresaFaixas por remetente: 1 mil → 10 mil → 100 mil → ilimitado em 24 h, conforme qualidade (Fase 2)
HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 2
X-RateLimit-Limit: 80
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1760528529

Ambientes

  • Produção: https://{gateway}/gateway/business
  • Sandbox: https://{gateway-de-teste}/gateway/business, no ambiente de testes usado hoje (Openapi-test), com um remetente de teste por comerciante e até 5 destinatários cadastrados (como o número de teste da Meta). Secrets phzsk_test_ só funcionam aqui.

Prefixo de rotas

A nova API roda no mesmo gateway da API atual, separada por prefixo:

  • /gateway/openapi/…: API atual, mantida durante a migração.
  • /gateway/business/…: esta API (autenticação, mensagens e webhooks).

Com prefixos distintos, cada contexto tem roteamento, autenticação, limites e logs próprios, e o legado pode ser desligado sem afetar a nova API. Todos os caminhos deste documento, como /oauth/token e /v1/{sender_id}/messages, são relativos a https://{gateway}/gateway/business. Nos exemplos, esse endereço aparece como $PHIZ_API_BASE.

12Área do Desenvolvedor no Open Platform

A configuração de desenvolvedor sai de Canal › Configuração de Desenvolvimento e vira um item de menu do comerciante, Desenvolvedor, ao lado de Configurações. Ela reaproveita o ID do Comerciante, as funções de Gerenciamento da equipe e o Registro de operações que já existem.

Mockups da nova área, com valores de exemplo. Clique em uma imagem para abrir em tamanho real.

Visão geral: ID do comerciante, URL base, integrações e primeiros passos

Tela 1. Visão geral: ID do comerciante, URL base, integrações e primeiros passos

Nova integração: nome, escopos, remetentes permitidos e ambiente

Tela 2. Nova integração: nome, escopos, remetentes permitidos e ambiente

Detalhe da integração: credenciais, segredos com rotação e escopos

Tela 3. Detalhe da integração: credenciais, segredos com rotação e escopos

Segredo criado, exibido uma única vez

Tela 4. Segredo criado, exibido uma única vez

Webhooks: configuração, eventos e entregas recentes

Tela 5. Webhooks: configuração, eventos e entregas recentes (Fase 2)

Remetentes: mensagem direta e canais, com as integrações que os usam

Tela 6. Remetentes: mensagem direta e canais, com as integrações que os usam

Página do canal com aviso de migração das credenciais

Tela 7. Página do canal com aviso de migração das credenciais

Telas

Visão geral
/developer
  • Mensagens enviadas, entregues e com falha nas últimas 24 h
  • Saúde dos webhooks (Fase 2)
  • Alertas: secret antigo em uso; webhook falhando (Fase 2)
Apps
/developer/apps
  • Criar app: nome, tipo de integração (própria, Blip, Salesforce, outra), ambiente
  • Escopos e remetentes permitidos
  • Client ID, secrets com rotação e último uso
  • Lista de IPs permitidos (opcional)
  • Desativar ou excluir app
Webhook do app (Fase 2)
/developer/apps/:id/webhook
  • Na Fase 1, a URL é cadastrada manualmente pela PHIZ (seção 10)
  • URL, verify token e eventos
  • Webhook secret com rotação
  • Verificar e enviar teste
  • Entregas das últimas 72 h com status HTTP, tentativas, latência e reenviar
Remetentes
/developer/senders
  • O próprio comerciante, sempre presente, e os canais, se houver
  • Cada um com sender_id, status, limite e qualidade
  • Escolhe a integração que recebe as conversas de cada remetente
  • Para canais, liga o modo “Phiz Business API”
Logs de API
/developer/logs
  • Requisições dos últimos 7 dias: método, rota, status, latência, trace_id
  • Filtro por app e por código de erro
Sandbox
/developer/sandbox
  • Remetente de teste
  • Cadastro de até 5 destinatários de teste
  • Credenciais phzsk_test_

Permissões

Novas permissões entram na tela Gestão de Funções já existente. O Superadministrador recebe todas.

PermissãoPermite
developer.viewVer integrações, remetentes, logs e entregas (sem secrets)
developer.apps.manageCriar, editar e desativar integrações; definir escopos e remetentes
developer.secrets.manageGerar, rotacionar e revogar secrets
developer.webhooks.manageConfigurar webhook, testar e reenviar entregas (Fase 2)

Criar, rotacionar ou revogar um secret, mudar escopos e alterar o webhook gera registro em Registro de operações e um e-mail para os administradores.

Remetente com mais de uma integração

Para evitar que dois sistemas respondam ao mesmo usuário, cada remetente (o comerciante ou um canal) tem uma integração responsável pelas conversas, escolhida na tela Remetentes. Só essa integração recebe o evento messages daquele remetente. Qualquer integração com permissão pode enviar por ele e recebe o message_status das mensagens que enviou. Um protocolo de passagem de conversa entre integrações (como o handover do Messenger) fica para depois.

Migração das credenciais de canal

  1. Criação automática. Para cada canal com AppID, criamos um app “Legado – nome do canal” no comerciante, com o mesmo AppID como client_id, o mesmo secret (já armazenado) e permissão só para aquele canal. Nenhuma integração para.
  2. Convivência. /gateway/openapi/auth e /token/refresh continuam funcionando. A página do canal mostra um aviso apontando para a nova área.
  3. Somente leitura. Após 3 meses, a página do canal deixa de permitir redefinir o secret; a gestão passa a ser só na nova área.
  4. Desligamento. Após 6 meses, e com o uso medido perto de zero, os endpoints antigos respondem 410 Gone.

13Integração com parceiros

No MVP, o comerciante cria um app na Área do Desenvolvedor e cola client_id e secret na Blip, no Salesforce ou no próprio sistema. As URLs de webhook vêm do parceiro e, na Fase 1, a PHIZ as cadastra manualmente (seção 10). Como os contratos seguem a Cloud API, parceiros que já têm conector de WhatsApp reaproveitam a maior parte dele.

Numa fase seguinte, criamos o programa de parceiros de tecnologia, equivalente ao Embedded Signup da Meta:

  • O parceiro registra um app de plataforma na PHIZ.
  • O comerciante clica em “Conectar com a PHIZ” dentro do parceiro e autoriza escopos e remetentes numa tela de consentimento (OAuth 2.0 authorization code + PKCE).
  • O parceiro recebe tokens limitados àquele comerciante e um único webhook para todos os seus clientes. O campo entry[].id do envelope identifica o comerciante; por isso o envelope já nasce com essa estrutura.

14Caminho para a Marketing API

Esta RFC reserva o espaço que a Marketing API vai ocupar, sem implementá-la:

  • type: template no envio e o escopo templates:manage já existem no contrato.
  • Templates com aprovação: /v1/merchants/{merchant_id}/message_templates e evento de webhook template_status.
  • Categorias de conversa (conversation.origin.type: service, utility, marketing), base para relatórios e preço.
  • Gestão de opt-in e opt-out por remetente, campanhas para segmentos e métricas em /v1/merchants/{merchant_id}/analytics.
  • Faixas de limite de envio e nota de qualidade por remetente, já presentes no recurso Sender.

15Plano de entrega

FASE 1 · MVP
Autenticar e enviar
  • /oauth/token, /oauth/revoke, /v1/me
  • Envio de text, image, audio, video e document para telefone e PHIZ ID
  • Upload de mídia (/v1/{sender_id}/media) com armazenamento temporário; sem links de terceiros
  • Webhooks messages e message_status com assinatura, com a URL cadastrada manualmente pela PHIZ
  • Área do Desenvolvedor: apps e secrets
  • Remetente padrão (o próprio comerciante) para todos; canais como remetentes opcionais
  • Migração automática das credenciais de canal
FASE 2
Webhook por conta própria e proteções
  • Configuração do webhook pela Área do Desenvolvedor e pela API (PUT /v1/apps/{app_id}/webhook, teste e saúde)
  • Retentativas com backoff exponencial por até 24 h
  • Histórico de entregas e reenvio manual por 72 h
  • Status failing com aviso aos administradores

Datas serão definidas após a revisão desta RFC com o time da China.

16Questões em aberto

  1. Política de opt-in. A seção 09 propõe a política (opt-in por comerciante e categoria, opt-out aplicado pela plataforma, registro no comerciante). Falta decidir: modelo A (como a Meta, sem validação) ou B (registro de consentimento no PHIZ, por exemplo /v1/merchants/{merchant_id}/consents, verificado no envio); quais categorias existem; se o app PHIZ mostra ao usuário o que ele autorizou; e como o usuário bloqueia ou para ofertas de um Business Account (ver Q3). Validar os pontos de LGPD com o jurídico. Vale para Business Account e Channel Service.
  2. IDs de remetente. Usar diretamente o merchant_id e o ID do canal como sender_id, ou criar IDs opacos novos? Usar os existentes simplifica a migração, mas expõe o ID sequencial do canal.
  3. Tipo de conversa Business Account. Confirmar o caminho Contas públicas › Service › Business Account e a definição: conversa de serviço de mão dupla para empresas, comerciantes, governos e instituições, com mensagens do usuário permitidas e Menu configurável, e mensagens ativas sujeitas às regras de autorização e envio. Esse tipo já existe no app ou precisa ser construído? Como o usuário bloqueia ou silencia um Business Account?
  4. Tipos de conversa vigentes. Revisar a hierarquia e a tabela de permissões (v1.2): quais tipos existem hoje, quais regras atuais são diferentes, quais tipos não existem e quais estão faltando.
  5. Telefones. Todos os usuários PHIZ têm telefone verificado? Como tratar números de outros países e trocas de número?
  6. Uma integração responsável por remetente é suficiente, ou precisamos de passagem de conversa entre apps (bot → atendente humano) já na Fase 2?
  7. Business Message API existente. O modo “Phiz Business API” do Robô de IA já tem endpoints de envio e callback e parece corresponder ao Channel Service com Channel Bot. Esta RFC substitui, estende ou encapsula essa API?
  8. Mídia. Decidido na v1.3: mídia só por upload na PHIZ, com armazenamento temporário, e sem links de terceiros na Fase 1 (seção 09). Falta definir: o app PHIZ reproduz todos os formatos da tabela de mídia, e com quais tamanhos? O prazo de 30 dias serve, ou deve ser menor? Depois da entrega, a mídia passa para o armazenamento normal do chat, ou some do histórico do usuário quando expira? Quando e para quais clientes liberar URLs de terceiros? Por quanto tempo o url das mídias recebidas fica válido?
  9. Webhook na Fase 1. Decidido na v1.4: a PHIZ cadastra a URL manualmente, e retentativas, reenvio manual e status failing ficam para a Fase 2 (seção 10). Falta definir: como o pedido de cadastro é feito, qual time da PHIZ o executa e em quanto tempo; como o webhook secret chega ao parceiro; qual o mínimo de retentativa da Fase 1 (nenhuma ou uma janela curta); como o parceiro recupera mensagens recebidas que se perderam, já que só o status tem consulta; e o que dispara a Fase 2.

17Referência OpenAPI

A especificação OpenAPI 3.1 abaixo cobre todos os endpoints e webhooks desta RFC, com descrições neste idioma. O botão “Try it out” está desligado porque a API ainda não existe. Use “Copiar OpenAPI” para levar o YAML para Apifox, Postman ou um gerador de SDK.

RFC PHZ-0001 · Draft for review

PHIZ Business Messaging API

Merchant-level authentication, sending messages to a phone number or PHIZ ID, and webhooks for messages and statuses. Inspired by Meta's Cloud API and designed as the foundation of the future Marketing API.

Status
Draft
Author
Gleison Silva - Product Solutions
Date
2026-10-15
Reviewers
Pedro Bettani (Brazil)
Vitor Mozer (Brazil)
Richard Liang (China)
API version
v1 (proposed)

01Summary

This document proposes a public API that lets external systems (Blip, Salesforce, CRMs or the merchant's own systems) talk to PHIZ users on behalf of a merchant. It has four parts:

  • OAuth 2.0 authentication (client credentials) with credentials issued to the merchant account instead of a channel.
  • Sending messages to a phone number (E.164) or a PHIZ ID directly on behalf of the merchant. Using a channel as the sender is optional.
  • Signed webhooks that deliver messages received from users and every status change of sent messages.
  • A merchant-level Developer area in the Open Platform where the merchant registers each external system that will use the API (for example Blip or Salesforce) as an integration. Each integration has its own client_id and secret, webhook and delivery history. Integrations are unrelated to Miniapps.

Message and webhook contracts reproduce the structure of Meta's Cloud API, renaming only domain terms. Anyone already integrated with the WhatsApp Business Platform can adapt by changing URLs, credentials and a few field names.

02Context: how it works today

Based on the current documentation (Apifox, Authentication module) and the Open Platform (open.phiz.chat).

Current authentication

The token is obtained at POST /gateway/openapi/auth with app_id, app_secret, description (required) and grant_type: "user_credentials". The response carries an access token valid for about 2 hours and a refresh token valid for about 90 days, renewed at POST /gateway/openapi/token/refresh.

HTTP
POST /gateway/openapi/auth
Content-Type: application/json

{
  "app_id": "69c37cb1821121af34201f48",
  "app_secret": "94B18B920D19463C7264BD0BDE30C31…",
  "description": "push news",
  "grant_type": "user_credentials",
  "device_fingerprint": ""
}

200 OK
{
  "code": 200,
  "msg": "success",
  "data": {
    "scope": "channel",
    "status": "active",
    "access_token": "at_1770343926041_1Y3iz6XN…",
    "token_type": "Bearer",
    "expires_in": 7199,
    "refresh_token": "rt_1770343926041_mxc7RV9…",
    "access_token_expires_at": "2026-02-06T04:…",
    "refresh_token_expires_at": "2026-05-07T02:…",
    "created_at": "2026-02-05T10:15:23.672+00:00"
  },
  "success": true
}

Where credentials live

In the Open Platform, every channel has a Channel › Development Settings page with an AppID and an AppSecret. The secret is never displayed and can only be reset. Under AI Robot, the channel also offers the “Phiz Business API” reply mode, which routes the channel's conversations to the Business Message API for sending and webhook callbacks.

At merchant level there is already a Merchant ID (UUID) and Team management with roles, members and an operation log. These three pieces are the foundation of the new area.

What this RFC changes

Each row shows how things work today, why that is a problem, what this proposal puts in its place, and a concrete use case.

TodayWhy it is a problemWith this RFCUse case
To reach a user through the API, the merchant needs a channel.Merchants without a channel, or that want to speak as the business itself, cannot send a direct message to the user.The merchant sends a direct message as itself. Using a channel becomes optional.A pharmacy with no channel tells its customer Maria, by her phone number, that her order is ready for pickup. The message arrives in a direct conversation with the pharmacy.
Each channel has its own AppID and secret.A merchant with 5 channels manages 5 credentials, and Blip needs one token per channel.One credential per integration, owned by the merchant account, with access to direct sending and the chosen channels.Loja Exemplo has 3 channels and uses Blip. Today it registers 3 AppIDs in Blip; with this RFC it creates one “Blip” integration and ticks direct sending and the 3 channels.
“Reset” invalidates the secret immediately.The integration stays down until someone updates the secret in the external system.Two valid secrets at the same time: the new one goes live and the old one keeps working for a period (default 24 h).The Salesforce secret showed up in a log. The team generates a new one, updates Salesforce in the afternoon and revokes the old one at the end of the day, without losing a single message.
Login uses grant_type: user_credentials and requires a description field.Tools that already speak OAuth, such as Salesforce, cannot connect without custom code.Standard OAuth 2.0 (client_credentials), which those tools support out of the box.The Salesforce admin connects PHIZ in Named Credentials by filling in only the token URL, client_id and secret, with no developer involved.
There is a refresh token valid for about 90 days.It is one more long-lived secret to store, with no benefit in a server-to-server integration.1-hour token with no refresh token; when it expires, the system requests a new one.Blip renews the token on its own every hour with the same client_id and secret. There is no refresh token that can expire over a weekend and take customer service down.
There is no screen for webhooks, logs or multiple integrations.The merchant cannot set up the webhook or find out alone why an integration stopped.A Developer area with separate integrations, webhook, delivery history and logs.Customer replies stop arriving in Blip. The merchant opens the delivery history, sees the webhook has returned HTTP 500 since 10 a.m. and, after the fix, replays the events.

03Reference: the WhatsApp experience

This RFC brings the WhatsApp Business Platform model to PHIZ. So that everyone (Brazil and China) shares the same picture of what we are copying, this section walks through a Brazilian user's journey on WhatsApp: how they create an account, how they authorize a business, how a message arrives and what they can do with it. Example: merchant PicPay, user Ana Souza, in São Paulo.

About the screens

They are illustrations, not the official WhatsApp UI. Text is shown in English; in the real app the user sees Portuguese. The codes (A2, O1, C3…) follow the Design canvas this section comes from. Click an image to open it at full size.

Message path

A business does not talk to WhatsApp directly: its system (CRM, customer service) calls a BSP (Blip, Zenvia, Twilio, Infobip), which calls Meta's Cloud API, which delivers to the phone. Replies, taps and statuses come back by webhook along the same path. In PHIZ, the PHIZ Business Messaging API takes the place of the Cloud API and the merchant account (merchant_id) takes the place of the WABA (see the mapping in section 06).

Company → BSP → Meta Cloud API → WhatsApp → user, with opt-in as a prerequisite and the webhook return path

B01. Company → BSP → Meta Cloud API → WhatsApp → user, with opt-in as a prerequisite and the webhook return path

User sign-up

On WhatsApp, the account is the phone number, verified by SMS. There are no friend requests: anyone with the number can write. Businesses and family share one inbox, which is why business messaging is tightly controlled. Since 2026 WhatsApp offers usernames and business-scoped user IDs (BSUID) instead of the phone number, which lines up with the use of PHIZ ID in this RFC.

Phone number = account identity

A02. Phone number = account identity

One inbox for everything

A10. One inbox for everything

User authorization (opt-in)

Before a business starts a conversation, the user must have authorized that business. There are four common ways: in the business's app (O1), at sign-up or checkout (O2), when the user writes first, which covers only that service conversation (O3), and by answering “yes” inside the chat (O4). Good practice is to split by purpose: authorizing account alerts does not authorize offers.

Consent lifecycle: collect, record, send within scope, revoke

O00. Consent lifecycle: collect, record, send within scope, revoke

Opt-in in the merchant's app, split by purpose

O01. Opt-in in the merchant's app, split by purpose

Opt-in at sign-up, with unticked boxes

O02. Opt-in at sign-up, with unticked boxes

The user writes first: authorizes only that service chat

O03. The user writes first: authorizes only that service chat

Opt-in asked inside the chat, with buttons

O04. Opt-in asked inside the chat, with buttons

Meta has the policy but does not verify the opt-in. There is no field or endpoint to prove authorization; Meta trusts the business and only reacts afterwards, through blocks, reports and the quality rating. Opt-out, on the other hand, is enforced by the platform: blocking, “stop offers” and the marketing cap make sends fail. Even so, under Brazil's LGPD the business must prove consent and honor withdrawal, so the record (who, merchant, category, source, date, wording shown, revocation) is required in practice.

Consent record on the merchant side

O05. Consent record on the merchant side

Meta does not verify opt-in; opt-out is enforced by the platform; why record it anyway

O06. Meta does not verify opt-in; opt-out is enforced by the platform; why record it anyway

What the user receives

Business-initiated messages use approved templates in four categories: utility (payments, bills), marketing, authentication (codes) and service. Each category depends on the matching opt-in.

Notification identical to a personal contact's

C01. Notification identical to a personal contact's

Chat list: verified business account, unread and on top, next to family and friends

C02. Chat list: verified business account, unread and on top, next to family and friends

Utility: Pix received, with buttons

C03. Utility: Pix received, with buttons

Marketing: needs its own opt-in for offers

C04. Marketing: needs its own opt-in for offers

Authentication: code with a copy button

C05. Authentication: code with a copy button

Bill with a Pix copy-and-paste code

C06. Bill with a Pix copy-and-paste code

What the user can do

The user replies (text, buttons, list, form), checks the business profile or withdraws permission. Each action does, or does not, create an event for the business.

Every user option and what the business receives back

D00. Every user option and what the business receives back

Quick-reply button

D02. Quick-reply button

List message

D04. List message

In-chat form (Flows), close to a mini app

D05. In-chat form (Flows), close to a mini app

Stop offers, keep alerts

D07. Stop offers, keep alerts

Block and report, with a reason

D08. Block and report, with a reason

Keyword opt-out, handled by the merchant

D10. Keyword opt-out, handled by the merchant

User actionOn WhatsAppOn PHIZ
Replies with text or mediamessages webhook; opens the 24 h windowmessage event (section 10)
Taps a button or list rowbutton_reply / list_replymessage event with the chosen id
Stops offersMarketing sends failProposal: the platform blocks the category for that merchant_id
Blocks or reportsAll sends fail; quality rating dropsProposal: send error plus a quality signal (see Q3)
Mutes or archivesInvisible to the businessSame

What we take to PHIZ

  • The business identity throughout the flow is the Merchant ID (merchant_id): it receives the opt-in, the block and the quality rating. There is no separate developer ID; integrations and apps only act on behalf of the merchant.
  • Consent per merchant and per category, with opt-out enforced by the platform (see Sending policy, section 09).
  • User signals (block, report, stop offers) feeding the merchant's sending limits.
  • In-chat forms (Flows) as a reference for mini apps.

04Opt-in in PhizChat: in the app and at the business

Section 03 showed that on WhatsApp opt-in almost always happens outside the app and Meta does not verify it. This section proposes how a user authorizes a business account in PhizChat, through two paths that end in the same opt-out controls:

  • Flow P — inside PhizChat: the user accepts the business account in the app itself. The platform keeps the proof.
  • Flow Q — at the business, outside PhizChat: the user authorized in the merchant's app, website or store, and the merchant starts the conversation.
About the screens

They are illustrative proposals, with PicPay as the example merchant. The codes (P0…P8, Q0…Q5) follow the Design canvas. Click an image to open it at full size.

Flow P · Opt-in inside PhizChat

The user reaches the business account through an SMS or e-mail link, a QR code, the merchant's app or website, or in-app search. Without Phiz installed, they see an H5 page with only the profile and a download button; acceptance never happens outside the app. After installing and logging in, a deferred deep link takes them to the same chat, which opens locked until Accept. On Accept, PhizChat records the consent (user, merchant_id, date, entry point), sends the welcome template if there is one, and unlocks the chat.

Flow P map: entry points, install, login, acceptance and afterwards

P00. Flow P map: entry points, install, login, acceptance and afterwards

Without the app: H5 page with profile and download, no Accept button

P01. Without the app: H5 page with profile and download, no Accept button

BA chat with the consent card; input locked until Accept

P02. BA chat with the consent card; input locked until Accept

After Accept: welcome message and chat unlocked

P03. After Accept: welcome message and chat unlocked

Business profile before consent, also with Accept

P04. Business profile before consent, also with Accept

Consent confirmation sheet

P05. Consent confirmation sheet

After Accept, the profile shows the consent status and date. The user can revoke (the chat goes back to the locked state and can be authorized again) or block (reuses today's Block; the API still accepts calls but returns the blocked status).

Profile after consent: status, date and Revoke consent

P06. Profile after consent: status, date and Revoke consent

Revocation confirmation; the chat is locked again

P07. Revocation confirmation; the chat is locked again

Blocked business account: the API returns the blocked status

P08. Blocked business account: the API returns the blocked status

Flow Q · Opt-in collected by the business, outside PhizChat

Most Brazilian companies already collect consent in their own channels. In flow Q, the user turns on “messages on PhizChat” in the PicPay app; PicPay keeps the proof and sends a template through the API. The API checks that the recipient is a Phiz user and has not blocked or revoked; in model B it also checks for a registered consent for that category. In the app, the first message arrives with a consent source card (what was authorized, where and when), and the user can keep receiving, stop or report.

Flow Q map: business side, API checks and user side, with the P vs Q comparison

Q00. Flow Q map: business side, API checks and user side, with the P vs Q comparison

Opt-in in the PicPay app, naming PhizChat

Q01. Opt-in in the PicPay app, naming PhizChat

First message in the list, labeled New business

Q02. First message in the list, labeled New business

Consent source card: keep receiving, stop or report

Q03. Consent source card: keep receiving, stop or report

Consent status per category, with source and date

Q04. Consent status per category, with source and date

Model B: registering consent through the API and send errors (illustrative proposal)

Q05. Model B: registering consent through the API and send errors (illustrative proposal)

P vs Q

P · opt-in in PhizChatQ · opt-in at the business
Who collectsPhizChat (Accept)The merchant (app, website, store)
Proof lives inPhizChatMerchant CRM (model A) · also PHIZ (model B)
First chatLocked until AcceptOpens with the message and the consent source
Opt-outSame for both: revoke, block, report — enforced by PhizChat

Open decisions

  • Model A vs B in flow Q (see Q1): policy only, like Meta, or a consent registry in PHIZ checked on every send. The full consent source card (Q3) and per-category status (Q4) depend on model B.
  • Delivery before Accept in flow Q: the proposal delivers the first message with the consent source card; the alternative is to lock the chat until Accept, as in flow P.
  • Consent categories (account alerts, offers, codes) and what the app shows the user.
  • Technical names — /v1/merchants/{merchant_id}/consents, the consent_update webhook and the error codes — are placeholders for discussion.

05Goals and non-goals

Goals

  • Credentials tied to the merchant account instead of each channel. The merchant creates one integration per external system (for example, one for Blip and another for Salesforce). Each integration gets its own client_id and secret, and the merchant decides what it can do (send messages, check statuses, set up the webhook) and which senders it can speak through: the merchant itself and, if the merchant wants, its channels.
  • Standard OAuth 2.0 (client credentials), compatible with common libraries and platforms.
  • Send messages to a phone number or PHIZ ID and receive replies and statuses through a signed webhook.
  • Structural compatibility with Meta's Cloud API so existing integrations migrate with little code.
  • Publish the API specification in OpenAPI 3.1 format (the Swagger standard). It is a file that describes every route, field, response and webhook, and works as the agreement between the team that builds the API and those who integrate with it. With it, partners generate documentation and code and can test before delivery.

Out of scope for this RFC

  • Templates, campaigns and segmentation (Marketing API, later phases).
  • Per-conversation pricing and billing.
  • Groups, voice and video calls.
  • Official SDKs (may come later, generated from the OpenAPI spec).

06Domain model

The merchant account becomes the root of everything. The merchant talks to the PhizChat user in two ways:

  • Direct message (default). The merchant sends as itself, with the name and photo from Merchant information. No channel needed.
  • Through a channel (optional). Merchants that have channels can send through them, for example to separate brands or departments.

Integrations belong to the merchant and receive permission to use either way or both.

flowchart LR
  M["Merchant account
merchant_id"] --> I["Integrations
Blip · Salesforce · own system
client_id + secret each"] I -- "POST /v1/{sender_id}/messages" --> API["PHIZ Business API"] API -- "Way 1 · default
sender_id = merchant_id" --> D["Direct message
from the merchant"] API -. "Way 2 · optional
sender_id = channel ID" .-> C["Message through
the merchant's channel"] D --> U(("PhizChat user
phone or PHIZ ID")) C --> U API -- "webhook: replies and statuses" --> I

Both ways use the same route and format; only the sender_id changes. Way 2 exists only for merchants that have a channel.

End-to-end flow

View for the engineering team: where a message travels from the business to the user, and how replies and statuses come back.

flowchart LR
  PP["PicPay"] --> B1["BSP
e.g. Blip"] AM["Ambar"] --> B2["BSP
e.g. Salesforce"] B1 --> API["PHIZ Business API
/gateway/business
Auth · Messages · Webhooks"] B2 --> API API -- "1–3 · token, send and
sender validation" --> MA subgraph OP["Open Platform"] MA["Merchant Account"] MA -- "4a · sender_id = merchant_id
default" --> DM["Direct Message"] MA -. "4b · sender_id = channel ID
optional" .-> CH["Channel"] CH --> CM["Channel Message"] end DM -- "5 · deliver" --> APP["Phiz App"] CM -- "5 · deliver" --> APP APP --> U(("User"))

Outbound: from the business to the user. The dashed line is the optional channel path.

Return: replies and statuses

flowchart LR
  U(("User")) -- "replies, receives, reads" --> APP["Phiz App"]
  APP -- "6 · events" --> WH["PHIZ Business API
Webhooks"] WH -- "signed webhook" --> BSP["BSP of the integration
that owns the sender"] BSP --> EMP["Business"]

Inbound: user replies and message statuses reach the BSP through the webhook.

  1. Integration. The business (for example PicPay or Ambar) uses a BSP for customer service. In the Developer area, the merchant creates an integration for that BSP and hands over the client_id and secret.
  2. Token. The BSP calls /oauth/token. Auth checks the integration in the Merchant Account (secret, scopes and allowed senders) and returns a 1-hour JWT.
  3. Send. The BSP calls POST /v1/{sender_id}/messages. The API validates the token, scope, sender, opt-in policy and Idempotency-Key, answers 202 and queues the message. For media, the BSP first uploads the file to POST /v1/{sender_id}/media and uses the returned id or PHIZ link in the send.
  4. Routing. With sender_id = merchant_id the message goes out as a Direct Message (default). With a channel ID it goes out as a Channel Message (optional).
  5. Delivery. The Phiz App shows the message to the user and downloads media from PHIZ storage, through the link generated at upload.
  6. Return. User replies and statuses (sent, delivered, read, failed) become events that the Webhooks service delivers, signed, to the integration that owns the sender.
ComponentResponsibilityStatus
BSP / business systemCalls the API, uploads media and receives the webhook.External
PHIZ Business API · AuthIssues and revokes JWTs; validates client_id and secret.New
PHIZ Business API · MessagesValidates and accepts sends (202), applies idempotency and limits, queues and routes by sender_id.New
PHIZ Business API · MediaReceives uploads, checks format and size, stores the file for a limited time and generates the id and PHIZ link used in the send.New
PHIZ Business API · WebhooksConsumes app events, signs and delivers them with retries; keeps 72 h of delivery history.New
Open Platform · Merchant AccountStores integrations, secrets (hashed), scopes, allowed senders and webhook settings. Hosts the Developer area.Changed: credentials move from the channel to the merchant
Direct MessageDirect conversation between merchant and user in the app.New (see Q3)
Channel / Channel MessageSending through the merchant's channel.Existing
Phiz AppShows messages, downloads media by PHIZ link and emits delivery, read and reply events.Changed

Mapping to Meta's Cloud API

Meta (WhatsApp Cloud API)PHIZ (this proposal)
WhatsApp Business Account (WABA)Merchant account · merchant_id
Business phone number · phone_number_idSender · sender_id: the merchant itself (default, equal to merchant_id) or, optionally, a channel (equal to the channel ID)
Meta app + System User tokenDeveloper app + OAuth client credentials
wa_idphiz_id
POST /{phone-number-id}/messagesPOST /v1/{sender_id}/messages
POST /{phone-number-id}/media · media by id or third-party linkPOST /v1/{sender_id}/media · media only by id or PHIZ link; third-party links are not accepted
object: whatsapp_business_accountobject: phiz_merchant_account
X-Hub-Signature-256X-Phiz-Signature-256 + X-Phiz-Timestamp
hub.mode / hub.verify_token / hub.challengeUnchanged

07Design principles

  • Same shape as the Cloud API. Send bodies, responses and webhooks follow Meta's structure. We diverge only for a clear gain in security (timestamp in the signature, media only through PHIZ storage) or clarity (to_type).
  • HTTP status codes, a single error envelope and a trace_id in every error response.
  • Sending is asynchronous and returns 202 Accepted; the outcome arrives by webhook.
  • Idempotency. The Idempotency-Key header prevents duplicate messages when a client retries.
  • Least privilege. Each integration has scopes and a list of allowed senders.
  • Versioning /v1. Compatible changes (new fields) ship in a standardized way; breaking changes require /v2.

08Authentication

We use the OAuth 2.0 client credentials flow (RFC 6749 §4.4). The app exchanges client_id and client_secret for a 1-hour access token and sends it as Authorization: Bearer on every call. There is no refresh token: when the token expires, the app requests a new one.

POST/oauth/tokenIssues an access token
POST/oauth/revokeRevokes a token (RFC 7009)
GET/v1/meShows the token's integration, merchant, scopes and senders
sequenceDiagram
  autonumber
  participant S as External system
  participant A as PHIZ Auth
  participant API as PHIZ API
  S->>A: POST /oauth/token (client_id, client_secret)
  A-->>S: 200 access_token (1 h), scopes
  S->>API: POST /v1/{sender_id}/messages · Bearer token
  API-->>S: 202 Accepted

Close to expiry, the system requests a new token. There is no refresh token.

Request and response

BASH
# Base URL of this API: current PHIZ gateway + /gateway/business prefix (illustrative host)
PHIZ_API_BASE="https://gateway.phiz.chat/gateway/business"

curl -X POST "$PHIZ_API_BASE/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_id=app_2Fk81PzQ \
  -d client_secret="$PHIZ_CLIENT_SECRET" \
  -d "scope=messages:send messages:read"
JSON
{
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InBoei0yMDI2LTEwIn0…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "messages:send messages:read",
  "merchant_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57"
}

The /oauth/* endpoints use the OAuth error format (RFC 6749 §5.2), which client libraries already understand:

JSON
{
  "error": "invalid_client",
  "error_description": "Client authentication failed."
}

Token format

Decision: the token is a JWT signed with ES256, keys published at /.well-known/jwks.json. The gateway validates tokens locally without a database lookup per call. Revoked tokens and rotated secrets go into a short-lived deny list (until the token's expiry). For integrators the token is opaque; the content below is informative.

JSON
{
  "iss": "https://gateway.phiz.chat/gateway/business",
  "sub": "app_2Fk81PzQ",
  "aud": "phiz-business-api",
  "mid": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
  "scope": "messages:send messages:read",
  "snd": [
    "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
    "1231"
  ],
  "env": "live",
  "iat": 1760527800,
  "exp": 1760531400,
  "jti": "tok_01JA9VQ4N2"
}

Scopes

ScopeAllows
messages:sendSend messages, upload and manage media, and mark messages as read
messages:readQuery message status
webhooks:manageSet up, test and replay the app's own webhook (Phase 2)
account:readRead merchant account and sender data
templates:manageReserved for the Marketing API

Secrets and rotation

  • Secrets carry an environment prefix (phzsk_live_, phzsk_test_) so leak scanners can recognise them.
  • A secret is shown only once, at creation or rotation, and stored only as a hash (Argon2id).
  • Each app can have up to two active secrets. When a new one is generated, the previous one stays valid for a chosen period (default 24 h, from 0 to 7 days), so the switch causes no downtime.
  • The screen shows when each secret was last used, so you know whether the old one is still in use before revoking it.
  • Optional: an IP allowlist per app.

Current vs proposed

AspectTodayProposal
Credential ownerChannelMerchant account (through an app)
Flowuser_credentials (custom)client_credentials (RFC 6749)
Extra required fieldsdescriptionNone
Lifetime~2 h + ~90-day refresh token1 h, no refresh token
ScopeschannelPer capability + allowed senders
RotationReset invalidates immediatelyTwo active secrets with overlap
ErrorsHTTP 200 + code in the bodyReal HTTP status + standard envelope

09Sending messages

POST/v1/{sender_id}/messagesSends a message or marks one as read
POST/v1/{sender_id}/mediaUploads a media file and returns its id and PHIZ link
GET/v1/media/{media_id}Media details: link, type, size and expiry
DELETE/v1/media/{media_id}Deletes the media before it expires
GET/v1/messages/{message_id}Latest known status (fallback)
GET/v1/merchants/{merchant_id}/sendersSenders (merchant and channels) the integration may use

Sender: the merchant or a channel

The sender_id in the path defines on whose behalf the message is sent:

  • The merchant itself (default). Use the merchant_id. The user sees the message in a Business Account conversation, directly with the business, and the merchant does not need any channel.
  • A channel (optional). Use the channel ID, for example 1231. Useful for merchants that separate brands or departments into different channels. The message appears in the channel's Channel Service conversation, the same one Channel Bot uses.

Both work the same way in the API: same fields, same responses and same webhooks. In webhooks, metadata.sender_type shows which one was used and metadata.conversation_type carries the conversation type (business_account or channel_service).

Recipient: phone number or PHIZ ID

The to field holds the recipient and to_type says how to read it:

  • phone (default): an E.164 phone number such as +5511987654321. PHIZ resolves the number to the user's account.
  • phiz_id: the user's PHIZ ID, such as 839201447. This is the identifier webhooks carry, so replies can use it directly.

If the phone number does not belong to a PHIZ account, or the user has blocked the sender, the API still answers 202 and the webhook reports failed with error 2104. This keeps the API from being used to find out which numbers have a PHIZ account.

Example: text to a phone number

BASH
# sender_id = merchant_id: direct message from the merchant, no channel needed
curl -X POST "$PHIZ_API_BASE/v1/7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57/messages" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-48213-envio" \
  -d '{
    "messaging_product": "phiz",
    "to": "+5511987654321",
    "to_type": "phone",
    "type": "text",
    "text": { "body": "Olá, Maria! Seu pedido #48213 foi enviado." },
    "biz_opaque_callback_data": "pedido=48213"
  }'
JSON
{
  "messaging_product": "phiz",
  "contacts": [
    {
      "input": "+5511987654321",
      "phiz_id": null
    }
  ],
  "messages": [
    {
      "id": "pmid.01JA9VZ6T8R3",
      "message_status": "accepted"
    }
  ]
}

contacts[].phiz_id is filled only if the user has already talked to this sender. messages[].id is the key to match status webhooks.

More examples

The same send, through a channel

BASH
# sender_id = 1231: same request, sent through the merchant's channel
curl -X POST "$PHIZ_API_BASE/v1/1231/messages" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+5511987654321", "type": "text", "text": { "body": "Novidades da semana na Loja Exemplo - Ofertas" } }'

Text to a PHIZ ID

JSON
{
  "to": "839201447",
  "to_type": "phiz_id",
  "type": "text",
  "text": {
    "body": "Seu cupom chegou: PHIZ10"
  }
}

Media upload

BASH
# 1. Upload the file to PHIZ (multipart/form-data). Third-party URLs are not accepted when sending.
curl -X POST "$PHIZ_API_BASE/v1/7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57/media" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -F messaging_product=phiz \
  -F type=application/pdf \
  -F file=@nota-fiscal-48213.pdf
JSON
{
  "id": "pmed.01JB2K7Q9X",
  "link": "https://media.phiz.chat/biz/pmed.01JB2K7Q9X",
  "mime_type": "application/pdf",
  "file_size": 184320,
  "sha256": "9b1f…",
  "expires_at": "2026-11-19T14:00:00Z"
}

Audio by media id

JSON
{
  "to": "+5511987654321",
  "type": "audio",
  "audio": {
    "id": "pmed.01JB2K4M7A"
  }
}

Video by PHIZ link, to a PHIZ ID

JSON
{
  "to": "839201447",
  "to_type": "phiz_id",
  "type": "video",
  "video": {
    "link": "https://media.phiz.chat/biz/pmed.01JB2K5V1C",
    "caption": "Como montar seu produto"
  }
}

Document with a file name

JSON
{
  "to": "+5511987654321",
  "type": "document",
  "document": {
    "id": "pmed.01JB2K7Q9X",
    "filename": "nota-fiscal-48213.pdf",
    "caption": "Nota fiscal do pedido #48213"
  }
}

Reply buttons

JSON
{
  "to": "+5511987654321",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": {
      "text": "Confirma a entrega amanhã entre 9h e 12h?"
    },
    "action": {
      "buttons": [
        {
          "type": "reply",
          "reply": {
            "id": "confirmar",
            "title": "Confirmar"
          }
        },
        {
          "type": "reply",
          "reply": {
            "id": "reagendar",
            "title": "Reagendar"
          }
        }
      ]
    }
  }
}

Mark an inbound message as read

JSON
{
  "messaging_product": "phiz",
  "status": "read",
  "message_id": "pmid.IN_01JA9W2C"
}

Supported types

typeContentPhase
textUp to 4,096 characters; optional preview_urlMVP
imageBy id or PHIZ link (upload first); optional captionMVP
audioBy id or PHIZ link (upload first); no captionMVP
videoBy id or PHIZ link (upload first); optional captionMVP
documentBy id or PHIZ link (upload first); optional caption and file nameMVP

Media: upload to PHIZ and temporary storage

All media goes through PHIZ storage. Sending takes two steps:

  1. Upload. The BSP sends the file to POST /v1/{sender_id}/media (multipart/form-data). PHIZ checks format and size, stores the file and returns an id and a PHIZ link, with the expiry date in expires_at.
  2. Send. In the message, the media object carries the id (recommended) or that link. The user's app downloads the file from PHIZ storage.
  • Only links generated by PHIZ. In Phase 1 the API rejects third-party URLs (the merchant's or partner's CDN) with error 2302. This way PHIZ guarantees the link is safe and the file stays reachable until delivery, and the app does not expose the user's IP to outside servers.
  • Temporary storage. The file is kept only as long as sending and delivery need it: proposed 30 days from upload, like the Cloud API media id (see Q8). Within that period the same id can be used in many sends, for example the same PDF to many customers. After it, the id and the link stop working and new sends fail with 2303. The merchant can delete it earlier with DELETE /v1/media/{media_id}.
  • Checked at upload. Format and size are checked at upload, not at delivery: files outside the table below are rejected right away (2304 and 2305).
  • Scope. Upload, lookup and deletion use the messages:send scope, and media can only be used by senders of the same merchant account.
Decision (v1.3)

In Phase 1, media only by upload to PHIZ, with temporary storage; third-party links are not accepted. Allowing third-party URLs for specific customers, for example with an allow-list of domains, is left for a later phase. Agreed with Henry Wu in the comments on this section.

TypeAccepted formatsMax size
imageJPEG, PNG, WebP5 MB
audioAAC, MP3, M4A, OGG (Opus), AMR16 MB
videoMP4 (H.264 + AAC), 3GP16 MB
documentPDF, DOC/DOCX, XLS/XLSX, PPT/PPTX, TXT, CSV100 MB

Formats and sizes the PHIZ app should accept, inspired by the Cloud API limits and set by this RFC. Conversion and compression are up to whoever sends the file; the upload rejects anything outside the table. They must be confirmed with the team that owns media in the app (see Q8).

Sending policy (consent)

Proposal inspired by WhatsApp (see section 03), with the opt-in flows detailed in section 04:

  • Starting a conversation requires opt-in from the user for that merchant (merchant_id) and category: account alerts, offers or codes. Replying within 24 h of a user message needs no prior opt-in.
  • Opt-in can be collected outside PHIZ (the merchant's app, website or store) or inside the PHIZ app.
  • Opt-out enforced by the platform: if the user blocks the merchant or stops offers, sends in that category fail with an error, without relying on the merchant.
  • Record required: the merchant keeps proof of each consent (user, merchant_id, category, source, date, wording shown and revocation). Under the LGPD, whoever processes the data must prove consent and honor withdrawal.
Product decision

Meta has an opt-in policy but does not verify it: it trusts the merchant and reacts afterwards, through reports and the quality rating. We need to choose between A, the same model (policy plus a record kept by the merchant), and B, a consent registry in PHIZ, checked before every send. See Q1.

Idempotency

Send an Idempotency-Key with every message. The same key within 24 h returns the original response without sending again. The same key with a different body returns 409. This makes retries after a timeout or a 5xx safe.

10Webhooks

Each app has one webhook subscription with a URL, a verify_token and a list of events.

Decision (v1.4)

Phase 1: the webhook URL is configured manually by PHIZ, based on the business relationship with the merchant or partner. Configuration through the Developer area and the API (the endpoints below) comes in Phase 2, when the Open Platform opens up. Agreed with Henry Wu in the comments on this section.

Manual setup in Phase 1

  • The merchant or partner asks for the setup through their business contact at PHIZ and provides the webhook URL and the events they want (messages, message_status).
  • PHIZ registers the URL on the server and checks it with the same GET handshake described below (proposal).
  • The event format, the signature and the handshake are the same as in Phase 2; only who configures it changes.

Setup through the API (Phase 2)

These endpoints will only be exposed in Phase 2, together with the Developer area.

PUT/v1/apps/{app_id}/webhookSets URL, verify_token and events (Phase 2)
GET/v1/apps/{app_id}/webhookSettings and health (Phase 2)
POST/v1/apps/{app_id}/webhook/testFires a signed test event (Phase 2)
GET/v1/apps/{app_id}/webhook/deliveriesDelivery history, 72 h (Phase 2)
POST/v1/apps/{app_id}/webhook/deliveries/{id}/replayReplays a delivery (Phase 2)
BASH
curl -X PUT "$PHIZ_API_BASE/v1/apps/app_2Fk81PzQ/webhook" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://integracao.lojaexemplo.com.br/phiz/webhook",
    "verify_token": "um-texto-que-so-voce-conhece",
    "fields": ["messages", "message_status"]
  }'

Example of the Phase 2 setup call.

Events

fieldWhen
messagesThe user sent a message, tapped a button or reacted
message_statusA sent message changed to sent, delivered, read or failed
sender_updateA sender's status, quality or limit changed (merchant or channel)
account_updateThe merchant account changed (verification, restriction)

URL verification

Before activating, PHIZ sends a GET to the URL with hub.mode=subscribe, hub.verify_token and hub.challenge. The server checks the token and answers 200 with the value of hub.challenge. It is the same handshake Meta uses.

Signature

Every POST carries the headers below. The signature is an HMAC-SHA256 of {X-Phiz-Timestamp}.{raw body} using the app's webhook secret, which is separate from the client secret. Reject events whose timestamp is more than 5 minutes off. While the webhook secret is being rotated, the header carries two comma-separated signatures.

HTTP
POST /phiz/webhook HTTP/1.1
Host: integracao.lojaexemplo.com.br
Content-Type: application/json
User-Agent: PHIZ-Webhooks/1.0
X-Phiz-Delivery-Id: dlv_01JA9W2D7K
X-Phiz-Timestamp: 1760528527
X-Phiz-Signature-256: sha256=6f1c0e…
JAVASCRIPT
// Express: verification handshake + signed events
import express from "express";
import crypto from "node:crypto";

const app = express();
app.use(express.raw({ type: "application/json" })); // keep the raw body for the HMAC

app.get("/phiz/webhook", (req, res) => {
  const ok = req.query["hub.mode"] === "subscribe" &&
             req.query["hub.verify_token"] === process.env.PHIZ_VERIFY_TOKEN;
  return ok ? res.status(200).send(req.query["hub.challenge"]) : res.sendStatus(403);
});

app.post("/phiz/webhook", (req, res) => {
  const ts = Number(req.get("X-Phiz-Timestamp"));
  if (Math.abs(Date.now() / 1000 - ts) > 300) return res.sendStatus(400);

  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.PHIZ_WEBHOOK_SECRET)
    .update(`${ts}.${req.body}`)
    .digest("hex");
  const received = (req.get("X-Phiz-Signature-256") || "").split(",");
  const valid = received.some(sig => sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
  if (!valid) return res.sendStatus(401);

  queue.push({ deliveryId: req.get("X-Phiz-Delivery-Id"), event: JSON.parse(req.body) });
  res.sendStatus(200); // answer fast, process asynchronously
});
PYTHON
# Flask: signature check
import hmac, hashlib, time, os
from flask import request, abort

def verify_phiz_signature() -> None:
    ts = int(request.headers["X-Phiz-Timestamp"])
    if abs(time.time() - ts) > 300:
        abort(400)
    msg = f"{ts}.".encode() + request.get_data()  # raw bytes, before JSON parsing
    expected = "sha256=" + hmac.new(os.environ["PHIZ_WEBHOOK_SECRET"].encode(), msg, hashlib.sha256).hexdigest()
    received = request.headers.get("X-Phiz-Signature-256", "").split(",")
    if not any(hmac.compare_digest(sig, expected) for sig in received):
        abort(401)

Inbound message

JSON
{
  "object": "phiz_merchant_account",
  "entry": [
    {
      "id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
      "changes": [
        {
          "field": "messages",
          "value": {
            "messaging_product": "phiz",
            "metadata": {
              "sender_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
              "sender_type": "merchant",
              "conversation_type": "business_account",
              "display_name": "Loja Exemplo"
            },
            "contacts": [
              {
                "profile": {
                  "name": "Maria Souza"
                },
                "phiz_id": "839201447",
                "phone": "+5511987654321"
              }
            ],
            "messages": [
              {
                "from": "839201447",
                "id": "pmid.IN_01JA9W2C",
                "timestamp": "1760528527",
                "type": "text",
                "text": {
                  "body": "Quero reagendar a entrega"
                },
                "context": {
                  "id": "pmid.01JA9VZ6T8R3"
                }
              }
            ]
          }
        }
      ]
    }
  ]
}

Inbound message with media

When the user sends an image, audio, video or document, the event carries a temporary url pointing to the file in the current chat storage. The messaging API does not copy or keep the media; download the file while the link is valid. Below is the messages[] item of a voice message:

JSON
{
  "from": "839201447",
  "id": "pmid.IN_01JA9W7R",
  "timestamp": "1760528731",
  "type": "audio",
  "audio": {
    "url": "https://media.phiz.chat/in/9xT2…?expires=1760615131&sig=…",
    "mime_type": "audio/ogg; codecs=opus",
    "sha256": "3f9a…",
    "voice": true
  }
}

Status of a sent message

JSON
{
  "object": "phiz_merchant_account",
  "entry": [
    {
      "id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
      "changes": [
        {
          "field": "message_status",
          "value": {
            "messaging_product": "phiz",
            "metadata": {
              "sender_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
              "sender_type": "merchant",
              "conversation_type": "business_account",
              "display_name": "Loja Exemplo"
            },
            "statuses": [
              {
                "id": "pmid.01JA9VZ6T8R3",
                "status": "delivered",
                "timestamp": "1760528410",
                "recipient_id": "839201447",
                "biz_opaque_callback_data": "pedido=48213",
                "conversation": {
                  "id": "conv_01JA9W",
                  "origin": {
                    "type": "service"
                  },
                  "expires_at": "1760614810"
                }
              }
            ]
          }
        }
      ]
    }
  ]
}
JSON
{
  "object": "phiz_merchant_account",
  "entry": [
    {
      "id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
      "changes": [
        {
          "field": "message_status",
          "value": {
            "messaging_product": "phiz",
            "metadata": {
              "sender_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
              "sender_type": "merchant",
              "conversation_type": "business_account",
              "display_name": "Loja Exemplo"
            },
            "statuses": [
              {
                "id": "pmid.01JA9X0QW1",
                "status": "failed",
                "timestamp": "1760528600",
                "recipient_id": "+5511900000000",
                "errors": [
                  {
                    "code": 2104,
                    "title": "recipient_not_reachable",
                    "message": "The phone number is not linked to a PHIZ account or blocked this sender.",
                    "href": "https://developers.phiz.chat/docs/errors#2104"
                  }
                ]
              }
            ]
          }
        }
      ]
    }
  ]
}
stateDiagram-v2
  direction LR
  [*] --> accepted: API returns 202
  accepted --> sent: reached PHIZ servers
  sent --> delivered: reached the device
  delivered --> read: user opened it
  accepted --> failed
  sent --> failed
  read --> [*]
  failed --> [*]

Status lifecycle. accepted appears only in the API response and in GET /v1/messages/{id}; the others arrive by webhook.

Delivery guarantees

Phase 1. The goal is to get the key points live and in use; the safeguards below come later.

  • Duplicates are possible. The same event may arrive more than once; use X-Phiz-Delivery-Id to drop duplicates.
  • No ordering guarantee. A read may arrive before delivered; order by timestamp and never move a status backwards.
  • Answer within 10 s with a 2xx and process afterwards. Any other response, or a timeout, counts as a failure.
  • For the status of sent messages, GET /v1/messages/{message_id} is the fallback if a webhook is lost.
Phase 1 risk

Without the full retry policy, an event that fails to deliver may not be sent again, and received messages have no fallback lookup. The minimum for Phase 1 will be set by the technical review (see Q9).

Phase 2 (safeguards). Guarantee mechanisms left for future development:

  • At least once. Delivery with retries and exponential backoff (10 s, 30 s, 1 min, 5 min, 15 min, 1 h, then hourly) for up to 24 h.
  • Manual replay. After the 24 h, the event stays available for replay for 72 h, together with the delivery history (/webhook/deliveries).
  • failing webhook. If the URL keeps failing for 24 h, the webhook moves to failing and the merchant's administrators are notified.

11Errors, limits and versions

Outside /oauth/*, every error uses one envelope:

JSON
{
  "error": {
    "code": 1003,
    "type": "invalid_parameter",
    "message": "Field 'to' must be E.164 when to_type is 'phone'.",
    "details": [
      {
        "field": "to",
        "issue": "format"
      }
    ],
    "trace_id": "trc_9f2c41ab"
  }
}
codeHTTPtypeMeaning
1001400invalid_requestMalformed JSON or missing required field
1003400invalid_parameterInvalid value (see details)
1101401token_expiredToken expired; request a new one
1102401invalid_tokenInvalid or revoked token
1201403insufficient_scopeThe token lacks the required scope
1202403sender_not_allowedThe integration is not allowed to use this sender
1301404not_foundResource does not exist or belongs to another merchant
1401409idempotency_conflictSame Idempotency-Key with a different body
1501429rate_limitedRate limit reached; honour Retry-After
2104webhookrecipient_not_reachableNo PHIZ account for the number, or the user blocked the sender
2201422policy_violationSending not allowed by the opt-in policy
2202422unsupported_typeMessage type not enabled for the sender
2301webhookmedia_unavailableThe app could not download the media from PHIZ storage (media expired or deleted before delivery)
2302400media_link_not_allowedThe link was not generated by PHIZ; upload the file and use the returned id or link
2303422media_expiredThe media id or link has expired, was deleted or belongs to another merchant
2304415unsupported_media_typeFile format not accepted at upload
2305413media_too_largeFile above the maximum size for its type
2401403sender_disabledSender disabled or restricted
2501429messaging_limit_reachedThe sender's daily recipient limit was reached
5000500internal_errorInternal error; retry with the same Idempotency-Key

Limits (initial proposal)

ResourceLimit
/oauth/token60 req/min per app
Sending messages80 msg/s per sender (adjustable by contract)
Other endpoints600 req/min per app
Unique recipients of business-initiated messagesPer-sender tiers: 1k → 10k → 100k → unlimited in 24 h, based on quality (Phase 2)
HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 2
X-RateLimit-Limit: 80
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1760528529

Environments

  • Production: https://{gateway}/gateway/business
  • Sandbox: https://{test-gateway}/gateway/business, in the test environment used today (Openapi-test), with one test sender per merchant and up to 5 registered recipients (like Meta's test number). phzsk_test_ secrets only work here.

Route prefix

The new API runs on the same gateway as the current API, separated by prefix:

  • /gateway/openapi/…: the current API, kept during the migration.
  • /gateway/business/…: this API (authentication, messages and webhooks).

With separate prefixes, each context has its own routing, authentication, limits and logs, and the legacy API can be shut down without affecting the new one. Every path in this document, such as /oauth/token and /v1/{sender_id}/messages, is relative to https://{gateway}/gateway/business. In the examples this address appears as $PHIZ_API_BASE.

12Developer area in the Open Platform

Developer settings move out of Channel › Development Settings into a merchant menu item, Developer, next to Settings. It reuses the existing Merchant ID, the roles from Team management and the Operation log.

Mockups of the new area, with sample values. Click an image to open it at full size.

Overview: Merchant ID, base URL, integrations and first steps

Screen 1. Overview: Merchant ID, base URL, integrations and first steps

New integration: name, scopes, allowed senders and environment

Screen 2. New integration: name, scopes, allowed senders and environment

Integration details: credentials, secrets with rotation and scopes

Screen 3. Integration details: credentials, secrets with rotation and scopes

Secret created, shown only once

Screen 4. Secret created, shown only once

Webhooks: setup, events and recent deliveries

Screen 5. Webhooks: setup, events and recent deliveries (Phase 2)

Senders: direct message and channels, with the integrations that use them

Screen 6. Senders: direct message and channels, with the integrations that use them

Channel page with the credentials migration notice

Screen 7. Channel page with the credentials migration notice

Screens

Overview
/developer
  • Messages sent, delivered and failed in the last 24 h
  • Webhook health (Phase 2)
  • Alerts: old secret still in use; webhook failing (Phase 2)
Apps
/developer/apps
  • Create app: name, integration type (own, Blip, Salesforce, other), environment
  • Scopes and allowed senders
  • Client ID, secrets with rotation and last use
  • IP allowlist (optional)
  • Disable or delete app
App webhook (Phase 2)
/developer/apps/:id/webhook
  • In Phase 1, PHIZ registers the URL manually (section 10)
  • URL, verify token and events
  • Webhook secret with rotation
  • Verify and send a test
  • Deliveries from the last 72 h with HTTP status, attempts, latency and replay
Senders
/developer/senders
  • The merchant itself, always present, plus channels if any
  • Each with sender_id, status, limit and quality
  • Picks the integration that receives each sender's conversations
  • For channels, turns on “Phiz Business API” mode
API logs
/developer/logs
  • Requests from the last 7 days: method, route, status, latency, trace_id
  • Filter by app and error code
Sandbox
/developer/sandbox
  • Test sender
  • Up to 5 test recipients
  • phzsk_test_ credentials

Permissions

New permissions are added to the existing Role management screen. The Super administrator gets all of them.

PermissionAllows
developer.viewView integrations, senders, logs and deliveries (no secrets)
developer.apps.manageCreate, edit and disable integrations; set scopes and senders
developer.secrets.manageGenerate, rotate and revoke secrets
developer.webhooks.manageSet up the webhook, test it and replay deliveries (Phase 2)

Creating, rotating or revoking a secret, changing scopes and editing the webhook all add an entry to the Operation log and email the administrators.

A sender with more than one integration

To keep two systems from answering the same user, each sender (the merchant or a channel) has one owning integration for conversations, chosen on the Senders screen. Only that integration receives the sender's messages event. Any integration with permission can send through it and receives message_status for the messages it sent. A conversation handover protocol between integrations (like Messenger's handover) is left for later.

Migrating channel credentials

  1. Automatic creation. For each channel with an AppID we create a “Legacy – channel name” app in the merchant, reusing the AppID as client_id, the stored secret, and permission for that channel only. No integration breaks.
  2. Coexistence. /gateway/openapi/auth and /token/refresh keep working. The channel page shows a notice pointing to the new area.
  3. Read only. After 3 months the channel page no longer allows resetting the secret; management happens only in the new area.
  4. Shutdown. After 6 months, and once measured usage is near zero, the old endpoints answer 410 Gone.

13Partner integrations

In the MVP, the merchant creates an app in the Developer area and pastes the client_id and secret into Blip, Salesforce or their own system. Webhook URLs come from the partner and, in Phase 1, PHIZ registers them manually (section 10). Because the contracts follow the Cloud API, partners that already have a WhatsApp connector can reuse most of it.

In a later phase we create a technology partner program, equivalent to Meta's Embedded Signup:

  • The partner registers a platform app with PHIZ.
  • The merchant clicks “Connect with PHIZ” inside the partner's product and grants scopes and senders on a consent screen (OAuth 2.0 authorization code + PKCE).
  • The partner receives tokens limited to that merchant and a single webhook for all its customers. The envelope's entry[].id identifies the merchant, which is why the envelope has that structure from day one.

14Path to the Marketing API

This RFC reserves the space the Marketing API will occupy, without implementing it:

  • type: template for sending and the templates:manage scope already exist in the contract.
  • Templates with approval: /v1/merchants/{merchant_id}/message_templates and a template_status webhook event.
  • Conversation categories (conversation.origin.type: service, utility, marketing), the basis for reporting and pricing.
  • Per-sender opt-in and opt-out management, campaigns to segments and metrics at /v1/merchants/{merchant_id}/analytics.
  • Messaging limit tiers and a quality rating per sender, already present in the Sender resource.

15Delivery plan

PHASE 1 · MVP
Authenticate and send
  • /oauth/token, /oauth/revoke, /v1/me
  • Send text, image, audio, video and document to phone and PHIZ ID
  • Media upload (/v1/{sender_id}/media) with temporary storage; no third-party links
  • Signed messages and message_status webhooks, with the URL registered manually by PHIZ
  • Developer area: apps and secrets
  • Default sender (the merchant itself) for everyone; channels as optional senders
  • Automatic migration of channel credentials
PHASE 2
Self-service webhook and safeguards
  • Webhook setup through the Developer area and the API (PUT /v1/apps/{app_id}/webhook, test and health)
  • Retries with exponential backoff for up to 24 h
  • Delivery history and manual replay for 72 h
  • failing status with a notice to administrators

Dates will be set after this RFC is reviewed with the China team.

16Open questions

  1. Opt-in policy. Section 09 proposes the policy (opt-in per merchant and category, opt-out enforced by the platform, record kept by the merchant). Still to decide: model A (like Meta, no verification) or B (a consent registry in PHIZ, for example /v1/merchants/{merchant_id}/consents, checked on send); which categories exist; whether the PHIZ app shows users what they authorized; and how a user blocks or stops offers from a Business Account (see Q3). Validate the LGPD points with Legal. Applies to Business Account and Channel Service.
  2. Sender IDs. Use the merchant_id and the channel ID directly as sender_id, or create new opaque IDs? Reusing existing IDs simplifies migration, but exposes the channel's sequential ID.
  3. Business Account conversation type. Confirm the path Public Accounts › Service › Business Account and its definition: a two-way service conversation for businesses, merchants, governments and institutions, allowing user messages and a configurable Menu, with proactive messages subject to authorization and messaging rules. Does this type already exist in the app or does it need to be built? How does the user block or mute a Business Account?
  4. Current conversation types. Review the hierarchy and the permissions table (v1.2): which types exist today, which current rules differ, which types do not exist and which are missing.
  5. Phone numbers. Do all PHIZ users have a verified phone number? How do we handle foreign numbers and number changes?
  6. Is one owning integration per sender enough, or do we need conversation handover between apps (bot → human agent) already in Phase 2?
  7. Existing Business Message API. The AI Robot's “Phiz Business API” mode already has send and callback endpoints and seems to match Channel Service with Channel Bot. Does this RFC replace, extend or wrap that API?
  8. Media. Decided in v1.3: media only by upload to PHIZ, with temporary storage, and no third-party links in Phase 1 (section 09). Still to define: does the PHIZ app play every format in the media table, and at which sizes? Is 30 days the right retention, or should it be shorter? After delivery, does the media move to the chat's normal storage, or disappear from the user's history when it expires? When, and for which customers, do we allow third-party URLs? How long does the url of received media stay valid?
  9. Webhook in Phase 1. Decided in v1.4: PHIZ registers the URL manually, and retries, manual replay and the failing status are left for Phase 2 (section 10). Still to define: how the setup request is made, which PHIZ team carries it out and how fast; how the webhook secret reaches the partner; the Phase 1 minimum for retries (none or a short window); how the partner recovers lost received messages, since only status has a lookup; and what triggers Phase 2.

17OpenAPI reference

The OpenAPI 3.1 specification below covers every endpoint and webhook in this RFC, with descriptions in this language. “Try it out” is turned off because the API does not exist yet. Use “Copy OpenAPI” to take the YAML into Apifox, Postman or an SDK generator.

RFC PHZ-0001 · 评审草案

PHIZ Business Messaging API

商户级鉴权、向手机号或 PHIZ ID 发送消息,以及消息与状态 Webhook。参考 Meta Cloud API 设计,并作为后续 Marketing API 的基础。

状态
草案
作者
Gleison Silva - 产品解决方案
日期
2026-10-15
评审人
Pedro Bettani(巴西)
Vitor Mozer(巴西)
Richard Liang(中国)
API 版本
v1(拟)

01概要

本文提出一套公开 API,使外部系统(Blip、Salesforce、各类 CRM 或商户自有系统)能够代表商户与 PHIZ 用户对话。方案包含四个部分:

  • OAuth 2.0 鉴权(客户端凭证模式),凭证签发给商户账号,不再绑定频道。
  • 发送消息:直接以商户名义向手机号(E.164)或 PHIZ ID 发送消息,是否通过频道发送为可选项。
  • 带签名的 Webhook:推送用户发来的消息,以及已发送消息的每一次状态变化。
  • 开放平台中商户级的开发者中心:商户将每个调用 API 的外部系统(如 Blip 或 Salesforce)登记为一个集成。每个集成拥有独立的 client_id 和密钥、Webhook 及投递记录。集成与小程序(Miniapp)无关。

消息与 Webhook 的数据结构沿用 Meta Cloud API,仅替换领域名称。已对接 WhatsApp Business Platform 的系统,只需修改 URL、凭证和少量字段名即可适配。

02背景:现状

以下内容基于现有文档(Apifox 的 Authentication 模块)和开放平台(open.phiz.chat)。

现有鉴权方式

通过 POST /gateway/openapi/auth 获取令牌,参数为 app_id、app_secret、description(必填)和 grant_type: "user_credentials"。响应包含约 2 小时有效的访问令牌和约 90 天有效的刷新令牌,刷新接口为 POST /gateway/openapi/token/refresh。

HTTP
POST /gateway/openapi/auth
Content-Type: application/json

{
  "app_id": "69c37cb1821121af34201f48",
  "app_secret": "94B18B920D19463C7264BD0BDE30C31…",
  "description": "push news",
  "grant_type": "user_credentials",
  "device_fingerprint": ""
}

200 OK
{
  "code": 200,
  "msg": "success",
  "data": {
    "scope": "channel",
    "status": "active",
    "access_token": "at_1770343926041_1Y3iz6XN…",
    "token_type": "Bearer",
    "expires_in": 7199,
    "refresh_token": "rt_1770343926041_mxc7RV9…",
    "access_token_expires_at": "2026-02-06T04:…",
    "refresh_token_expires_at": "2026-05-07T02:…",
    "created_at": "2026-02-05T10:15:23.672+00:00"
  },
  "success": true
}

凭证所在位置

在开放平台中,每个频道都有 频道 › 开发配置 页面,显示 AppID 和 AppSecret。密钥不会明文显示,只能重置。频道的 AI 机器人 中还提供 “Phiz Business API” 回复模式,将频道会话交给 Business Message API 发送并通过 Webhook 回调。

商户层面已有商户 ID(UUID),以及包含角色、成员和操作记录的团队管理。新功能将以这三者为基础。

本 RFC 带来的变化

每一行分别说明现状、存在的问题、本方案的做法,以及一个具体使用场景。

现状问题所在本 RFC 的做法使用场景
商户要通过 API 联系用户,必须拥有频道。没有频道、或希望以企业本身身份沟通的商户,无法直接给用户发消息。商户可以以自身名义直接发送消息,频道变为可选项。一家没有频道的药店通过手机号通知顾客 Maria 订单已可取货。消息出现在她与药店的直接对话中。
每个频道都有自己的 AppID 和密钥。有 5 个频道的商户要管理 5 套凭证,Blip 需要为每个频道单独获取令牌。每个集成一套凭证,归属商户账号,可使用直接发送及选定的频道。Loja Exemplo 有 3 个频道并使用 Blip。现在要在 Blip 中录入 3 个 AppID;采用本 RFC 后,只需创建一个 “Blip” 集成,勾选直接发送和这 3 个频道。
“重置”会立即使密钥失效。在外部系统更新密钥之前,集成一直不可用。两个密钥可同时有效:新密钥生效后,旧密钥在一段时间内仍可用(默认 24 小时)。Salesforce 的密钥出现在日志中。团队生成新密钥,下午更新 Salesforce,当天结束时撤销旧密钥,没有丢失任何消息。
登录使用 grant_type: user_credentials,并要求填写 description。Salesforce 等原生支持 OAuth 的工具,必须写定制代码才能接入。标准 OAuth 2.0(client_credentials),这些工具开箱即用。Salesforce 管理员在 Named Credentials 中只需填写令牌 URL、client_id 和密钥即可连接 PHIZ,无需开发人员。
存在有效期约 90 天的刷新令牌。多了一个需要保管的长期密钥,对服务端之间的集成没有好处。1 小时令牌,不提供刷新令牌;过期后重新申请。Blip 每小时用同一组 client_id 和密钥自动续期令牌,不会出现刷新令牌在周末过期、导致客服中断的情况。
没有配置 Webhook、查看日志或管理多个集成的页面。商户无法自行配置 Webhook,也无法自行排查集成为何中断。开发者中心:独立的集成、Webhook、投递记录和日志。顾客回复不再进入 Blip。商户打开投递记录,发现 Webhook 从上午 10 点起一直返回 500,修复后重放这些事件。

03参考:WhatsApp 上的用户体验

本 RFC 将 WhatsApp Business Platform 的模式引入 PHIZ。为了让巴西和中国团队对“我们要复刻的是什么”有相同的理解,本节展示一位巴西用户在 WhatsApp 上的完整旅程:如何注册账号、如何授权一家企业、消息如何到达,以及收到后能做什么。示例:商户 PicPay,用户 Ana Souza(圣保罗)。

关于画面

这些是示意图,并非 WhatsApp 官方界面。画面文字已译为中文(真实应用中用户看到的是葡萄牙语)。编号(A2、O1、C3…)与本节来源的设计画布一致。点击图片可查看原尺寸。

消息路径

企业不直接对接 WhatsApp:企业系统(CRM、客服)调用 BSP(Blip、Zenvia、Twilio、Infobip),BSP 调用 Meta 的 Cloud API,再投递到用户手机。回复、点击和状态沿同一路径通过 Webhook 返回。在 PHIZ 中,PHIZ 商家消息 API 对应 Cloud API,商户账户(merchant_id)对应 WABA(对照见第 06 节)。

企业 → BSP → Meta Cloud API → WhatsApp → 用户;opt-in 是前提,Webhook 为返回路径

B01. 企业 → BSP → Meta Cloud API → WhatsApp → 用户;opt-in 是前提,Webhook 为返回路径

用户注册

在 WhatsApp 上,账号就是经短信验证的手机号。没有好友申请:知道号码就能发消息。企业和家人在同一个收件箱里,因此企业消息受到严格管控。自 2026 年起,WhatsApp 提供用户名和企业范围用户 ID(BSUID)来替代手机号,这与本 RFC 中使用 PHIZ ID 的思路一致。

手机号即账号身份

A02. 手机号即账号身份

所有对话共用一个收件箱

A10. 所有对话共用一个收件箱

用户授权(opt-in)

企业主动发起会话之前,用户必须已授权该企业。常见方式有四种:在企业自己的 App 中(O1)、注册或结账时(O2)、用户先发消息,仅覆盖这次客服会话(O3),以及在聊天中回答“是”(O4)。最佳实践是按用途分开:同意账户提醒不等于同意营销优惠。

同意的生命周期:收集、记录、在授权范围内发送、撤回

O00. 同意的生命周期:收集、记录、在授权范围内发送、撤回

在商户 App 中 opt-in,按用途分开

O01. 在商户 App 中 opt-in,按用途分开

注册时 opt-in,复选框默认不勾选

O02. 注册时 opt-in,复选框默认不勾选

用户先发消息:仅授权本次客服

O03. 用户先发消息:仅授权本次客服

在聊天中用按钮请求 opt-in

O04. 在聊天中用按钮请求 opt-in

Meta 有政策,但不验证 opt-in。没有用于证明授权的字段或接口;Meta 信任企业,只在事后通过屏蔽、举报和质量评级作出反应。而 opt-out 由平台强制执行:屏蔽、“停止优惠”和营销上限都会使发送失败。尽管如此,根据巴西 LGPD,企业必须能证明已获得同意并尊重撤回,因此实际上必须保存记录(谁、商户、类别、来源、时间、展示的文案、撤回)。

商户侧的同意记录

O05. 商户侧的同意记录

Meta 不验证 opt-in;opt-out 由平台执行;为何仍需记录

O06. Meta 不验证 opt-in;opt-out 由平台执行;为何仍需记录

用户收到什么

企业主动发送的消息使用经审核的模板,分四类:交易通知(付款、账单)、营销、验证码和客服。每一类都依赖对应的 opt-in。

通知与私人联系人的通知完全相同

C01. 通知与私人联系人的通知完全相同

聊天列表:已认证的企业账号未读并置顶,与家人朋友的对话并列

C02. 聊天列表:已认证的企业账号未读并置顶,与家人朋友的对话并列

交易通知:收到 Pix,带按钮

C03. 交易通知:收到 Pix,带按钮

营销:需要单独的优惠 opt-in

C04. 营销:需要单独的优惠 opt-in

验证码:带复制按钮

C05. 验证码:带复制按钮

账单提醒,附 Pix 复制粘贴付款码

C06. 账单提醒,附 Pix 复制粘贴付款码

用户能做什么

用户可以回复(文字、按钮、列表、表单)、查看企业资料,或撤回授权。每个操作可能会、也可能不会向企业产生事件。

用户的所有选项,以及企业收到的反馈

D00. 用户的所有选项,以及企业收到的反馈

快捷回复按钮

D02. 快捷回复按钮

列表消息

D04. 列表消息

聊天内表单(Flows),接近小程序

D05. 聊天内表单(Flows),接近小程序

停止优惠,保留提醒

D07. 停止优惠,保留提醒

屏蔽并举报,附原因

D08. 屏蔽并举报,附原因

关键词退订,由商户处理

D10. 关键词退订,由商户处理

用户操作WhatsAppPHIZ
回复文字或媒体messages Webhook;开启 24 小时窗口message 事件(第 10 节)
点击按钮或列表项button_reply / list_reply带所选 id 的 message 事件
停止优惠营销消息发送失败提议:平台对该 merchant_id 屏蔽此类别
屏蔽或举报所有发送失败;质量评级下降提议:发送报错并产生质量信号(见 Q3)
静音或归档企业不可见相同

对 PHIZ 的启示

  • 整个流程中企业的身份是商户 ID(merchant_id):opt-in、屏蔽和质量评级都归属于它。不存在单独的开发者 ID;集成和应用只是代表商户执行操作。
  • 按商户、按类别管理同意,opt-out 由平台执行(见第 09 节发送策略)。
  • 用户信号(屏蔽、举报、停止优惠)影响商户的发送额度。
  • 聊天内表单(Flows)可作为小程序的参考。

04PhizChat 中的 opt-in:应用内与企业侧

第 03 节说明了在 WhatsApp 上 opt-in 几乎都发生在应用之外,而且 Meta 并不核实。本节提议用户在 PhizChat 中如何授权企业账号,共有两条路径,最终都使用相同的退订控制:

  • 流程 P——在 PhizChat 内:用户在应用中直接接受企业账号,由平台保存证明。
  • 流程 Q——在企业侧、PhizChat 之外:用户在商户的 App、网站或门店中授权,由商户发起对话。
关于画面

这些是示意性提议,以 PicPay 作为示例商户。编号(P0…P8、Q0…Q5)与设计画布一致。点击图片可查看原尺寸。

流程 P · 在 PhizChat 内 opt-in

用户通过短信或邮件链接、二维码、商户 App 或网站,或应用内搜索找到企业账号。未安装 Phiz 时,只会看到带资料和下载按钮的 H5 页面;接受永远不会在应用之外发生。安装并登录后,延迟深度链接把用户带回同一个对话,该对话在接受之前保持锁定。用户接受后,PhizChat 记录同意(用户、merchant_id、日期、入口),如有欢迎模板则发送,并解锁对话。

流程 P 总览:入口、安装、登录、接受及之后

P00. 流程 P 总览:入口、安装、登录、接受及之后

未安装应用:H5 页面只有资料和下载,没有“接受”按钮

P01. 未安装应用:H5 页面只有资料和下载,没有“接受”按钮

带同意卡片的企业账号对话;接受之前输入框锁定

P02. 带同意卡片的企业账号对话;接受之前输入框锁定

接受之后:欢迎消息,对话解锁

P03. 接受之后:欢迎消息,对话解锁

同意之前的企业资料页,同样提供“接受”

P04. 同意之前的企业资料页,同样提供“接受”

同意确认弹窗

P05. 同意确认弹窗

接受之后,资料页显示同意状态和日期。用户可以撤回(对话回到锁定状态,之后可以重新授权),或屏蔽(沿用现有屏蔽功能;API 仍可调用,但会返回已屏蔽状态)。

同意之后的资料页:状态、日期和“撤回同意”

P06. 同意之后的资料页:状态、日期和“撤回同意”

确认撤回;对话重新锁定

P07. 确认撤回;对话重新锁定

已屏蔽的企业账号:API 返回已屏蔽状态

P08. 已屏蔽的企业账号:API 返回已屏蔽状态

流程 Q · 企业在 PhizChat 之外收集的 opt-in

大多数巴西企业已经在自己的渠道中收集同意。在流程 Q 中,用户在 PicPay App 中开启“PhizChat 消息”;PicPay 保存证明,并通过 API 发送模板。API 检查收件人是否为 Phiz 用户、是否已屏蔽或撤回;在模式 B 中,还会检查该类别是否有已登记的同意。在应用中,第一条消息会附带同意来源卡片(授权了什么、在哪里、何时),用户可以继续接收、停止或举报。

流程 Q 总览:企业侧、API 检查和用户侧,以及 P 与 Q 的对比

Q00. 流程 Q 总览:企业侧、API 检查和用户侧,以及 P 与 Q 的对比

在 PicPay App 中 opt-in,并写明 PhizChat

Q01. 在 PicPay App 中 opt-in,并写明 PhizChat

列表中的第一条消息,带“新企业”标签

Q02. 列表中的第一条消息,带“新企业”标签

同意来源卡片:继续接收、停止或举报

Q03. 同意来源卡片:继续接收、停止或举报

按类别显示同意状态,含来源和日期

Q04. 按类别显示同意状态,含来源和日期

模式 B:通过 API 登记同意及发送错误(示意性提议)

Q05. 模式 B:通过 API 登记同意及发送错误(示意性提议)

P 与 Q 对比

P · 在 PhizChat 内 opt-inQ · 在企业处 opt-in
由谁收集PhizChat(接受)商户(App、网站、门店)
证明保存在PhizChat商户 CRM(模式 A)· 同时在 PHIZ(模式 B)
首次对话接受之前锁定以消息和同意来源打开
退订两者相同:撤回、屏蔽、举报——由 PhizChat 执行

待决定事项

  • 流程 Q 采用模式 A 还是 B(见 Q1):像 Meta 一样只有政策,还是在 PHIZ 中建立同意登记并在每次发送时校验。完整的同意来源卡片(Q3)和按类别显示的状态(Q4)依赖模式 B。
  • 流程 Q 中接受之前是否投递:本提议会投递第一条消息并显示同意来源卡片;另一种做法是像流程 P 一样,在接受之前锁定对话。
  • 同意类别(账户提醒、优惠、验证码)以及应用向用户展示什么。
  • 技术命名——/v1/merchants/{merchant_id}/consents、consent_update Webhook 和错误码——均为待讨论的占位内容。

05目标与非目标

目标

  • 凭证归属商户账号,不再按频道签发。商户为每个外部系统创建一个集成(例如 Blip 一个、Salesforce 一个)。每个集成拥有独立的 client_id 和密钥,由商户决定它能做什么(发送消息、查询状态、配置 Webhook)以及能以哪些发送方身份沟通:商户本身,以及商户可选的频道。
  • 采用标准 OAuth 2.0(客户端凭证模式),兼容主流库和平台。
  • 向手机号或 PHIZ ID 发送消息,并通过签名 Webhook 接收回复和状态。
  • 与 Meta Cloud API 结构兼容,现有集成只需少量改动即可迁移。
  • 以 OpenAPI 3.1 格式(即 Swagger 标准)发布 API 规范。该文件描述全部路由、字段、响应和 Webhook,是 API 开发团队与集成方之间的约定。合作方可据此生成文档和代码,并在交付前进行测试。

不在本 RFC 范围内

  • 模板、营销活动和用户分群(Marketing API,后续阶段)。
  • 按会话计费与结算。
  • 群组、语音和视频通话。
  • 官方 SDK(后续可基于 OpenAPI 生成)。

06领域模型

商户账号成为一切的根节点。商户可以通过两种方式与 PhizChat 用户沟通:

  • 直接消息(默认)。商户以自身名义发送,使用 商户信息 中的名称和头像,无需频道。
  • 通过频道(可选)。拥有频道的商户可通过频道发送,例如用于区分品牌或部门。

集成隶属于商户,可被授权使用其中一种或两种方式。

flowchart LR
  M["商户账号
merchant_id"] --> I["集成
Blip · Salesforce · 自有系统
各有 client_id + 密钥"] I -- "POST /v1/{sender_id}/messages" --> API["PHIZ Business API"] API -- "方式 1 · 默认
sender_id = merchant_id" --> D["商户直接消息"] API -. "方式 2 · 可选
sender_id = 频道 ID" .-> C["通过商户频道
发送的消息"] D --> U(("PhizChat 用户
手机号或 PHIZ ID")) C --> U API -- "Webhook:回复与状态" --> I

两种方式使用相同的路由和格式,只有 sender_id 不同。方式 2 仅适用于拥有频道的商户。

端到端流程

面向技术团队:消息从企业到用户经过哪些环节,回复和状态又如何返回。

flowchart LR
  PP["PicPay"] --> B1["BSP
如 Blip"] AM["Ambar"] --> B2["BSP
如 Salesforce"] B1 --> API["PHIZ Business API
/gateway/business
Auth · Messages · Webhooks"] B2 --> API API -- "1–3 · 令牌、发送
与发送方校验" --> MA subgraph OP["Open Platform"] MA["Merchant Account"] MA -- "4a · sender_id = merchant_id
默认" --> DM["Direct Message"] MA -. "4b · sender_id = 频道 ID
可选" .-> CH["Channel"] CH --> CM["Channel Message"] end DM -- "5 · 投递" --> APP["Phiz App"] CM -- "5 · 投递" --> APP APP --> U(("用户"))

去程:从企业到用户。虚线为通过频道发送的可选路径。

返回:回复与状态

flowchart LR
  U(("用户")) -- "回复、接收、阅读" --> APP["Phiz App"]
  APP -- "6 · 事件" --> WH["PHIZ Business API
Webhooks"] WH -- "签名 Webhook" --> BSP["负责该发送方的
集成所属 BSP"] BSP --> EMP["企业"]

回程:用户回复和消息状态通过 Webhook 送达 BSP。

  1. 集成。企业(如 PicPay 或 Ambar)通过 BSP 提供客服。商户在开发者中心为该 BSP 创建集成,并提供 client_id 和密钥。
  2. 令牌。BSP 调用 /oauth/token。Auth 在 Merchant Account 中校验集成(密钥、范围和可用发送方),返回 1 小时有效的 JWT。
  3. 发送。BSP 调用 POST /v1/{sender_id}/messages。API 校验令牌、范围、发送方、选择接收策略和 Idempotency-Key,返回 202 并将消息入队。如需发送媒体,BSP 先将文件上传到 POST /v1/{sender_id}/media,再在发送时使用返回的 id 或 PHIZ 链接。
  4. 路由。sender_id = merchant_id 时作为 Direct Message 发送(默认);使用频道 ID 时作为 Channel Message 发送(可选)。
  5. 投递。Phiz App 向用户展示消息,并通过上传时生成的链接从 PHIZ 存储下载媒体。
  6. 返回。用户回复和状态(sent、delivered、read、failed)生成事件,由 Webhooks 服务签名后投递给负责该发送方的集成。
组件职责状态
BSP / 企业系统调用 API,上传媒体,接收 Webhook。外部
PHIZ Business API · Auth签发和撤销 JWT;校验 client_id 和密钥。新增
PHIZ Business API · Messages校验并受理发送(202),处理幂等和限流,入队并按 sender_id 路由。新增
PHIZ Business API · Media接收上传,校验格式和大小,限时存储文件,并生成发送时使用的 id 和 PHIZ 链接。新增
PHIZ Business API · Webhooks消费 App 事件,签名并带重试投递;保留 72 小时投递记录。新增
Open Platform · Merchant Account存储集成、密钥(哈希)、范围、可用发送方和 Webhook 配置;承载开发者中心。变更:凭证从频道迁移到商户
Direct MessageApp 内商户与用户的直接对话。新增(见 Q3)
Channel / Channel Message通过商户频道发送。现有
Phiz App展示消息,通过 PHIZ 链接下载媒体,产生送达、已读和回复事件。变更

与 Meta Cloud API 的对应关系

Meta(WhatsApp Cloud API)PHIZ(本方案)
WhatsApp Business Account(WABA)商户账号 · merchant_id
企业号码 · phone_number_id发送方 · sender_id:商户本身(默认,等于 merchant_id),或可选的频道(等于频道 ID)
Meta 应用 + System User 令牌开发者应用 + OAuth 客户端凭证
wa_idphiz_id
POST /{phone-number-id}/messagesPOST /v1/{sender_id}/messages
POST /{phone-number-id}/media · 媒体可用 id 或第三方 linkPOST /v1/{sender_id}/media · 媒体仅可用 id 或 PHIZ 链接;不接受第三方链接
object: whatsapp_business_accountobject: phiz_merchant_account
X-Hub-Signature-256X-Phiz-Signature-256 + X-Phiz-Timestamp
hub.mode / hub.verify_token / hub.challenge保持一致

07设计原则

  • 与 Cloud API 结构一致。发送请求、响应和 Webhook 均沿用 Meta 的结构,只有在安全性(签名带时间戳、媒体只经由 PHIZ 存储)或清晰度(to_type)有明确收益时才有所不同。
  • HTTP 状态码、统一的错误结构,所有错误响应都带 trace_id。
  • 发送为异步处理,返回 202 Accepted,结果通过 Webhook 推送。
  • 幂等。Idempotency-Key 请求头可避免客户端重试导致重复消息。
  • 最小授权。每个集成都有范围和可用发送方列表。
  • 版本号 /v1。兼容性变更(新增字段)按统一规范上线;不兼容变更需发布 /v2。

08鉴权

采用 OAuth 2.0 客户端凭证模式(RFC 6749 §4.4)。应用用 client_id 和 client_secret 换取 1 小时有效的访问令牌,并在每次调用时通过 Authorization: Bearer 传递。不提供刷新令牌:令牌过期后重新申请即可。

POST/oauth/token签发访问令牌
POST/oauth/revoke撤销令牌(RFC 7009)
GET/v1/me查看令牌对应的集成、商户、范围和发送方
sequenceDiagram
  autonumber
  participant S as 外部系统
  participant A as PHIZ 鉴权服务
  participant API as PHIZ API
  S->>A: POST /oauth/token(client_id, client_secret)
  A-->>S: 200 access_token(1 小时)、范围
  S->>API: POST /v1/{sender_id}/messages · Bearer 令牌
  API-->>S: 202 Accepted

临近过期时重新申请令牌,不使用刷新令牌。

请求与响应

BASH
# Base URL of this API: current PHIZ gateway + /gateway/business prefix (illustrative host)
PHIZ_API_BASE="https://gateway.phiz.chat/gateway/business"

curl -X POST "$PHIZ_API_BASE/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=client_credentials \
  -d client_id=app_2Fk81PzQ \
  -d client_secret="$PHIZ_CLIENT_SECRET" \
  -d "scope=messages:send messages:read"
JSON
{
  "access_token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InBoei0yMDI2LTEwIn0…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "messages:send messages:read",
  "merchant_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57"
}

/oauth/* 接口使用 OAuth 标准错误格式(RFC 6749 §5.2),客户端库可直接识别:

JSON
{
  "error": "invalid_client",
  "error_description": "Client authentication failed."
}

令牌格式

决定:令牌采用 ES256 签名的 JWT,公钥发布在 /.well-known/jwks.json。网关在本地校验令牌,无需每次调用都查询数据库。被撤销的令牌和已轮换的密钥会进入短期拒绝列表(直到令牌过期)。对集成方而言令牌是不透明的,以下内容仅供参考。

JSON
{
  "iss": "https://gateway.phiz.chat/gateway/business",
  "sub": "app_2Fk81PzQ",
  "aud": "phiz-business-api",
  "mid": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
  "scope": "messages:send messages:read",
  "snd": [
    "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
    "1231"
  ],
  "env": "live",
  "iat": 1760527800,
  "exp": 1760531400,
  "jti": "tok_01JA9VQ4N2"
}

范围(scope)

范围允许的操作
messages:send发送消息、上传和管理媒体、标记已读
messages:read查询消息状态
webhooks:manage配置、测试和重放本应用的 Webhook(第 2 阶段)
account:read读取商户账号和发送方信息
templates:manage预留给 Marketing API

密钥与轮换

  • 密钥带环境前缀(phzsk_live_、phzsk_test_),便于泄露扫描工具识别。
  • 密钥仅在创建或轮换时显示一次,服务端只保存哈希(Argon2id)。
  • 每个应用最多可有两个有效密钥。生成新密钥后,旧密钥在设定时间内仍然有效(默认 24 小时,可设 0 至 7 天),切换过程不中断。
  • 页面显示每个密钥的最近使用时间,便于确认旧密钥已停用后再撤销。
  • 可选:按应用配置 IP 白名单。

现状与方案对比

方面现状方案
凭证归属频道商户账号(通过应用)
授权流程user_credentials(自定义)client_credentials(RFC 6749)
额外必填字段description无
有效期约 2 小时 + 约 90 天刷新令牌1 小时,无刷新令牌
范围channel按能力划分 + 可用发送方
轮换重置后立即失效两个有效密钥,平滑过渡
错误HTTP 200 + 正文中的 code真实 HTTP 状态 + 统一错误结构

09发送消息

POST/v1/{sender_id}/messages发送消息或标记已读
POST/v1/{sender_id}/media上传媒体文件,返回 id 和 PHIZ 链接
GET/v1/media/{media_id}媒体信息:链接、类型、大小和有效期
DELETE/v1/media/{media_id}在到期前删除媒体
GET/v1/messages/{message_id}最新已知状态(兜底)
GET/v1/merchants/{merchant_id}/senders集成可使用的发送方(商户与频道)

发送方:商户或频道

路径中的 sender_id 决定消息以谁的名义发出:

  • 商户本身(默认)。使用 merchant_id。用户在 Business Account 会话中看到与企业的直接对话,商户无需拥有频道。
  • 频道(可选)。使用频道 ID,例如 1231。适合按品牌或部门划分频道的商户。消息出现在该频道的 Channel Service 会话中,与 Channel Bot 使用的会话相同。

两者在 API 中的用法完全相同:字段、响应和 Webhook 一致。Webhook 中的 metadata.sender_type 标明使用的是哪一种,metadata.conversation_type 标明会话类型(business_account 或 channel_service)。

接收方:手机号或 PHIZ ID

to 字段填写接收方,to_type 指明其类型:

  • phone(默认):E.164 格式手机号,例如 +5511987654321。PHIZ 会将号码解析为对应的用户账号。
  • phiz_id:用户的 PHIZ ID,例如 839201447。Webhook 中携带的就是这个标识,回复时可直接使用。

如果手机号没有对应的 PHIZ 账号,或用户屏蔽了该发送方,API 仍返回 202,Webhook 会推送 failed 和错误码 2104。这样可以防止通过 API 探测哪些号码注册了 PHIZ。

示例:向手机号发送文本

BASH
# sender_id = merchant_id: direct message from the merchant, no channel needed
curl -X POST "$PHIZ_API_BASE/v1/7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57/messages" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pedido-48213-envio" \
  -d '{
    "messaging_product": "phiz",
    "to": "+5511987654321",
    "to_type": "phone",
    "type": "text",
    "text": { "body": "Olá, Maria! Seu pedido #48213 foi enviado." },
    "biz_opaque_callback_data": "pedido=48213"
  }'
JSON
{
  "messaging_product": "phiz",
  "contacts": [
    {
      "input": "+5511987654321",
      "phiz_id": null
    }
  ],
  "messages": [
    {
      "id": "pmid.01JA9VZ6T8R3",
      "message_status": "accepted"
    }
  ]
}

只有当用户曾与该发送方对话过,contacts[].phiz_id 才会有值。messages[].id 用于关联状态 Webhook。

更多示例

同样的消息,通过频道发送

BASH
# sender_id = 1231: same request, sent through the merchant's channel
curl -X POST "$PHIZ_API_BASE/v1/1231/messages" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+5511987654321", "type": "text", "text": { "body": "Novidades da semana na Loja Exemplo - Ofertas" } }'

向 PHIZ ID 发送文本

JSON
{
  "to": "839201447",
  "to_type": "phiz_id",
  "type": "text",
  "text": {
    "body": "Seu cupom chegou: PHIZ10"
  }
}

上传媒体

BASH
# 1. Upload the file to PHIZ (multipart/form-data). Third-party URLs are not accepted when sending.
curl -X POST "$PHIZ_API_BASE/v1/7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57/media" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -F messaging_product=phiz \
  -F type=application/pdf \
  -F file=@nota-fiscal-48213.pdf
JSON
{
  "id": "pmed.01JB2K7Q9X",
  "link": "https://media.phiz.chat/biz/pmed.01JB2K7Q9X",
  "mime_type": "application/pdf",
  "file_size": 184320,
  "sha256": "9b1f…",
  "expires_at": "2026-11-19T14:00:00Z"
}

通过媒体 id 发送音频

JSON
{
  "to": "+5511987654321",
  "type": "audio",
  "audio": {
    "id": "pmed.01JB2K4M7A"
  }
}

通过 PHIZ 链接向 PHIZ ID 发送视频

JSON
{
  "to": "839201447",
  "to_type": "phiz_id",
  "type": "video",
  "video": {
    "link": "https://media.phiz.chat/biz/pmed.01JB2K5V1C",
    "caption": "Como montar seu produto"
  }
}

带文件名的文档

JSON
{
  "to": "+5511987654321",
  "type": "document",
  "document": {
    "id": "pmed.01JB2K7Q9X",
    "filename": "nota-fiscal-48213.pdf",
    "caption": "Nota fiscal do pedido #48213"
  }
}

快捷回复按钮

JSON
{
  "to": "+5511987654321",
  "type": "interactive",
  "interactive": {
    "type": "button",
    "body": {
      "text": "Confirma a entrega amanhã entre 9h e 12h?"
    },
    "action": {
      "buttons": [
        {
          "type": "reply",
          "reply": {
            "id": "confirmar",
            "title": "Confirmar"
          }
        },
        {
          "type": "reply",
          "reply": {
            "id": "reagendar",
            "title": "Reagendar"
          }
        }
      ]
    }
  }
}

将收到的消息标记为已读

JSON
{
  "messaging_product": "phiz",
  "status": "read",
  "message_id": "pmid.IN_01JA9W2C"
}

支持的消息类型

type内容阶段
text最多 4,096 个字符;可选 preview_urlMVP
image通过 id 或 PHIZ 链接(需先上传);可选说明文字MVP
audio通过 id 或 PHIZ 链接(需先上传);不支持说明文字MVP
video通过 id 或 PHIZ 链接(需先上传);可选说明文字MVP
document通过 id 或 PHIZ 链接(需先上传);可选说明文字和文件名MVP

媒体:上传至 PHIZ 并临时存储

所有媒体都经由 PHIZ 存储,发送分两步:

  1. 上传。BSP 将文件发送到 POST /v1/{sender_id}/media(multipart/form-data)。PHIZ 校验格式和大小,存储文件,返回 id 和 PHIZ link,有效期见 expires_at。
  2. 发送。消息中的媒体对象携带 id(推荐)或该 link,用户 App 从 PHIZ 存储下载文件。
  • 只接受 PHIZ 生成的链接。第 1 阶段 API 拒绝第三方 URL(商户或合作方的 CDN),返回错误 2302。这样 PHIZ 能保证链接安全、文件在投递前持续可读取,App 也不会向外部服务器暴露用户 IP。
  • 临时存储。文件只保存到完成发送和投递所需的时间:建议自上传起 30 天,与 Cloud API 的媒体 id 相同(见 Q8)。有效期内同一个 id 可用于多次发送,例如将同一份 PDF 发给多位客户。到期后 id 和链接失效,新的发送会返回 2303。商户也可通过 DELETE /v1/media/{media_id} 提前删除。
  • 上传时校验。格式和大小在上传时校验,而不是在投递时:不符合下表的文件会被立即拒绝(2304 和 2305)。
  • 权限范围。上传、查询和删除使用 messages:send 范围,媒体只能由同一商户账号下的发送方使用。
决定(v1.3)

第 1 阶段媒体只能上传至 PHIZ 并临时存储,不接受第三方链接。为特定客户开放第三方 URL(例如通过域名白名单)留待后续阶段。已与 Henry Wu 在本节评论中达成一致。

类型支持格式大小上限
imageJPEG、PNG、WebP5 MB
audioAAC、MP3、M4A、OGG(Opus)、AMR16 MB
videoMP4(H.264 + AAC)、3GP16 MB
documentPDF、DOC/DOCX、XLS/XLSX、PPT/PPTX、TXT、CSV100 MB

PHIZ App 应支持的格式和大小,参考 Cloud API 的限制,由本 RFC 定义。格式转换和压缩由文件发送方负责;上传时会拒绝不符合下表的文件。需与负责 App 媒体能力的团队确认(见 Q8)。

发送策略(同意)

参考 WhatsApp 的提议(见第 03 节),opt-in 流程详见第 04 节:

  • 主动发起会话需要 opt-in:用户需针对该商户(merchant_id)和类别(账户提醒、优惠或验证码)授权。用户发消息后 24 小时内回复,无需事先 opt-in。
  • opt-in 可以在 PHIZ 之外收集(商户的 App、网站或门店),也可以在 PHIZ App 内收集。
  • opt-out 由平台执行:用户屏蔽商户或停止优惠后,该类别的发送直接报错失败,不依赖商户处理。
  • 必须保存记录:商户为每次同意保存证明(用户、merchant_id、类别、来源、时间、展示的文案及撤回)。根据 LGPD,数据处理方须证明已获得同意并尊重撤回。
待产品决定

Meta 有 opt-in 政策但不做验证:它信任商户,事后通过举报和质量评级作出反应。我们需要在 A(相同模式:政策 + 商户自行记录)和 B(在 PHIZ 中建立同意登记,并在每次发送前校验)之间做出选择。见 Q1。

幂等

每次发送都应携带 Idempotency-Key。24 小时内使用相同的键会直接返回原始响应,不会重复发送;相同的键配不同的请求体则返回 409。因此在超时或 5xx 后重试是安全的。

10Webhook

每个应用有一个 Webhook 订阅,包含 URL、verify_token 和事件列表。

决定(v1.4)

第 1 阶段:Webhook URL 由 PHIZ 手工配置,依托与商户或合作方的商务关系。通过开发者中心和 API 配置(下列接口)在 第 2 阶段 开放平台开放时提供。已与 Henry Wu 在本节评论中达成一致。

第 1 阶段:手工配置

  • 商户或合作方通过与 PHIZ 的商务对接人申请配置,并提供 Webhook URL 和想接收的事件(messages、message_status)。
  • PHIZ 在服务端登记该 URL,并用下文相同的 GET 握手进行验证(提议)。
  • 事件格式、签名和握手与第 2 阶段相同,只是配置的人不同。

通过 API 配置(第 2 阶段)

以下接口将在第 2 阶段与开发者中心一并开放。

PUT/v1/apps/{app_id}/webhook设置 URL、verify_token 和事件(第 2 阶段)
GET/v1/apps/{app_id}/webhook配置与健康状态(第 2 阶段)
POST/v1/apps/{app_id}/webhook/test推送带签名的测试事件(第 2 阶段)
GET/v1/apps/{app_id}/webhook/deliveries投递记录,72 小时(第 2 阶段)
POST/v1/apps/{app_id}/webhook/deliveries/{id}/replay重放某次投递(第 2 阶段)
BASH
curl -X PUT "$PHIZ_API_BASE/v1/apps/app_2Fk81PzQ/webhook" \
  -H "Authorization: Bearer $PHIZ_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://integracao.lojaexemplo.com.br/phiz/webhook",
    "verify_token": "um-texto-que-so-voce-conhece",
    "fields": ["messages", "message_status"]
  }'

第 2 阶段的配置调用示例。

事件

field触发时机
messages用户发送消息、点击按钮或回应表情
message_status已发送消息变为 sent、delivered、read 或 failed
sender_update发送方(商户或频道)的状态、质量或额度发生变化
account_update商户账号发生变化(认证、限制)

URL 验证

启用前,PHIZ 会向 URL 发起 GET 请求,携带 hub.mode=subscribe、hub.verify_token 和 hub.challenge。服务器校验令牌后返回 200,正文为 hub.challenge 的值。与 Meta 的握手方式相同。

签名

每个 POST 都带有以下请求头。签名是使用应用 webhook secret(独立于 client secret)对 {X-Phiz-Timestamp}.{原始请求体} 计算的 HMAC-SHA256。时间戳偏差超过 5 分钟的事件应拒绝。轮换 webhook secret 期间,请求头会包含以逗号分隔的两个签名。

HTTP
POST /phiz/webhook HTTP/1.1
Host: integracao.lojaexemplo.com.br
Content-Type: application/json
User-Agent: PHIZ-Webhooks/1.0
X-Phiz-Delivery-Id: dlv_01JA9W2D7K
X-Phiz-Timestamp: 1760528527
X-Phiz-Signature-256: sha256=6f1c0e…
JAVASCRIPT
// Express: verification handshake + signed events
import express from "express";
import crypto from "node:crypto";

const app = express();
app.use(express.raw({ type: "application/json" })); // keep the raw body for the HMAC

app.get("/phiz/webhook", (req, res) => {
  const ok = req.query["hub.mode"] === "subscribe" &&
             req.query["hub.verify_token"] === process.env.PHIZ_VERIFY_TOKEN;
  return ok ? res.status(200).send(req.query["hub.challenge"]) : res.sendStatus(403);
});

app.post("/phiz/webhook", (req, res) => {
  const ts = Number(req.get("X-Phiz-Timestamp"));
  if (Math.abs(Date.now() / 1000 - ts) > 300) return res.sendStatus(400);

  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.PHIZ_WEBHOOK_SECRET)
    .update(`${ts}.${req.body}`)
    .digest("hex");
  const received = (req.get("X-Phiz-Signature-256") || "").split(",");
  const valid = received.some(sig => sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)));
  if (!valid) return res.sendStatus(401);

  queue.push({ deliveryId: req.get("X-Phiz-Delivery-Id"), event: JSON.parse(req.body) });
  res.sendStatus(200); // answer fast, process asynchronously
});
PYTHON
# Flask: signature check
import hmac, hashlib, time, os
from flask import request, abort

def verify_phiz_signature() -> None:
    ts = int(request.headers["X-Phiz-Timestamp"])
    if abs(time.time() - ts) > 300:
        abort(400)
    msg = f"{ts}.".encode() + request.get_data()  # raw bytes, before JSON parsing
    expected = "sha256=" + hmac.new(os.environ["PHIZ_WEBHOOK_SECRET"].encode(), msg, hashlib.sha256).hexdigest()
    received = request.headers.get("X-Phiz-Signature-256", "").split(",")
    if not any(hmac.compare_digest(sig, expected) for sig in received):
        abort(401)

收到的消息

JSON
{
  "object": "phiz_merchant_account",
  "entry": [
    {
      "id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
      "changes": [
        {
          "field": "messages",
          "value": {
            "messaging_product": "phiz",
            "metadata": {
              "sender_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
              "sender_type": "merchant",
              "conversation_type": "business_account",
              "display_name": "Loja Exemplo"
            },
            "contacts": [
              {
                "profile": {
                  "name": "Maria Souza"
                },
                "phiz_id": "839201447",
                "phone": "+5511987654321"
              }
            ],
            "messages": [
              {
                "from": "839201447",
                "id": "pmid.IN_01JA9W2C",
                "timestamp": "1760528527",
                "type": "text",
                "text": {
                  "body": "Quero reagendar a entrega"
                },
                "context": {
                  "id": "pmid.01JA9VZ6T8R3"
                }
              }
            ]
          }
        }
      ]
    }
  ]
}

收到的媒体消息

用户发送图片、音频、视频或文档时,事件中包含一个指向现有聊天存储中该文件的临时 url。消息 API 不复制也不保存媒体,请在链接有效期内下载。下面是一条语音消息的 messages[] 条目:

JSON
{
  "from": "839201447",
  "id": "pmid.IN_01JA9W7R",
  "timestamp": "1760528731",
  "type": "audio",
  "audio": {
    "url": "https://media.phiz.chat/in/9xT2…?expires=1760615131&sig=…",
    "mime_type": "audio/ogg; codecs=opus",
    "sha256": "3f9a…",
    "voice": true
  }
}

已发送消息的状态

JSON
{
  "object": "phiz_merchant_account",
  "entry": [
    {
      "id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
      "changes": [
        {
          "field": "message_status",
          "value": {
            "messaging_product": "phiz",
            "metadata": {
              "sender_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
              "sender_type": "merchant",
              "conversation_type": "business_account",
              "display_name": "Loja Exemplo"
            },
            "statuses": [
              {
                "id": "pmid.01JA9VZ6T8R3",
                "status": "delivered",
                "timestamp": "1760528410",
                "recipient_id": "839201447",
                "biz_opaque_callback_data": "pedido=48213",
                "conversation": {
                  "id": "conv_01JA9W",
                  "origin": {
                    "type": "service"
                  },
                  "expires_at": "1760614810"
                }
              }
            ]
          }
        }
      ]
    }
  ]
}
JSON
{
  "object": "phiz_merchant_account",
  "entry": [
    {
      "id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
      "changes": [
        {
          "field": "message_status",
          "value": {
            "messaging_product": "phiz",
            "metadata": {
              "sender_id": "7f3e2a10-5b8c-4d21-9e6f-2a1c0b9d4e57",
              "sender_type": "merchant",
              "conversation_type": "business_account",
              "display_name": "Loja Exemplo"
            },
            "statuses": [
              {
                "id": "pmid.01JA9X0QW1",
                "status": "failed",
                "timestamp": "1760528600",
                "recipient_id": "+5511900000000",
                "errors": [
                  {
                    "code": 2104,
                    "title": "recipient_not_reachable",
                    "message": "The phone number is not linked to a PHIZ account or blocked this sender.",
                    "href": "https://developers.phiz.chat/docs/errors#2104"
                  }
                ]
              }
            ]
          }
        }
      ]
    }
  ]
}
stateDiagram-v2
  direction LR
  [*] --> accepted: API 返回 202
  accepted --> sent: 已到达 PHIZ 服务器
  sent --> delivered: 已到达设备
  delivered --> read: 用户已打开
  accepted --> failed
  sent --> failed
  read --> [*]
  failed --> [*]

状态生命周期。accepted 只出现在 API 响应和 GET /v1/messages/{id} 中,其余状态通过 Webhook 推送。

投递保证

第 1 阶段。目标是让关键节点上线并可用;下列保障机制在后续阶段实现。

  • 可能重复。同一事件可能多次到达,请用 X-Phiz-Delivery-Id 去重。
  • 不保证顺序。read 可能早于 delivered 到达;请按 timestamp 排序,状态不可回退。
  • 10 秒内返回 2xx,再异步处理。其他响应或超时均视为失败。
  • 已发送消息的状态,如果 Webhook 丢失,可用 GET /v1/messages/{message_id} 兜底查询。
第 1 阶段风险

没有完整的重试策略时,投递失败的事件可能不会再次发送,收到的消息也没有兜底查询。第 1 阶段的最低要求由技术评审确定(见 Q9)。

第 2 阶段(保障机制)。留待后续开发实现的保障机制:

  • 至少一次。带重试的投递,采用指数退避(10 秒、30 秒、1 分钟、5 分钟、15 分钟、1 小时,之后每小时一次),最长 24 小时。
  • 手动重放。24 小时之后,事件在 72 小时内可手动重放,并提供投递记录(/webhook/deliveries)。
  • failing 状态。若 URL 连续 24 小时失败,Webhook 状态变为 failing,并通知商户管理员。

11错误、限额与版本

除 /oauth/* 外,所有错误使用统一结构:

JSON
{
  "error": {
    "code": 1003,
    "type": "invalid_parameter",
    "message": "Field 'to' must be E.164 when to_type is 'phone'.",
    "details": [
      {
        "field": "to",
        "issue": "format"
      }
    ],
    "trace_id": "trc_9f2c41ab"
  }
}
codeHTTPtype含义
1001400invalid_requestJSON 格式错误或缺少必填字段
1003400invalid_parameter参数值无效(见 details)
1101401token_expired令牌已过期,请重新申请
1102401invalid_token令牌无效或已撤销
1201403insufficient_scope令牌缺少所需范围
1202403sender_not_allowed集成无权使用该发送方
1301404not_found资源不存在或属于其他商户
1401409idempotency_conflict相同 Idempotency-Key 但请求体不同
1501429rate_limited超出速率限制,请遵循 Retry-After
2104webhookrecipient_not_reachable号码无 PHIZ 账号,或用户屏蔽了发送方
2201422policy_violation选择接收(opt-in)策略不允许此次发送
2202422unsupported_type该发送方未开通此消息类型
2301webhookmedia_unavailableApp 无法从 PHIZ 存储下载媒体(媒体在投递前已过期或被删除)
2302400media_link_not_allowedlink 不是由 PHIZ 生成的;请先上传,再使用返回的 id 或链接
2303422media_expired媒体 id 或链接已过期、已删除或属于其他商户
2304415unsupported_media_type上传的文件格式不受支持
2305413media_too_large文件超过该类型的大小上限
2401403sender_disabled发送方已停用或受限
2501429messaging_limit_reached发送方当日接收方额度已用完
5000500internal_error内部错误,请使用相同 Idempotency-Key 重试

限额(初步建议)

资源限额
/oauth/token每个应用 60 次/分钟
发送消息每个发送方 80 条/秒(可按合同调整)
其他接口每个应用 600 次/分钟
企业主动消息的唯一接收方按发送方分级:24 小时内 1 千 → 1 万 → 10 万 → 不限,依质量评级调整(第 2 阶段)
HTTP
HTTP/1.1 429 Too Many Requests
Retry-After: 2
X-RateLimit-Limit: 80
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1760528529

环境

  • 生产环境:https://{gateway}/gateway/business
  • 沙箱环境:https://{测试网关}/gateway/business,即目前使用的测试环境(Openapi-test),每个商户一个测试发送方,最多登记 5 个测试接收方(类似 Meta 的测试号码)。phzsk_test_ 密钥仅在此可用。

路由前缀

新 API 与现有 API 部署在同一网关上,通过前缀区分:

  • /gateway/openapi/…:现有 API,迁移期间保留。
  • /gateway/business/…:本 API(鉴权、消息和 Webhook)。

前缀分开后,两个上下文各自拥有独立的路由、鉴权、限额和日志,旧接口下线也不会影响新 API。本文中的所有路径(如 /oauth/token 和 /v1/{sender_id}/messages)均相对于 https://{gateway}/gateway/business。示例中该地址写作 $PHIZ_API_BASE。

12开放平台开发者中心

开发配置从 频道 › 开发配置 移出,成为商户菜单中的独立入口 开发者,位于 设置 旁边。它复用现有的商户 ID、团队管理 中的角色以及 操作记录。

新区域的界面示意,数值均为示例。点击图片可查看原图。

概览:商户 ID、基础 URL、集成和入门步骤

界面 1. 概览:商户 ID、基础 URL、集成和入门步骤

新建集成:名称、范围、可用发送方和环境

界面 2. 新建集成:名称、范围、可用发送方和环境

集成详情:凭证、可轮换的密钥和范围

界面 3. 集成详情:凭证、可轮换的密钥和范围

密钥已创建,仅显示一次

界面 4. 密钥已创建,仅显示一次

Webhook:配置、事件和最近投递记录

界面 5. Webhook:配置、事件和最近投递记录(第 2 阶段)

发送方:直接消息和频道,以及使用它们的集成

界面 6. 发送方:直接消息和频道,以及使用它们的集成

带凭证迁移提示的频道页面

界面 7. 带凭证迁移提示的频道页面

页面

概览
/developer
  • 最近 24 小时已发送、已送达和失败的消息数
  • Webhook 健康状态(第 2 阶段)
  • 提醒:旧密钥仍在使用;Webhook 失败(第 2 阶段)
应用
/developer/apps
  • 创建应用:名称、集成类型(自有、Blip、Salesforce、其他)、环境
  • 范围和可用发送方
  • Client ID、可轮换的密钥及最近使用时间
  • IP 白名单(可选)
  • 停用或删除应用
应用 Webhook(第 2 阶段)
/developer/apps/:id/webhook
  • 第 1 阶段由 PHIZ 手工登记 URL(第 10 节)
  • URL、verify token 和事件
  • 可轮换的 webhook secret
  • 验证并发送测试
  • 最近 72 小时投递记录:HTTP 状态、重试次数、延迟、重放
发送方
/developer/senders
  • 商户本身(始终存在),以及已有的频道
  • 每个发送方的 sender_id、状态、额度和质量
  • 为每个发送方指定接收会话的集成
  • 频道可开启 “Phiz Business API” 模式
API 日志
/developer/logs
  • 最近 7 天的请求:方法、路由、状态、延迟、trace_id
  • 按应用和错误码筛选
沙箱
/developer/sandbox
  • 测试发送方
  • 最多 5 个测试接收方
  • phzsk_test_ 凭证

权限

在现有的 角色管理 页面中新增以下权限,超级管理员默认拥有全部权限。

权限允许的操作
developer.view查看集成、发送方、日志和投递记录(不含密钥)
developer.apps.manage创建、编辑和停用集成;设置范围和发送方
developer.secrets.manage生成、轮换和撤销密钥
developer.webhooks.manage配置 Webhook、测试及重放投递(第 2 阶段)

创建、轮换或撤销密钥,修改范围和 Webhook,都会写入 操作记录 并邮件通知管理员。

一个发送方对应多个集成

为避免两个系统同时回复同一用户,每个发送方(商户或频道)有一个负责会话的集成,在 发送方 页面中指定。只有该集成会收到此发送方的 messages 事件。任何获得授权的集成都可以通过它发送消息,并收到自己所发消息的 message_status。集成之间的会话移交协议(类似 Messenger 的 handover)留待后续。

频道凭证迁移

  1. 自动创建。为每个已有 AppID 的频道,在商户下创建 “Legacy – 频道名称” 应用,沿用原 AppID 作为 client_id、沿用已存储的密钥,并仅授权该频道。现有集成不受影响。
  2. 并行期。/gateway/openapi/auth 和 /token/refresh 继续可用,频道页面显示提示,引导至新入口。
  3. 只读。3 个月后,频道页面不再允许重置密钥,统一在新入口管理。
  4. 下线。6 个月后,且实际调用量接近零时,旧接口返回 410 Gone。

13合作伙伴集成

MVP 阶段,商户在开发者中心创建应用,把 client_id 和密钥填入 Blip、Salesforce 或自有系统,Webhook URL 由合作方提供,第 1 阶段由 PHIZ 手工登记(第 10 节)。由于契约沿用 Cloud API,已有 WhatsApp 连接器的合作方可以复用大部分实现。

后续阶段推出技术合作伙伴计划,对应 Meta 的 Embedded Signup:

  • 合作伙伴在 PHIZ 注册平台应用。
  • 商户在合作方产品中点击 “连接 PHIZ”,在授权页面确认范围和发送方(OAuth 2.0 授权码模式 + PKCE)。
  • 合作伙伴获得仅限该商户的令牌,并用一个 Webhook 接收所有客户的事件。外层结构中的 entry[].id 标识商户,因此该结构从一开始就这样设计。

14通往 Marketing API

本 RFC 为 Marketing API 预留空间,但不做实现:

  • 契约中已包含发送用的 type: template 和 templates:manage 范围。
  • 需审核的模板:/v1/merchants/{merchant_id}/message_templates,以及 template_status Webhook 事件。
  • 会话类别(conversation.origin.type:service、utility、marketing),作为报表与计费的基础。
  • 按发送方管理选择接收与退订,面向分群的营销活动,以及 /v1/merchants/{merchant_id}/analytics 指标。
  • 按发送方的发送额度分级和质量评级,已体现在 Sender 资源中。

15交付计划

第 1 阶段 · MVP
鉴权与发送
  • /oauth/token、/oauth/revoke、/v1/me
  • 向手机号和 PHIZ ID 发送 text、image、audio、video 和 document
  • 媒体上传(/v1/{sender_id}/media)并临时存储;不支持第三方链接
  • 带签名的 messages 和 message_status Webhook,URL 由 PHIZ 手工登记
  • 开发者中心:应用和密钥
  • 所有商户的默认发送方(商户本身);频道作为可选发送方
  • 频道凭证自动迁移
第 2 阶段
自助配置 Webhook 与保障机制
  • 通过开发者中心和 API 配置 Webhook(PUT /v1/apps/{app_id}/webhook、测试和健康状态)
  • 指数退避重试,最长 24 小时
  • 投递记录与 72 小时内手动重放
  • failing 状态并通知管理员

具体时间将在与中国团队评审本 RFC 后确定。

16待讨论问题

  1. 选择接收(opt-in)策略。第 09 节已给出策略提议(按商户和类别 opt-in、平台执行 opt-out、商户保存记录)。仍需决定:模式 A(与 Meta 相同,不验证)还是 B(在 PHIZ 中建立同意登记,例如 /v1/merchants/{merchant_id}/consents,发送时校验);有哪些类别;PHIZ App 是否向用户展示其授权内容;以及用户如何屏蔽 Business Account 或停止其优惠(见 Q3)。LGPD 相关要点需与法务确认。适用于 Business Account 和 Channel Service。
  2. 发送方 ID。直接使用 merchant_id 和频道 ID 作为 sender_id,还是新建不透明 ID?沿用现有 ID 便于迁移,但会暴露频道的自增 ID。
  3. Business Account 会话类型。确认路径 公众号 › Service › Business Account 及其定义:面向企业、商户、政府和机构的双向服务会话,允许用户发消息、菜单可配置,主动消息需遵守授权与发送规则。该类型在 App 中已存在还是需要新建?用户如何屏蔽或免打扰某个 Business Account?
  4. 现有会话类型。核对层级图和权限表(v1.2):哪些类型现已存在、哪些现行规则不同、哪些类型不存在、还缺少哪些类型。
  5. 手机号。所有 PHIZ 用户都有已验证的手机号吗?境外号码和换号如何处理?
  6. 每个发送方一个负责集成是否足够,还是第 2 阶段就需要应用间会话移交(机器人 → 人工客服)?
  7. 现有 Business Message API。AI 机器人的 “Phiz Business API” 模式已有发送和回调接口,似乎对应带 Channel Bot 的 Channel Service。本 RFC 是替代、扩展还是封装这套 API?
  8. 媒体。v1.3 已决定:媒体只能上传至 PHIZ 并临时存储,第 1 阶段不接受第三方链接(第 09 节)。仍需确定:PHIZ App 能否播放媒体表中的所有格式,支持多大的文件?30 天的保存期是否合适,还是应更短?投递后媒体是否转入聊天的常规存储,还是到期后从用户的聊天记录中消失?何时、对哪些客户开放第三方 URL?收到的媒体 url 有效期多长?
  9. 第 1 阶段的 Webhook。v1.4 已决定:由 PHIZ 手工登记 URL,重试、手动重放和 failing 状态留待第 2 阶段(第 10 节)。仍需确定:申请配置的方式、由 PHIZ 哪个团队执行以及时效;webhook secret 如何交给合作方;第 1 阶段的最低重试要求(不重试或短时间窗口);合作方如何找回丢失的收到消息(目前只有状态可查询);以及什么条件触发第 2 阶段。

17OpenAPI 参考

下方 OpenAPI 3.1 规范覆盖本 RFC 的全部接口和 Webhook,描述为当前语言。由于 API 尚未上线,“Try it out” 已关闭。可通过 “复制 OpenAPI” 将 YAML 导入 Apifox、Postman 或 SDK 生成器。