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étodo | Endpoint | Descrição |
|---|---|---|
POST | /api/sdk/webhooks/endpoints | Registrar endpoint |
GET | /api/sdk/webhooks/endpoints | Listar endpoints |
GET | /api/sdk/webhooks/endpoints/:id | Detalhes do endpoint |
PUT | /api/sdk/webhooks/endpoints/:id | Atualizar endpoint |
DELETE | /api/sdk/webhooks/endpoints/:id | Remover endpoint |
POST | /api/sdk/webhooks/endpoints/:id/test | Testar endpoint |
Monitorar Entregas (Deliveries)
| Método | Endpoint | Descrição |
|---|---|---|
GET | /api/sdk/webhooks | Listar entregas |
GET | /api/sdk/webhooks/stats | Estatísticas |
GET | /api/sdk/webhooks/:id | Detalhes de uma entrega |
POST | /api/sdk/webhooks/:id/retry | Retry manual |
Registrar Endpoint
POST /api/sdk/webhooks/endpointsBody
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
url | string | ✅ | URL do webhook (HTTPS recomendado) |
events | string[] | ✅ | 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 novamenteResposta (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/endpointscurl -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/:idcurl -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/:idBody (todos opcionais)
| Campo | Tipo | Descrição |
|---|---|---|
url | string | Nova URL |
events | string[] | Nova lista de eventos |
isActive | boolean | Ativar/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/:idcurl -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/testEnvia 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
| Evento | Quando é disparado |
|---|---|
order.created | O pedido nasceu no checkout, ainda sem pagamento |
checkout.completed | O comprador finalizou o checkout, ainda sem pagamento |
order.paid | O gateway confirmou o pagamento. É o único que libera o pedido |
order.cancelled | O gateway cancelou o pagamento |
order.refunded | O 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
| Evento | Quando é disparado |
|---|---|
product.created | Produto criado, pelo painel ou pela API |
product.updated | Produto alterado, pelo painel ou pela API (preço, estoque, variantes) |
product.deleted | Produto 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: cmtn5akvi0001hm4lrqnzkd0xE o corpo traz id, event, timestamp, storeId, storeName e data. Nos avisos de pedido, data é o pedido inteiro, no formato da API de Pedidos:
{
"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!);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)
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:
| Tentativa | Quando |
|---|---|
| 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/webhooksParâmetros de Query
| Parâmetro | Tipo | Descrição |
|---|---|---|
limit | number | Máximo de itens (1-100, padrão: 50) |
status | string | delivered, failed, pending, retrying |
event | string | Filtrar por tipo de evento |
include_stats | string | true 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/:idcurl -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/statscurl -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/retryRetenta 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.