PaystationdocsPaystation

O Paystation avisa o seu servidor quando uma transação muda de status: criada, paga, recusada, estornada. Cada aviso é um POST com JSON para a URL que você cadastrou. É por ele que você libera o produto: o evento na página do checkout serve só à interface.

Checkout
status muda
Paystation
POST JSON assinado
Seu webhook

Cadastrar o destino ​

Os eventos vêm em três famílias. Cada destino escolhe as famílias que recebe.

FamíliaEventosQuando
checkouttransaction_<status>Cada mudança de uma compra feita no checkout ou pela API.
subscription_cyclesubscription_cycle_<status>Cada cobrança de renovação de uma assinatura.
subscriptionsubscription_<status>Cada mudança de estado de uma assinatura: ativa, em atraso, cancelada, encerrada.
Seu servidor

Cadastre pela API, com a chave secreta. Até 15 destinos por cliente:

bash
curl -X POST https://paystation.quero.space/v1/webhook_destinations \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://api.sualoja.com.br/webhooks/paystation", "events": ["checkout", "subscription_cycle", "subscription"] }'
  • A URL precisa ser https com uma chave live, sem usuário e senha na URL e apontando para um endereço público.
  • Repetir o mesmo cadastro devolve o destino que já existe, com 200.
  • Enquanto o checkout do cliente ainda é entregue nas URLs antigas, configuradas pelo time do Paystation, um cadastro que mexe na família checkout recebe 409 legacy_configuration_active. A migração do checkout é feita com o suporte; destinos só de subscription_cycle ou subscription podem ser cadastrados direto.
  • Destinos que o time do Paystation configurou aparecem como managed_by: "paystation" e só podem ser alterados pelo suporte.

A referência completa está em API /v1. Renovações e mudanças de estado de assinatura só chegam a destinos que assinam subscription_cycle e subscription.

O que chega ​

Seu webhook
http
POST /webhooks/paystation HTTP/1.1
Content-Type: application/json
X-Signature-SHA256: 3q2+7wF5mQk1oZ0yJ6l0b8f9Xq2c4P1aT7u0sVnYd3E=
json
{
  "eventName": "transaction_paid",
  "webhookData": {
    "transaction": { "id": "6f1c2b1e-7a4d-4c3b-9a55-2f7e0b6d1a11" },
    "paymentStatus": "paid",
    "createdAt": "2026-10-05T14:22:31.482Z",
    "clientToken": "sualoja",
    "offerData": { "id": "9f1c2b1e-7a4d-4c3b-9a55-2f7e0b6d1a11", "title": "Formação em Análise de Dados", "metadata": null },
    "paymentData": {
      "paymentMethod": "card",
      "amount": 119640,
      "paidAt": "2026-10-05T14:22:29.000Z",
      "acquirerName": "pagarme_v5",
      "acquirerId": "ch_8Kx2…",
      "acquirerCardId": "card_3Pz…",
      "paymentGatewayReference": "paystation-6f1c2b1e-…",
      "paymentTypeData": {
        "installments": 12,
        "card": { "brand": "mastercard", "first_digits": "555555", "last_digits": "4444", "holder_name": "MARIA SOUZA" }
      }
    },
    "customerData": {
      "name": "Maria Souza",
      "email": "maria@exemplo.com",
      "document": "52998224725",
      "document_type": "cpf",
      "phone": "11987654321",
      "address": { "street": "Rua das Flores", "number": "100", "city": "São Paulo", "state": "SP", "zip_code": "01000000" },
      "custom_fields": null
    },
    "coupon": { "id": null, "label": null },
    "orderBumps": null,
    "externalMetadata": { "order_id": "PEDIDO-123" },
    "tracking": { "utm_source": "google" }
  }
}
CampoO que é
eventNameO evento. Veja Eventos.
webhookData.transaction.idId da transação no Paystation: o mesmo id de GET /v1/charges/{id}.
webhookData.paymentStatusStatus da transação nesta mensagem.
webhookData.createdAtQuando a mensagem foi montada. Fica igual em todas as tentativas: use-o para ordenar.
webhookData.clientTokenO identificador do cliente no Paystation. Não é o segredo da assinatura.
webhookData.offerData.idO id da oferta no Paystation, ou o offer_id do seu catálogo quando as ofertas do cliente vêm do seu próprio webhook de ofertas.
webhookData.paymentData.amountValor em centavos: 119640 é R$ 1.196,40.
webhookData.paymentData.acquirerCardIdToken do cartão na adquirente, para cobranças futuras do mesmo cartão. Vem null nas renovações.
webhookData.customerDataO comprador, com endereço e campos personalizados.
webhookData.externalMetadataO que a sua integração mandou junto com o pedido: numa cobrança da API, o metadata, com o attempt_id. Pode vir do navegador: não libere um pedido só por ele.
webhookData.trackingParâmetros de rastreamento da venda, como UTMs.

O exemplo mostra uma compra no cartão. Os campos são os mesmos para Pix e boleto; o que muda é paymentTypeData:

