Webhooks
A Vixpi faz um POST na sua URL quando algo muda na conta: fatura paga, serviço ativado, ticket respondido. Você não precisa consultar a API de tempos em tempos.
Cadastre um endpoint
Em Desenvolvedor > Webhooks informe a URL e
escolha os eventos. A URL precisa ser https://, na porta 443 ou 8443. A Vixpi mostra
o segredo (whsec_...) uma única vez; é com ele que você confere a assinatura. O botão Enviar teste manda um evento webhook.test só para aquele endpoint.
Eventos
Assine eventos exatos (invoice.paid) ou uma família inteira
(invoice.*).
Serviços
| Evento | Quando acontece | Campos em data |
|---|---|---|
service.created | Um serviço foi criado, ainda aguardando ativação. | serviceId,productId,planId |
service.activated | O serviço foi provisionado e está ativo. | serviceId,productId,nextDueDate |
service.suspended | O serviço foi suspenso. | serviceId,suspendedAt |
service.unsuspended | O serviço suspenso voltou a ficar ativo. | serviceId,nextDueDate |
service.terminated | O serviço foi encerrado em definitivo. | serviceId,terminatedAt |
service.cancellation.requested | Um cancelamento no fim do ciclo foi agendado. | cancellationId,serviceId,effectiveDate,requestedAt |
service.upgrade.applied | A troca de plano foi aplicada ao serviço. | serviceId,serviceUpgradeId,oldProductId,newProductId,newPlanId |
service.upgrade.cancelled | Um pedido de troca de plano foi cancelado. | serviceUpgradeId,serviceId |
Faturas
| Evento | Quando acontece | Campos em data |
|---|---|---|
invoice.created | Uma fatura foi emitida. | invoiceId,orderId,number,totalCents,currency,dueDate |
invoice.paid | Uma fatura foi paga. | invoiceId,orderId,totalCents,currency,paidAt |
invoice.cancelled | Uma fatura foi cancelada. | invoiceId,orderId,totalCents,currency,cancelledAt |
invoice.refunded | Parte ou o total de uma fatura paga foi devolvido. | invoiceId,refundId,method,currency,refundedCents,totalRefundedCents,full,refundedAt |
Tickets
| Evento | Quando acontece | Campos em data |
|---|---|---|
ticket.created | Você abriu um ticket de suporte. | ticketId,subject |
ticket.replied | A equipe respondeu um ticket seu. | ticketId,replyId,subject,excerpt |
ticket.closed | Um ticket seu foi fechado. | ticketId,subject,closedAt |
Corpo e cabeçalhos
O corpo é sempre JSON com id (o mesmo de X-Vixpi-Delivery), type, createdAt e data. Valores em centavos chegam como
texto, como "4990". Cada evento leva só os campos listados acima.
{
"id": "0198c7b2-5d1e-7a3c-9e44-2f6a1c0b8d37",
"type": "invoice.paid",
"createdAt": "2026-10-25T14:32:08.114Z",
"data": {
"invoiceId": "0198c7b1-0a52-7c10-8b7e-4d9f3a21c6e5",
"orderId": "0198c7b0-e3f4-7d28-a1b6-90c5d8e7f412",
"totalCents": "4990",
"currency": "BRL",
"paidAt": "2026-10-25T14:32:07.902Z"
}
}POST /seu-endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Vixpi-Webhooks/1
X-Vixpi-Event: invoice.paid
X-Vixpi-Delivery: 0198c7b2-5d1e-7a3c-9e44-2f6a1c0b8d37
X-Vixpi-Signature: t=<unix>,v1=<hmac-sha256 em hexadecimal>Verifique a assinatura
O valor v1 de X-Vixpi-Signature é o HMAC-SHA256, em hexadecimal, da
string ${t}.${corpo}, com o segredo whsec_... inteiro como chave. Três
cuidados:
- Use o corpo cru, como chegou. Reserializar o JSON muda os bytes e a assinatura deixa de bater.
- Compare em tempo constante (
timingSafeEqual,compare_digest). - Recuse se
testiver a mais de 5 minutos do seu relógio; isso barra reenvio de uma captura antiga.
import { createHmac, timingSafeEqual } from 'node:crypto'
const TOLERANCE_SECONDS = 300
// rawBody: o corpo EXATO recebido (string), antes de qualquer JSON.parse
export function verifyVixpiSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')))
const t = Number(parts.t)
if (!parts.v1 || !Number.isInteger(t)) return false
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(parts.v1)
return a.length === b.length && timingSafeEqual(a, b)
}
// Express: use express.raw({ type: 'application/json' }) nesta rota
app.post('/webhooks/vixpi', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8')
if (!verifyVixpiSignature(raw, req.get('X-Vixpi-Signature') ?? '', process.env.VIXPI_WEBHOOK_SECRET)) {
return res.sendStatus(400)
}
const event = JSON.parse(raw)
// trate event.type; se já viu event.id, ignore
res.sendStatus(200)
})import hashlib
import hmac
import os
import time
TOLERANCE_SECONDS = 300
def verify_vixpi_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(part.split("=", 1) for part in header.split(","))
try:
t = int(parts["t"])
received = parts["v1"]
except (KeyError, ValueError):
return False
if abs(time.time() - t) > TOLERANCE_SECONDS:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, received)
# Flask
@app.post("/webhooks/vixpi")
def vixpi_webhook():
raw = request.get_data() # bytes originais, sem request.json
header = request.headers.get("X-Vixpi-Signature", "")
if not verify_vixpi_signature(raw, header, os.environ["VIXPI_WEBHOOK_SECRET"]):
return "", 400
event = request.get_json()
return "", 200Responda e trate repetições
Responda com qualquer 2xx em até 10 segundos, e só então faça o trabalho pesado
(guarde o evento numa fila). Redirecionamentos não são seguidos. Qualquer outra resposta, ou
falta de resposta, conta como falha.
Pode chegar o mesmo evento mais de uma vez. Guarde o id do que já processou e ignore
repetidos.
Retentativas
Em caso de falha a Vixpi tenta de novo, no máximo 7 vezes no total, com intervalos crescentes:
| Tentativa | Quando |
|---|---|
| 1ª | Na hora |
| 2ª | 1 minuto depois da falha |
| 3ª | 5 minutos depois |
| 4ª | 30 minutos depois |
| 5ª | 2 horas depois |
| 6ª | 6 horas depois |
| 7ª | 24 horas depois |
Se a última falhar, a entrega é dada como esgotada e fica no histórico do endpoint, no painel. Depois de 20 entregas esgotadas seguidas, sem nenhum sucesso no meio, o endpoint é desativado e você pode reativá-lo no painel.