# Receba eventos da Kernos

Cadastre um endereço HTTPS e a Kernos manda um POST assinado a cada evento: `package.close`, `pix.paid`, `receipt.close`, `return.close`, `sales_order.finish_packing`, `shipment.dispatch`.

## Configure o endereço

O endereço precisa ser HTTPS, público e sem usuário e senha. Um por empresa.

#### curl

**Cadastrar o webhook**

```sh
curl -X PUT "https://stag.kernos.com.br/v1/webhook-endpoint" \
  -H "Authorization: Bearer ksk_test_…" \
  -H "Kernos-Version: 2026-10-03" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://seu-sistema.com.br/kernos/webhook",
    "events": ["sales_order.finish_packing", "shipment.dispatch"]
  }'
```

#### Node

**Cadastrar o webhook**

```js
import { Kernos } from '@kernosbr/kernos'

const kernos = new Kernos(process.env.KERNOS_API_KEY)
const result = await kernos.integrationWebhook.save({
  url: 'https://seu-sistema.com.br/kernos/webhook',
  events: ['sales_order.finish_packing', 'shipment.dispatch']
})
```

#### Python

**Cadastrar o webhook**

```python
import os
from kernos import Kernos

kernos = Kernos(os.environ["KERNOS_API_KEY"])
result = kernos.integration_webhook.save({
  "url": "https://seu-sistema.com.br/kernos/webhook",
  "events": ["sales_order.finish_packing", "shipment.dispatch"]
})
```

#### PHP

**Cadastrar o webhook**

```php
<?php
$kernos = new Kernos\Kernos(getenv('KERNOS_API_KEY'));
$result = $kernos->integrationWebhook->save([
  'url' => 'https://seu-sistema.com.br/kernos/webhook',
  'events' => ['sales_order.finish_packing', 'shipment.dispatch']
]);
```

#### Java

**Cadastrar o webhook**

```java
Kernos kernos = new Kernos(System.getenv("KERNOS_API_KEY"));
JsonNode result = kernos.integrationWebhook().save(Map.of(
    "url", "https://seu-sistema.com.br/kernos/webhook",
    "events", List.of("sales_order.finish_packing", "shipment.dispatch")
));
```

#### C#

**Cadastrar o webhook**

```csharp
var kernos = new KernosClient(Environment.GetEnvironmentVariable("KERNOS_API_KEY"));
var result = await kernos.IntegrationWebhook.SaveAsync(new {
  url = "https://seu-sistema.com.br/kernos/webhook",
  events = new[] {"sales_order.finish_packing", "shipment.dispatch"}
});
```

#### Go

**Cadastrar o webhook**

```go
client, err := kernos.New(os.Getenv("KERNOS_API_KEY"))
result, err := client.IntegrationWebhook.Save(ctx, map[string]any{
  "url": "https://seu-sistema.com.br/kernos/webhook",
  "events": []any{"sales_order.finish_packing", "shipment.dispatch"},
})
```

#### Rust

**Cadastrar o webhook**

```rust
let kernos = Kernos::new(&std::env::var("KERNOS_API_KEY")?)?;
let result = kernos.integration_webhook().save(&json!({
  "url": "https://seu-sistema.com.br/kernos/webhook",
  "events": ["sales_order.finish_packing", "shipment.dispatch"]
}), None)?;
```

Na primeira gravação a resposta traz o `secret` (`whsec_…`). Ele aparece **uma vez**. `GET /v1/webhook-endpoint` lê o endereço, os eventos e as últimas 50 entregas.

## Confira a assinatura

Cada entrega traz `Kernos-Signature: t=<unix>,v1=<hex>`, com `v1 = HMAC-SHA256(segredo, "<t>.<corpo cru>")`. Confira **antes** de confiar no corpo, com o corpo CRU, e recuse `t` velho (mais de 5 minutos): é o que barra o replay.