paymentMethodpaymentTypeData
cardinstallments, card (brand, first_digits, last_digits, holder_name, validade), strategy (smart num parcelamento inteligente) e, numa recusa, refusal_reason e refusal_message.
pixqr_code (o copia e cola) e expiration_at.
boletourl (o PDF), barcode, expiration_date e instructions.

Validar a assinatura ​

X-Signature-SHA256 é o HMAC-SHA256 do corpo, calculado com o webhook token do cliente e codificado em base64. O cabeçalho só vem quando o cliente tem webhook token cadastrado: peça o token ao time do Paystation e recuse uma mensagem sem o cabeçalho.

Seu webhook
  1. Leia o corpo bruto, exatamente como chegou, antes do parse do JSON.
  2. Calcule o HMAC-SHA256 desse corpo com o webhook token e codifique em base64.
  3. Compare com o cabeçalho numa comparação de tempo constante.
  4. Se não conferir, responda 401 e descarte a mensagem.
Validar X-Signature-SHA256
js
import { createHmac, timingSafeEqual } from 'node:crypto'

// rawBody: o corpo como string, antes do JSON.parse
export function isFromPaystation(rawBody, signature, webhookToken) {
  const expected = createHmac('sha256', webhookToken).update(rawBody).digest('base64')
  const a = Buffer.from(expected)
  const b = Buffer.from(signature ?? '')
  return a.length === b.length && timingSafeEqual(a, b)
}
php
<?php
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE_SHA256'] ?? '';
$expected = base64_encode(hash_hmac('sha256', $rawBody, getenv('PAYSTATION_WEBHOOK_TOKEN'), true));

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}
python
import base64, hashlib, hmac

def is_from_paystation(raw_body: bytes, signature: str, webhook_token: str) -> bool:
    digest = hmac.new(webhook_token.encode(), raw_body, hashlib.sha256).digest()
    expected = base64.b64encode(digest).decode()
    return hmac.compare_digest(expected, signature or "")

Cole uma mensagem real e o seu token para conferir:

Verificar uma assinatura

Tudo é calculado no seu navegador; nada é enviado.

Assinatura esperada—

Informe o webhook token para calcular a assinatura.

Responder e novas tentativas ​

Responda com qualquer 2xx em até 30 segundos. Processe depois, numa fila: uma resposta lenta conta como falha.

Quando a entrega falha, o Paystation tenta de novo com espera crescente, até 8 tentativas em cerca de 10 minutos:

  1. 10s
  2. +5s25s
  3. +10s315s
  4. +20s435s
  5. +40s51min15
  6. +1min2062min35
  7. +2min4075min15
  8. +5min20810min35
Tempo desde a primeira tentativa. Depois da 8ª, a entrega é encerrada sem novas tentativas.
Resposta do seu servidorCheckoutRenovação e assinatura
2xxEntregue.Entregue.
5xx, 408, 429, 401, 403, tempo esgotado, conexão recusadaTenta de novo.Tenta de novo.
Outros 4xx, como 400, 404 ou 422Não tenta de novo.Tenta de novo.
3xx (redirecionamento), num destino seuNão segue nem tenta de novo.Não segue; tenta de novo.
  • Destinos cadastrados por você não seguem redirecionamento: cadastre a URL final. Os configurados pelo time do Paystation seguem.
  • Com mais de um destino na mesma família, a entrega termina quando qualquer um responde 2xx: um destino que falhou não recebe de novo aquela mensagem.

Entregas repetidas e fora de ordem ​

A entrega é pelo menos uma vez: a mesma mensagem pode chegar duas vezes, e dois status da mesma transação podem chegar fora de ordem. O suporte também pode reenviar um evento pelo painel, e o reenvio é uma mensagem nova, com outro createdAt. Por isso, não confie numa chave de mensagem: faça o efeito ser idempotente.

  • Aplique cada efeito uma vez por transação. Libere o produto se a transação ainda não foi liberada, guardando isso pelo webhookData.transaction.id, não pela mensagem.
  • Nunca volte um status. Um transaction_pending que chega depois de um transaction_paid deve ser ignorado. Use webhookData.createdAt para desempatar duas mensagens da mesma transação.
  • Um status pode voltar de verdade. Uma contestação ganha chega como um segundo transaction_paid depois do transaction_chargedback: trate como "voltou a ser pago", sem liberar o produto de novo.

Eventos ​

Checkout ​

eventName é transaction_ mais o status da transação:

EventoQuando
transaction_createdA transação foi registrada.
transaction_pendingAguardando pagamento: um Pix ou boleto em aberto.
transaction_paidPagamento confirmado. Libere o produto.
transaction_overpaidPago com valor acima do esperado.
transaction_unpaidO pagamento não foi feito a tempo.
transaction_refusedA adquirente recusou o pagamento.
transaction_failedA tentativa de pagamento falhou.
transaction_waiting_refundEstorno pedido, em andamento.
transaction_refundedEstorno concluído.
transaction_chargedbackO comprador contestou a compra no banco.

Renovação de assinatura ​

Cada cobrança de renovação chega como subscription_cycle_<status>:

