Plasma

Webhooks

Receba notificações em tempo real sobre eventos da sua loja.

Visão Geral

Webhooks permitem que sua aplicação receba notificações em tempo real quando eventos acontecem na sua loja Plasma. Quando um evento ocorre, enviamos uma requisição HTTP POST para a URL que você configurou.

Endpoints

A API de Webhooks está dividida em dois grupos:

Gerenciar Endpoints (CRUD)

MétodoEndpointDescrição
POST/api/sdk/webhooks/endpointsRegistrar endpoint
GET/api/sdk/webhooks/endpointsListar endpoints
GET/api/sdk/webhooks/endpoints/:idDetalhes do endpoint
PUT/api/sdk/webhooks/endpoints/:idAtualizar endpoint
DELETE/api/sdk/webhooks/endpoints/:idRemover endpoint
POST/api/sdk/webhooks/endpoints/:id/testTestar endpoint

Monitorar Entregas (Deliveries)

MétodoEndpointDescrição
GET/api/sdk/webhooksListar entregas
GET/api/sdk/webhooks/statsEstatísticas
GET/api/sdk/webhooks/:idDetalhes de uma entrega
POST/api/sdk/webhooks/:id/retryRetry manual

Registrar Endpoint

POST /api/sdk/webhooks/endpoints

Body

CampoTipoObrigatórioDescrição
urlstring✅URL do webhook (HTTPS recomendado)
eventsstring[]✅Lista de eventos para ouvir

Exemplo

curl -X POST "https://api.plasmacheckout.com/api/sdk/webhooks/endpoints" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://meusite.com/api/webhooks/plasma",
    "events": ["order.paid", "order.cancelled", "order.refunded", "product.updated"]
  }'
const response = await fetch(
  'https://api.plasmacheckout.com/api/sdk/webhooks/endpoints',
  {
    method: 'POST',
    headers: {
      'X-PLASMA-Public-Key': 'pk_live_xxxxxxxxxxxxxxxxxxxx',
      'X-PLASMA-Secret-Key': 'sk_live_xxxxxxxxxxxxxxxxxxxx',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url: 'https://meusite.com/api/webhooks/plasma',
      events: ['order.paid', 'order.cancelled', 'order.refunded', 'product.updated']
    })
  }
);

const { data } = await response.json();
console.log(data.secret);  // Salve! Não será mostrado novamente

Resposta (201 Created)

{
  "data": {
    "id": "cm5webhook123abc",
    "url": "https://meusite.com/api/webhooks/plasma",
    "events": ["order.paid", "order.cancelled", "order.refunded", "product.updated"],
    "secret": "a1b2c3d4e5f6...64chars...hex",
    "isActive": true,
    "createdAt": "2025-01-15T10:00:00Z"
  },
  "message": "Webhook endpoint registered. Save the secret — it will not be shown again."
}

Salve o secret imediatamente! Ele é retornado apenas na criação e é necessário para verificar a assinatura dos webhooks recebidos. Máximo de 10 endpoints por loja.


Listar Endpoints

GET /api/sdk/webhooks/endpoints
curl -X GET "https://api.plasmacheckout.com/api/sdk/webhooks/endpoints" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx"

Resposta

{
  "data": [
    {
      "id": "cm5webhook123abc",
      "url": "https://meusite.com/api/webhooks/plasma",
      "events": ["order.paid", "order.cancelled", "order.refunded", "product.updated"],
      "isActive": true,
      "createdAt": "2025-01-15T10:00:00Z",
      "updatedAt": "2025-01-15T10:00:00Z"
    }
  ]
}

Detalhes do Endpoint

GET /api/sdk/webhooks/endpoints/:id
curl -X GET "https://api.plasmacheckout.com/api/sdk/webhooks/endpoints/cm5webhook123abc" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx"

Atualizar Endpoint

PUT /api/sdk/webhooks/endpoints/:id

Body (todos opcionais)

CampoTipoDescrição
urlstringNova URL
eventsstring[]Nova lista de eventos
isActivebooleanAtivar/desativar endpoint