> **Não mexa no corpo cru** A assinatura vale sobre o texto exato que chegou. Se o seu framework reparseia o JSON antes de você conferir, a conferência falha.

#### curl

**Conferir a assinatura**

```sh
# Sem SDK, confira à mão: v1 = HMAC-SHA256(segredo, "<t>.<corpo cru>"), em hexadecimal.
# O cabeçalho é: Kernos-Signature: t=<unix>,v1=<hex>  (na troca do segredo vêm dois v1: aceite qualquer um)
```

#### Node

**Conferir a assinatura**

```js
import express from 'express'
import { Kernos } from '@kernosbr/kernos'

const app = express()

// O corpo CRU: a assinatura vale sobre o texto exato que chegou.
app.post('/kernos/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  let event
  try {
    event = Kernos.webhooks.constructEvent(req.body.toString(), req.header('Kernos-Signature'), process.env.KERNOS_WEBHOOK_SECRET)
  } catch {
    return res.sendStatus(400)
  }
  res.sendStatus(200) // responda 2xx logo; o trabalho pesado vai para uma fila
  console.log(event.type, event.id)
})
```

#### Python

**Conferir a assinatura**

```python
from kernos import webhooks

# raw_body é o texto exato que chegou, antes de qualquer json.loads
event = webhooks.construct_event(raw_body, request.headers.get("Kernos-Signature"), os.environ["KERNOS_WEBHOOK_SECRET"])
print(event["type"], event["id"])
```

#### PHP

**Conferir a assinatura**

```php
<?php
// $rawBody é o texto exato que chegou: file_get_contents('php://input')
$event = Kernos\Webhook::constructEvent($rawBody, $_SERVER['HTTP_KERNOS_SIGNATURE'] ?? null, getenv('KERNOS_WEBHOOK_SECRET'), 300);
echo $event['type'], ' ', $event['id'];
```

#### Java

**Conferir a assinatura**

```java
// rawBody é o texto exato que chegou, antes de qualquer parser
JsonNode event = Webhook.constructEvent(rawBody, request.getHeader("Kernos-Signature"), System.getenv("KERNOS_WEBHOOK_SECRET"), Duration.ofMinutes(5));
System.out.println(event.get("type").asText());
```

#### C#

**Conferir a assinatura**

```csharp
// rawBody é o texto exato que chegou, antes de qualquer parser
var evt = Webhook.ConstructEvent(rawBody, Request.Headers["Kernos-Signature"], Environment.GetEnvironmentVariable("KERNOS_WEBHOOK_SECRET"), TimeSpan.FromMinutes(5));
Console.WriteLine(evt["type"]);
```

#### Go

**Conferir a assinatura**

```go
// rawBody é o texto exato que chegou, antes de qualquer parser
event, err := kernos.ConstructEvent(rawBody, r.Header.Get("Kernos-Signature"), os.Getenv("KERNOS_WEBHOOK_SECRET"), 5*time.Minute)
if err != nil {
	http.Error(w, "assinatura inválida", http.StatusBadRequest)
	return
}
fmt.Println(event.Type, event.ID)
```

#### Rust

**Conferir a assinatura**

```rust
// raw_body é o texto exato que chegou, antes de qualquer parser
let event = kernos::webhook::construct_event(&raw_body, signature.as_deref(), &secret, Some(300))?;
println!("{}", event["type"]);
```

Responda 2xx logo e faça o trabalho pesado depois. O `Kernos-Event-Id` é o id do fato e é o mesmo em cada nova tentativa: use-o para não processar duas vezes.

## Teste sem esperar o galpão

`POST /v1/webhook-endpoint/ping` manda um evento de teste ao seu endereço, assinado como em produção, com `data` de exemplo e `test: true`. Sem `type`, é um `ping`; com ele, um dos eventos públicos.

#### curl

**Mandar um evento de teste**

