# Justt Webhooks

[Justt](https://www.justt.ai) manages chargebacks and automates disputes for merchants. Its webhooks report chargeback, pre-chargeback alert and CRM-subscription-cancellation events as a signed JSON POST to each endpoint you add in the Justt webhooks dashboard. Signatures follow Standard Webhooks, so each delivery carries `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. Webhooks are not available to every Justt customer yet. The dashboard, message ids and retry/recovery tooling are Svix's.

## When to Use This Skill

* How do I receive Justt webhooks?
* How do I verify a Justt webhook signature (`webhook-signature`)?
* Where do I find my Justt webhook signing secret (`whsec_...`)?
* How do I handle `chargeback.created`, `chargeback.status.updated` or `pre_chargeback_alert.received` events?
* Why is my Justt webhook verification failing, or failing only on retries and replays?

## Verification (core)

| Header | Value |
| --- | --- |
| `webhook-id` | `msg_...`; identifies the message and is stable across retries. Your idempotency key (the body has no event id) |
| `webhook-timestamp` | Unix seconds |
| `webhook-signature` | One or more space-separated `v1,<base64>` entries |

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`, HMAC-SHA256, base64 output. The key is the base64-**decoded** part of the `whsec_...` secret after the prefix. Compare against every `v1,` entry; ignore other versions. If `webhook-id` is absent, read `svix-id`, `svix-timestamp` and `svix-signature` instead, as Hookdeck does. The examples use the `standardwebhooks` library (npm and PyPI):

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

// rawBody: exact request body (Buffer or string), NOT re-serialised JSON
function verifyJustt(rawBody, headers, secret = process.env.JUSTT_WEBHOOK_SECRET) {
  const p = headers['webhook-id'] ? 'webhook' : 'svix'; // fall back to the svix-* names
  new Webhook(secret).verify(rawBody, {
    'webhook-id': headers[`${p}-id`],
    'webhook-timestamp': headers[`${p}-timestamp`],
    'webhook-signature': headers[`${p}-signature`],
  }, { jsonParse: false }); // throws on a bad signature or stale timestamp → respond 401
}

```

The library always enforces a fixed **5-minute timestamp window**. Hookdeck retries and replays resend the original `webhook-timestamp`, so the library rejects them once they are more than 5 minutes old. If you receive through Hookdeck and need late retries to pass, use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/justt-webhooks/references/verification.md), which can drop or widen the window. In Express, read the body with `express.raw({ type: '*/*' })` so any content type verifies, and parse JSON only after the check.

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

## Event Type and Payload

Every body is the same envelope, and the event name is the top-level `type`:

```json
{
  "type": "chargeback.created",
  "timestamp": "2022-03-15T09:39:24.514Z",
  "data": { "id": "04318fb0-e4ff-4712-972e-4db99b3fb23c", "chargebackId": "QRZ54SXB4K6TF232", "psp": "stripe", "status": "waiting_for_data" }
}

```

`timestamp` is when the event occurred (ISO 8601). `data` is the event-specific object. The signature page's code sample uses a `ping` body with `event_type` instead; that is a sample, not one of the reference payloads, so route on `type`.

## Common Event Types

| Event | Fired when |
| --- | --- |
| `chargeback.created` | A new chargeback is received from a connected PSP |
| `chargeback.updated` | A chargeback is updated |
| `chargeback.status.updated` | A chargeback status update is received |
| `chargeback.evidence.submitted` | Evidence is submitted to the PSP (`data.status` is `succeeded` or `failed`) |
| `chargeback.accepted` | A chargeback acceptance is confirmed by the PSP (`data.status` is `succeeded` or `failed`) |
| `pre_chargeback_alert.received` | A pre-chargeback alert arrives from CDRN, RDR or Ethoca |
| `pre_chargeback_alert.refund.succeeded` | The transaction linked to a refunded alert was refunded through the PSP |
| `pre_chargeback_alert.refund.failed` | Processing a refund alert failed |
| `pre_chargeback_alert.transaction_matched` | An alert was matched to its transaction (opt-in, enabled by Justt per account) |
| `subscription.cancel_immediately.success` | A CRM subscription (for example Chargebee or Recurly) was cancelled after a refund |
| `subscription.cancel_immediately.failed` | That cancellation failed (`data.failureCode`, `data.failureReason`) |

These 11 are the documented events, and the examples route all of them (the list may not be exhaustive: ignore types you don't recognise). See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/justt-webhooks/references/overview.md).

## Delivery Semantics

Return a `2xx` within 15 seconds (the reference spec says 30), then process asynchronously. 4xx responses other than `410`, 5xx responses, timeouts and connection failures are retried on an 8-attempt schedule (immediately, then after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h and 10 h, about 27.6 hours in all). `410 Gone` is permanent and is not retried. An endpoint that fails for 5 consecutive days is disabled. Deduplicate on `webhook-id`. Return `400` for missing headers and `401` for a bad signature; handlers fail closed (`500`) when `JUSTT_WEBHOOK_SECRET` is unset.

## Environment Variables

```bash
JUSTT_WEBHOOK_SECRET=whsec_xxxxx   # full "Signing Secret" from the endpoint's settings; handlers fail closed without it

```

Each endpoint has its own secret. The docs never print the prefix; the sample base64-decodes the part after `_`, which matches `whsec_<base64>`.

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 justt --path /webhooks/justt

```

Set the printed URL as the endpoint URL in the Justt webhooks dashboard, then use the endpoint's **Testing** tab to send a signed example.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/justt-webhooks/references/overview.md) - Event catalog, envelope and payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/justt-webhooks/references/setup.md) - Add an endpoint, find the secret, test, retries
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/justt-webhooks/references/verification.md) - Library and manual verification, gotchas, debugging