Exemplo

curl -X PUT "https://api.plasmacheckout.com/api/sdk/webhooks/endpoints/cm5webhook123abc" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["order.paid", "order.cancelled", "order.refunded", "checkout.completed"],
    "isActive": true
  }'

Resposta

{
  "data": {
    "id": "cm5webhook123abc",
    "url": "https://meusite.com/api/webhooks/plasma",
    "events": ["order.paid", "order.cancelled", "order.refunded", "checkout.completed"],
    "isActive": true,
    "createdAt": "2025-01-15T10:00:00Z",
    "updatedAt": "2025-01-15T12:00:00Z"
  },
  "message": "Webhook endpoint updated"
}

Remover Endpoint

DELETE /api/sdk/webhooks/endpoints/:id
curl -X DELETE "https://api.plasmacheckout.com/api/sdk/webhooks/endpoints/cm5webhook123abc" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx"

Resposta

{
  "data": { "id": "cm5webhook123abc", "deleted": true },
  "message": "Webhook endpoint deleted"
}

Testar Endpoint

POST /api/sdk/webhooks/endpoints/:id/test

Envia um payload de teste para o endpoint e retorna o resultado.

curl -X POST "https://api.plasmacheckout.com/api/sdk/webhooks/endpoints/cm5webhook123abc/test" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx"

Resposta

{
  "success": true,
  "endpointId": "cm5webhook123abc",
  "url": "https://meusite.com/api/webhooks/plasma",
  "httpStatus": 200,
  "latencyMs": 145,
  "responseBody": "{\"received\":true}",
  "error": null
}

O teste envia um payload com event: "test". O timeout é de 10 segundos.


Eventos Disponíveis

São estes oito, e só estes: o registro recusa outro nome com 400.

Pedidos

EventoQuando é disparado
order.createdO pedido nasceu no checkout, ainda sem pagamento
checkout.completedO comprador finalizou o checkout, ainda sem pagamento
order.paidO gateway confirmou o pagamento. É o único que libera o pedido
order.cancelledO gateway cancelou o pagamento
order.refundedO pagamento foi devolvido (inclui reembolso parcial e contestação)

Cartão recusado e Pix que ninguém pagou não geram aviso próprio: o pedido nunca recebe order.paid.

Produtos

EventoQuando é disparado
product.createdProduto criado, pelo painel ou pela API
product.updatedProduto alterado, pelo painel ou pela API (preço, estoque, variantes)
product.deletedProduto removido

Assine os eventos de produto para revalidar o cache da sua vitrine na hora certa.


Payload do Webhook

Toda entrega chega com estes cabeçalhos:

X-PLASMA-Event:       order.paid
X-PLASMA-Event-Id:    evt_1788537907378_9f2c1a
X-PLASMA-Signature:   sha256=0cc20a679de32bbe5be4b1fd6df656bba06ecc98b9550d5bac6168a06da8419e
X-PLASMA-Timestamp:   2026-10-04T12:00:01.000Z
X-PLASMA-Delivery-Id: cmtn5akvi0001hm4lrqnzkd0x

E o corpo traz id, event, timestamp, storeId, storeName e data. Nos avisos de pedido, data é o pedido inteiro, no formato da API de Pedidos:

Exemplo: order.paid
{
  "id": "evt_1788537907378_9f2c1a",
  "event": "order.paid",
  "timestamp": "2026-10-04T12:00:01.000Z",
  "storeId": "cmq490bw60008g075bkvdi9rl",
  "storeName": "Minha Loja",
  "data": {
    "id": "cmu9pedido0001",
    "orderNumber": "P-7K2M9Q",
    "checkoutToken": "c08b6109-4c32-46ac-9a8a-bb72b7ec71de",
    "paymentStatus": "paid",
    "customer": { "email": "cliente@email.com", "name": "João Silva", "phone": null },
    "items": [
      { "productId": "cmu1splash0001", "variantId": "cmu1var10ml", "title": "Body Splash Lavanda",
        "variantTitle": "10ml", "sku": "BSL-10", "quantity": 2, "priceCents": 4990,
        "totalCents": 9980, "image": null, "kind": "item" }
    ],
    "totals": { "subtotalCents": 9980, "shippingCents": 1500, "discountCents": 0, "taxCents": 0, "totalCents": 11480 },
    "payment": { "method": "pix", "paidAt": "2026-10-04T12:00:00.000Z", "refundedAt": null },
    "metadata": { "pedidoDaLoja": "Y-123" }
  }
}

