# GST Cranes Webhooks

GST Cranes webhooks let a signed-in member register an HTTPS endpoint and receive signed JSON notifications for account-owned marketplace events.

## Endpoints

- Register or list endpoints: `https://gstcranes.com/api/webhooks/endpoints`
- Send a test delivery: `https://gstcranes.com/api/webhooks/test`
- Machine-readable webhook profile: `https://gstcranes.com/.well-known/webhooks`

Endpoint registration requires the normal GST Cranes browser session. Public documentation and the webhook profile are crawlable without sign-in.

## Events

| Event | When it is sent |
| --- | --- |
| `webhook.test` | A signed-in member sends a test delivery to one of their active webhook endpoints. |
| `saved_alert.match` | Best-effort companion delivery when a newly published marketplace listing matches a saved alert and the saved-alert notification fan-out reaches that alert. |

## Signing

Every delivery is signed with HMAC-SHA256 using the endpoint secret shown once during creation.

GST Cranes never sends unsigned webhook deliveries. Reject any delivery without a signature header before parsing the JSON payload.

Secret format: `whsec_<random endpoint secret shown once at creation>`

Signature version: `v1`

Branded signature header: `GST-Webhook-Signature`. HTTP clients may expose it as lowercase `gst-webhook-signature`; header names are case-insensitive.

Sent header names: `gst-webhook-id`, `gst-webhook-event`, `gst-webhook-timestamp`, `gst-webhook-signature`.

Header details:

- `gst-webhook-id` / `GST-Webhook-Id`
- `gst-webhook-event` / `GST-Webhook-Event`
- `gst-webhook-timestamp` / `GST-Webhook-Timestamp`
- `gst-webhook-signature` / `GST-Webhook-Signature: v1=<hex digest>`

The signature base string is `timestamp + "." + raw_body`.

The signature input is `GST-Webhook-Timestamp + "." + raw_body`, where `raw_body` is the exact request body bytes before JSON parsing.

Verify the raw request body bytes before JSON parsing. Do not reserialize parsed JSON to verify a delivery; JSON key order and whitespace must not be reconstructed.

Verification rules:

- Read the raw request body before JSON parsing.
- Reject `GST-Webhook-Timestamp` values outside 300 seconds of your server clock.
- Compute HMAC-SHA256 over `GST-Webhook-Timestamp + "." + raw_body` using the endpoint secret.
- Compare the expected `v1=` digest against `GST-Webhook-Signature` with a constant-time compare.
- Store processed `GST-Webhook-Id` values per endpoint and reject repeats before processing.
- Reject duplicate `GST-Webhook-Id` values per endpoint for replay protection.

Node verification sketch:

```js
import crypto from 'node:crypto';

function verifyGstWebhook({ rawBody, timestamp, signature, secret }) {
  const expected =
    'v1=' + crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    throw new Error('Invalid GST-Webhook-Signature');
  }
}
```

Official SDK verifier:

`npm install @gstcranes/gstcranes`

Package: https://www.npmjs.com/package/@gstcranes/gstcranes

```ts
import { verifyGstWebhookSignature } from '@gstcranes/gstcranes';

const ok = await verifyGstWebhookSignature({
  rawBody,
  timestamp: request.headers.get('GST-Webhook-Timestamp') ?? '',
  signature: request.headers.get('GST-Webhook-Signature') ?? '',
  secret: process.env.GST_WEBHOOK_SECRET ?? '',
});

if (!ok) throw new Error('Invalid GST-Webhook-Signature');
```

## Example payload

```json
{
  "id": "wh_123",
  "event": "saved_alert.match",
  "created": 1780000000,
  "data": {
    "alert": { "id": "alert_1" },
    "listing": {
      "id": "listing_1",
      "slug": "liebherr-ltm-1750-9-1",
      "brand": "Liebherr",
      "model": "LTM 1750-9.1",
      "country": "DE",
      "kind": "sell"
    }
  }
}
```
