API da Vixpi Portal de desenvolvedores, versão v1

Painel do desenvolvedor

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

EventoQuando aconteceCampos em data
service.createdUm serviço foi criado, ainda aguardando ativação.serviceId,productId,planId
service.activatedO serviço foi provisionado e está ativo.serviceId,productId,nextDueDate
service.suspendedO serviço foi suspenso.serviceId,suspendedAt
service.unsuspendedO serviço suspenso voltou a ficar ativo.serviceId,nextDueDate
service.terminatedO serviço foi encerrado em definitivo.serviceId,terminatedAt
service.cancellation.requestedUm cancelamento no fim do ciclo foi agendado.cancellationId,serviceId,effectiveDate,requestedAt
service.upgrade.appliedA troca de plano foi aplicada ao serviço.serviceId,serviceUpgradeId,oldProductId,newProductId,newPlanId
service.upgrade.cancelledUm pedido de troca de plano foi cancelado.serviceUpgradeId,serviceId

Faturas

EventoQuando aconteceCampos em data
invoice.createdUma fatura foi emitida.invoiceId,orderId,number,totalCents,currency,dueDate
invoice.paidUma fatura foi paga.invoiceId,orderId,totalCents,currency,paidAt
invoice.cancelledUma fatura foi cancelada.invoiceId,orderId,totalCents,currency,cancelledAt
invoice.refundedParte ou o total de uma fatura paga foi devolvido.invoiceId,refundId,method,currency,refundedCents,totalRefundedCents,full,refundedAt

Tickets

EventoQuando aconteceCampos em data
ticket.createdVocê abriu um ticket de suporte.ticketId,subject
ticket.repliedA equipe respondeu um ticket seu.ticketId,replyId,subject,excerpt
ticket.closedUm 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.

corpo
{
  "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"
  }
}
cabeçalhos
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 t estiver a mais de 5 minutos do seu relógio; isso barra reenvio de uma captura antiga.
Node.js
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)
})
Python
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 "", 200

Responda 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:

TentativaQuando
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.