Como achar o seu pedido: data.checkoutToken é o token que o create-link devolveu e que você guardou com o carrinho.

Nos avisos product.created e product.updated, data é o produto inteiro, com variants, no formato da API de Produtos. Em product.deleted, data é { id, title, deleted, deletedAt }.


Verificando Assinatura

Todo aviso traz uma assinatura HMAC-SHA256 no cabeçalho X-PLASMA-Signature, no formato sha256=<hex>, calculada sobre o corpo cru: o texto exato que chegou.

Assine o texto cru, antes de qualquer parse. Converter o corpo em objeto e serializar de novo produz outro texto, e a assinatura deixa de bater: toda entrega é rejeitada. Em Next.js, leia com await request.text(); em Express, não use express.json() nessa rota.

Verificação em Node.js

import { verifyWebhookSignature } from '@plasmacheckout/sdk';

const valido = verifyWebhookSignature(corpoCru, assinaturaDoHeader, process.env.PLASMA_WEBHOOK_SECRET!);
lib/verify-webhook.js
import crypto from 'crypto';

export function verifyWebhookSignature(corpoCru, assinatura, segredo) {
  const esperada = 'sha256=' + crypto.createHmac('sha256', segredo).update(corpoCru).digest('hex');

  // timingSafeEqual lança se os tamanhos forem diferentes: compare antes.
  if (!assinatura || assinatura.length !== esperada.length) return false;
  return crypto.timingSafeEqual(Buffer.from(assinatura), Buffer.from(esperada));
}

Exemplo de Endpoint (Next.js)

app/api/webhooks/plasma/route.ts
import { NextResponse } from 'next/server';
import { verifyWebhookSignature } from '@plasmacheckout/sdk';

export async function POST(request: Request) {
  // 1. corpo cru, antes de qualquer parse
  const raw = await request.text();
  const assinatura = request.headers.get('x-plasma-signature') ?? '';

  if (!verifyWebhookSignature(raw, assinatura, process.env.PLASMA_WEBHOOK_SECRET!)) {
    return NextResponse.json({ error: 'Assinatura inválida' }, { status: 401 });
  }

  const evento = JSON.parse(raw);

  // 2. idempotência: a mesma entrega pode chegar de novo
  const chave = request.headers.get('x-plasma-event-id') ?? evento.id ?? null;
  if (chave && (await jaProcessado(chave))) return NextResponse.json({ received: true });

  // 3. data é o pedido inteiro; checkoutToken liga ao seu carrinho
  switch (evento.event) {
    case 'order.paid':
      await liberarPedido(evento.data.checkoutToken, evento.data);
      break;
    case 'order.cancelled':
    case 'order.refunded':
      await reverterPedido(evento.data.checkoutToken, evento.data);
      break;
    case 'product.updated':
      await revalidarProduto(evento.data.id);
      break;
  }

  if (chave) await marcarProcessado(chave);
  return NextResponse.json({ received: true });
}

Retry Automático

Se a sua rota responder 5xx ou 429, ou não responder em 10 segundos, o Plasma tenta de novo. São no máximo 5 tentativas:

TentativaQuando
1ªImediato
2ª1 minuto depois
3ª5 minutos depois
4ª15 minutos depois
5ª1 hora depois

Resposta 4xx (exceto 429) não é retentada: vira falha definitiva na hora. Responda 200 assim que validar e persistir. Se a assinatura não bater, responder 401 é o certo, mas confira antes se você está assinando o corpo cru. Depois da última tentativa, use o retry manual pela API.