EventoQuando
subscription_cycle_paidA renovação foi paga.
subscription_cycle_refundedA renovação foi estornada. A assinatura não é cancelada.
subscription_cycle_manually_refundedA renovação foi estornada fora do Paystation.
subscription_cycle_chargedbackO comprador contestou a renovação no banco.

Não existe aviso por recusa de cartão na renovação, porque a cobrança é tentada de novo algumas vezes. Uma renovação que não foi paga chega como subscription_past_due, na família subscription.

A mensagem tem o mesmo formato da de checkout, com estas diferenças:

  • webhookData.transaction.id é sempre o da compra original, em todas as renovações.
  • paymentStatus é igual a eventName, com o prefixo subscription_cycle_.
  • paymentData.paymentTypeData, paymentData.paymentGatewayReference, paymentData.acquirerCardId e orderBumps vêm null: eles descrevem a primeira compra, não a renovação.
  • paymentData.paymentMethod pode vir unknown se a cobrança usou um método que o Paystation não mapeia.
  • Um bloco subscriptionCycle identifica a cobrança:
json
"subscriptionCycle": {
  "subscriptionId": 4812,
  "cycleIndex": 3,
  "engineTransactionId": 99217,
  "refundedAt": null
}

subscriptionCycle.subscriptionId é um número interno da recorrência, diferente do sub_… da API: a assinatura da API é sub_ mais webhookData.transaction.id.

Aplique cada renovação uma vez por subscriptionCycle.engineTransactionId e status, pelo mesmo motivo. refundedAt é só a data: ela também vem preenchida numa contestação, quando o dinheiro ainda não voltou. Decida pelo eventName.

subscriptionCycle.cycleIndex conta as renovações: o ciclo 1 é o segundo pagamento, porque o primeiro é o da compra. Numa oferta de assinatura, o último ciclo é o subscription.cycles que a oferta tinha na compra.

Estado da assinatura ​

A família subscription avisa quando o estado da assinatura muda. Só sai aviso numa mudança: o fim do período de teste, por exemplo, não gera nada.

EventoQuando
subscription_activeFicou ativa: ao ser criada, ao voltar de past_due com o pagamento em dia, ou ao ser reativada pelo suporte.
subscription_past_dueA renovação atrasou. No cartão, no dia seguinte ao vencimento sem pagamento; no Pix e no boleto, quando a cobrança expira. A assinatura continua sendo cobrada e pode voltar a subscription_active.
subscription_canceledFoi cancelada: pela API, pelo suporte ou por uma contestação de renovação. Não cobre estorno nem contestação da compra que abriu a assinatura: esses chegam na família checkout, e a assinatura continua ativa.
subscription_finishedTerminou o número de ciclos contratado. Num parcelamento inteligente, quer dizer "todas as parcelas pagas".

canceled e finished não são definitivos: o suporte pode reativar uma assinatura, e isso gera um aviso novo.

O payload é enxuto e não traz dados do comprador:

json
{
  "eventName": "subscription_canceled",
  "webhookData": {
    "createdAt": "2026-12-01T13:00:05.000Z",
    "paymentStatus": "subscription_canceled",
    "sequence": 4,
    "transaction": { "id": "6f1c2b1e-7a4d-4c3b-9a55-2f7e0b6d1a11" },
    "offerData": { "id": "9f1c2b1e-7a4d-4c3b-9a55-2f7e0b6d1a11", "title": "Formação em Análise de Dados", "metadata": null },
    "externalMetadata": null,
    "tracking": null,
    "subscription": {
      "id": "sub_6f1c2b1e-7a4d-4c3b-9a55-2f7e0b6d1a11",
      "object": "subscription",
      "status": "canceled",
      "charge": "6f1c2b1e-7a4d-4c3b-9a55-2f7e0b6d1a11",
      "payment_method": "card",
      "amount": null,
      "interval_days": 30,
      "current_period_end": null,
      "canceled_at": "2026-12-01T13:00:03.000Z"
    }
  }
}

webhookData.subscription é o mesmo objeto que GET /v1/subscriptions/{id} responde. transaction.id é a compra que abriu a assinatura, o mesmo do aviso de checkout.

  • Ordene pelo sequence. Ele cresce a cada mudança daquela assinatura, mas pode pular números. Ignore um sequence menor ou igual ao último que você aplicou: um reenvio pode chegar depois de um aviso mais novo.
  • Trate o aviso como estado, não como comando. "Ativa" numa assinatura que já está ativa não pede nada.
  • Um aviso que não chega é reenviado por até um dia, depois das 8 tentativas, a cada 15 minutos. Depois disso, consulte GET /v1/subscriptions/{id}, que sempre responde o estado atual.
  • As famílias não têm ordem entre si: subscription_active pode chegar antes do transaction_paid da compra que abriu a assinatura.

Checklist de produção ​

  • Valide a assinatura antes de qualquer efeito, e recuse a mensagem que chega sem ela.
  • Responda 2xx rápido e processe numa fila.
  • Torne cada efeito idempotente por transação, como em Entregas repetidas.
  • Registre transaction.id, eventName e createdAt de cada entrega, para falar com o suporte.
  • Teste antes num destino de homologação, com uma chave test.