# Skovik Webhooks

[Skovik](https://developer.skovik.com/docs/api-webhooks-receiving) is an operations platform whose API
exposes users, branches and export orders. You add webhook endpoints in the settings UI, and Skovik
delivers each event as a JSON `POST` signed with
Standard Webhooks headers: `webhook-id`,
`webhook-timestamp` and `webhook-signature`. Deliveries are thin notifications: the event name is the
top-level `type`, the related resource is only linked, and `data.id` is the deduplication key.

## When to Use This Skill

* How do I receive Skovik webhooks?
* How do I verify Skovik webhook signatures (`webhook-signature`, `whsec_` secret)?
* How do I handle `export_orders.completed`, `users.created` or `branches.updated` events?
* What does the `misc.ping` test event from the settings UI look like?
* Why is my Skovik webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. Skovik signs it with HMAC-SHA256 using the
base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one or more
space-separated `v1,<signature>` entries. Use a Standard Webhooks library (`standardwebhooks` on npm and
PyPI), which compares in constant time. Read the `webhook-*` headers, and when `webhook-id` is absent fall
back to the `svix-*` headers (`svix-id`, `svix-timestamp`, `svix-signature`), as Hookdeck does.

```javascript
const { Webhook } = require('standardwebhooks');

const prefix = req.get('webhook-id') ? 'webhook' : 'svix';
const headers = {
  'webhook-id': req.get(`${prefix}-id`),
  'webhook-timestamp': req.get(`${prefix}-timestamp`),
  'webhook-signature': req.get(`${prefix}-signature`),
};
// rawBody: the raw Buffer/string, never re-serialized JSON
const event = new Webhook(process.env.SKOVIK_WEBHOOK_SECRET).verify(rawBody, headers);
// throws on a bad signature or a timestamp outside the window; returns the parsed body

```

```python
from standardwebhooks.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["SKOVIK_WEBHOOK_SECRET"])  # "whsec_..."
event = wh.verify(raw_body, {"webhook-id": id_, "webhook-timestamp": ts, "webhook-signature": sig})

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/skovik-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/skovik-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/skovik-webhooks/examples/fastapi/).

Each endpoint has its own secret, managed in the settings page. It starts with `whsec_`, and regenerating
it invalidates the old one immediately. Keep the full value in `SKOVIK_WEBHOOK_SECRET`. Handlers fail
closed: with the variable unset they return 500 instead of accepting anything. A missing header or a bad
signature returns 400. The raw body is required, so in Express use `express.raw({ type: '*/*' })` and
parse the body only after the check.

### Hookdeck retries and replays hit the 5-minute window

The Standard Webhooks libraries always enforce a fixed 5-minute timestamp window, and it can't be
configured. Skovik re-signs each attempt with a fresh `webhook-timestamp`, but Hookdeck retries and
replays resend the **original** timestamp, so the library rejects any retry or replay that happens more
than 5 minutes after the original delivery. If you verify behind Hookdeck (or replay old events), use the
manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/skovik-webhooks/references/verification.md), where the window can be
dropped or widened. Deduplicate on `data.id` instead of relying on the timestamp.

## Common Event Types

Ten events are documented, and the list may not be exhaustive. The name is `type` in the JSON body. The
examples handle all of these:

| Event | Triggered When |
| --- | --- |
| `users.created` | A user is created |
| `users.updated` | Some part of the user object changed |
| `users.archived` | A user is archived |
| `export_orders.created` | An export order is created |
| `export_orders.completed` | An export order completed; fetch it for the export file URL |
| `export_orders.failed` | An export order failed; fetch it and read `error_message` |
| `branches.created` | A branch is created |
| `branches.updated` | A branch changed |
| `branches.archived` | A branch is archived |
| `misc.ping` | A test sent from the settings UI (signed, never retried, no related resource) |

The related resource is only linked, at `data.relationships.related_object` (omitted when there is none).
Fetch its current state from the `links.related` URL. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/skovik-webhooks/references/overview.md)
for the payload shape.

## Environment Variables

```bash
# Signing secret of the webhook endpoint (Standard Webhooks format, starts with whsec_)
SKOVIK_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 skovik --path /webhooks/skovik

```

Add an HTTPS endpoint in Skovik's settings with the tunnel URL, then use the settings UI to send a
`misc.ping`.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/skovik-webhooks/references/overview.md) - Skovik webhook concepts, events, payload shape, delivery and retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/skovik-webhooks/references/setup.md) - Add an endpoint in the settings UI, get the secret, send a test ping
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/skovik-webhooks/references/verification.md) - Standard Webhooks verification details, header fallback, manual verifier, timestamp window