Listar Entregas (Deliveries)

GET /api/sdk/webhooks

Parâmetros de Query

ParâmetroTipoDescrição
limitnumberMáximo de itens (1-100, padrão: 50)
statusstringdelivered, failed, pending, retrying
eventstringFiltrar por tipo de evento
include_statsstringtrue para incluir estatísticas na mesma resposta

Exemplo

curl -X GET "https://api.plasmacheckout.com/api/sdk/webhooks?status=failed&include_stats=true" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx"

Resposta

{
  "data": {
    "deliveries": [
      {
        "id": "cm5delivery123",
        "event": "order.paid",
        "status": "failed",
        "attempts": 3,
        "webhookUrl": "https://meusite.com/api/webhooks/plasma",
        "errorMessage": "Connection timeout",
        "createdAt": "2025-01-15T10:30:00Z"
      }
    ],
    "stats": {
      "total": 1500,
      "delivered": 1480,
      "failed": 15,
      "pending": 5,
      "successRate": 98.67
    }
  },
  "meta": {
    "count": 1,
    "limit": 50,
    "filters": {
      "status": "failed",
      "event": null
    }
  }
}

Detalhes de uma Entrega

GET /api/sdk/webhooks/:id
curl -X GET "https://api.plasmacheckout.com/api/sdk/webhooks/cm5delivery123" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx"

Resposta

{
  "data": {
    "id": "cm5delivery123",
    "event": "order.paid",
    "eventId": "evt_abc123",
    "webhookUrl": "https://meusite.com/api/webhooks/plasma",
    "status": "failed",
    "httpStatus": null,
    "responseBody": null,
    "errorMessage": "Connection timeout",
    "attempts": 6,
    "maxAttempts": 6,
    "nextRetryAt": null,
    "createdAt": "2025-01-15T10:30:00Z",
    "deliveredAt": null,
    "payload": {
      "event": "order.paid",
      "data": { "..." : "..." }
    }
  }
}

Estatísticas de Webhooks

GET /api/sdk/webhooks/stats
curl -X GET "https://api.plasmacheckout.com/api/sdk/webhooks/stats" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx"

Resposta

{
  "data": {
    "total": 1500,
    "delivered": 1480,
    "failed": 15,
    "pending": 5,
    "successRate": 98.67
  }
}

Retry Manual

POST /api/sdk/webhooks/:id/retry

Retenta uma entrega que falhou.

curl -X POST "https://api.plasmacheckout.com/api/sdk/webhooks/cm5delivery123/retry" \
  -H "X-PLASMA-Public-Key: pk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-PLASMA-Secret-Key: sk_live_xxxxxxxxxxxxxxxxxxxx"

Resposta

{
  "success": true,
  "deliveryId": "cm5delivery123",
  "event": "order.paid",
  "httpStatus": 200,
  "error": null,
  "retryScheduled": false
}

Não é possível retentá-la se o status já for delivered.


Boas Práticas

Responda rapidamente

Retorne 200 o mais rápido possível. Processe pesado em background.

Use uma fila

Coloque eventos em uma fila (Redis, SQS) e processe assincronamente.

Implemente idempotência

Use o cabeçalho X-PLASMA-Event-Id (ou o id do corpo) para não processar a mesma entrega duas vezes, guardando os já processados no seu banco. Trate a chave como opcional: o disparo de teste do painel não traz id.

Verifique sempre a assinatura

Nunca processe webhooks sem verificar a assinatura X-PLASMA-Signature.

Use HTTPS

Sua URL de webhook deve usar HTTPS obrigatoriamente em produção.


Configurando pelo Painel

O painel também cria endpoints, em Loja → Aplicativos → Custom, com seleção de eventos e botão de teste.

O painel nunca mostra o segredo HMAC, nem na criação nem depois. Sem ele não há como validar a assinatura. Registre o endpoint pela API (POST /api/sdk/webhooks/endpoints), que devolve o secret uma única vez.