```sh
curl -X POST "https://stag.kernos.com.br/v1/webhook-endpoint/ping" \
  -H "Authorization: Bearer ksk_test_…" \
  -H "Kernos-Version: 2026-10-03" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "type": "ping" }'
```

#### Node

**Mandar um evento de teste**

```js
import { Kernos } from '@kernosbr/kernos'

const kernos = new Kernos(process.env.KERNOS_API_KEY)
const result = await kernos.integrationWebhook.ping({ type: 'ping' })
```

#### Python

**Mandar um evento de teste**

```python
import os
from kernos import Kernos

kernos = Kernos(os.environ["KERNOS_API_KEY"])
result = kernos.integration_webhook.ping({"type": "ping"})
```

#### PHP

**Mandar um evento de teste**

```php
<?php
$kernos = new Kernos\Kernos(getenv('KERNOS_API_KEY'));
$result = $kernos->integrationWebhook->ping(['type' => 'ping']);
```

#### Java

**Mandar um evento de teste**

```java
Kernos kernos = new Kernos(System.getenv("KERNOS_API_KEY"));
JsonNode result = kernos.integrationWebhook().ping(Map.of("type", "ping"));
```

#### C#

**Mandar um evento de teste**

```csharp
var kernos = new KernosClient(Environment.GetEnvironmentVariable("KERNOS_API_KEY"));
var result = await kernos.IntegrationWebhook.PingAsync(new { type = "ping" });
```

#### Go

**Mandar um evento de teste**

```go
client, err := kernos.New(os.Getenv("KERNOS_API_KEY"))
result, err := client.IntegrationWebhook.Ping(ctx, map[string]any{"type": "ping"})
```

#### Rust

**Mandar um evento de teste**

```rust
let kernos = Kernos::new(&std::env::var("KERNOS_API_KEY")?)?;
let result = kernos.integration_webhook().ping(&json!({ "type": "ping" }), None)?;
```

## Depure as entregas

Cada entrega é Entregue, Pendente ou Falhou, com o status HTTP e a hora da próxima tentativa.

| Resposta do seu servidor | Causa | O que fazer |
| --- | --- | --- |
| Não conectou | A Kernos não alcançou o seu servidor. | Confira se o domínio é público e responde na internet. |
| 3xx | O seu servidor redirecionou. Redirecionamento conta como falha. | Cadastre o endereço final, sem redirecionar. |
| 4xx | O endereço recusou o POST. | Aceite POST e deixe a rota sem login. |
| 5xx | O seu código falhou ao processar. | Veja o log do seu servidor e responda 2xx antes do trabalho pesado. |
| Tempo esgotado | O endereço demorou mais de 10 s. | Responda na hora e processe numa fila. |

## Como a entrega se comporta

- **Novas tentativas.** A Kernos tenta de novo por até 3 dias, com espera crescente (30 s × 2ⁿ, até 1 hora).
- **Reenvio.** [`POST /v1/webhook-endpoint/deliveries/{delivery}/resend`](/referencia/integration/webhook/resend.md) põe a entrega de volta na fila agora, com os 3 dias contando de novo. Pela CLI: `kernos events resend <entrega>`.
- **Ordem.** Os eventos não têm ordem garantida. Use o id do evento para não processar duas vezes.
- **Trocar o segredo.** `PUT /v1/webhook-endpoint` com `rotate_secret: true` troca o `whsec_`. Com `grace_hours` (0 a 168), o segredo antigo continua valendo e a assinatura traz **dois** `v1`, o novo e o antigo: o seu receptor aceita qualquer um. Os SDKs já aceitam.
- **Ler eventos sem endereço.** [`GET /v1/events`](/referencia/event/list.md) devolve os mesmos corpos, das últimas 24 horas ou depois de um `after`.

Os eventos têm [uma página cada](/eventos.md), com o corpo e o exemplo. Para o endereço lido: [`GET /v1/webhook-endpoint`](/referencia/integration/webhook/get.md).
