HGV Traders Docs

Webhooks

Receive real-time event notifications via HTTP webhooks.

Overview

Webhooks deliver event payloads to a URL you control whenever something notable happens in your HGV Traders account — new enquiries, stock status changes, job applications, and more.

Registering an endpoint

Configure webhook URLs from Dashboard → Settings → Webhooks. Each endpoint can be filtered to receive only the event types you need.

When you create an endpoint, a signing secret is generated with the prefix whsec_ and shown once. Store it securely — it cannot be retrieved again.

URL requirements

Endpoint URLs must be publicly reachable HTTPS URLs. The platform runs SSRF checks at registration and on every delivery — localhost, private IP ranges, and similar targets are rejected.

Payload format

Every webhook delivery is a POST request with a JSON body:

{
  "event": "enquiry.created",
  "timestamp": "2024-06-01T12:00:00Z",
  "data": { ... }
}

Verifying signatures

Every delivery is signed with HMAC-SHA256, keyed with your webhook secret, and carries two headers:

HeaderExampleMeaning
X-HGV-Signaturet=1717243200,v1=5f5e01a2…The signed timestamp (t, unix seconds) and the signature (v1).
X-HGV-Timestamp1717243200The same unix-seconds timestamp, also available on its own.
X-HGV-EventSTOCK_PUBLISHEDThe event type that triggered the delivery.

The X-HGV-Event header carries the internal enum name (e.g. STOCK_PUBLISHED). The JSON body's event field uses a dot-separated slug (e.g. stock.published). Use the header to match your subscribed event types; use the body event field for logging.

The signature is computed over the timestamped payload "{timestamp}.{rawBody}"not the raw body alone. Including the timestamp in the signed material lets you reject replayed deliveries.

To verify a delivery:

  1. Parse t and v1 out of the X-HGV-Signature header.
  2. Reject the delivery if t is outside your tolerance window (recommended: 5 minutes300 seconds — either side of your current time). This is what stops an attacker from replaying a captured-but-valid payload later.
  3. Recompute HMAC-SHA256(secret, "{t}.{rawBody}") and compare it to v1 using a constant-time comparison.

Verify against the raw request body before JSON parsing — re-serialising can change bytes and break the signature.

import { createHmac, timingSafeEqual } from 'crypto'

const TOLERANCE_SECONDS = 300 // 5 minutes

function verifySignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
  nowSeconds: number = Math.floor(Date.now() / 1000)
): boolean {
  // 1. Parse "t=<unix>,v1=<hex>"
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((kv) => kv.split('=') as [string, string])
  )
  const timestamp = Number(parts.t)
  const provided = parts.v1
  if (!Number.isFinite(timestamp) || !provided) return false

  // 2. Reject replays outside the tolerance window
  if (Math.abs(nowSeconds - timestamp) > TOLERANCE_SECONDS) return false

  // 3. Recompute over "{t}.{rawBody}" and compare in constant time
  const expected = createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(provided)
  return a.length === b.length && timingSafeEqual(a, b)
}

Retry policy

If your endpoint returns a non-2xx status or times out after 10 seconds, delivery is retried with exponential back-off — up to 5 attempts over 24 hours (Inngest-managed).

Return 2xx promptly to acknowledge receipt. Process the payload asynchronously if your handler needs more time.

Delivery logs

The dashboard shows recent delivery attempts per endpoint. Log rows older than 7 days are pruned automatically.

Testing your endpoint

Use Send test on an endpoint in Dashboard → Settings → Webhooks. The platform delivers a signed webhook.test payload:

{
  "event": "webhook.test",
  "timestamp": "2024-06-01T12:00:00.000Z",
  "data": {
    "message": "This is a test delivery from HGV Traders.",
    "subscribedEvent": "ENQUIRY_CREATED"
  }
}

The X-HGV-Event header reflects the endpoint's first subscribed event type. Test deliveries use the same HMAC signing scheme as production events. Test requests are rate-limited (shared with auth attempts per organisation).

Available events

EventDescription
enquiry.createdA new enquiry was submitted on one of your listings
enquiry.repliedYou replied to a buyer enquiry (email or in-app message)
stock.publishedA stock item was published
stock.unpublishedA stock item was unpublished
stock.soldA stock item was marked as sold
job.applicationA new job application was received

Event payload reference

Each delivery wraps event-specific fields in data. The top-level shape is always { event, timestamp, data }.

enquiry.created

{
  "event": "enquiry.created",
  "timestamp": "2024-06-01T12:00:00.000Z",
  "data": {
    "enquiryId": "clenq123abc",
    "stockId": "clstk456def",
    "name": "Jane Buyer",
    "email": "jane@example.com",
    "message": "Is this still available?"
  }
}

enquiry.replied

{
  "event": "enquiry.replied",
  "timestamp": "2024-06-01T14:30:00.000Z",
  "data": {
    "enquiryId": "clenq123abc",
    "channel": "email",
    "stockId": "clstk456def",
    "buyerEmail": "jane@example.com"
  }
}

channel is "email" for outbound email replies or "message" for in-app messages.

stock.published

{
  "event": "stock.published",
  "timestamp": "2024-06-01T09:00:00.000Z",
  "data": {
    "stockId": "clstk456def",
    "title": "2020 DAF XF 530 6x2 Tractor Unit"
  }
}

stock.unpublished

{
  "event": "stock.unpublished",
  "timestamp": "2024-06-02T11:00:00.000Z",
  "data": {
    "stockId": "clstk456def",
    "title": "2020 DAF XF 530 6x2 Tractor Unit"
  }
}

stock.sold

{
  "event": "stock.sold",
  "timestamp": "2024-06-03T16:00:00.000Z",
  "data": {
    "stockId": "clstk456def",
    "title": "2020 DAF XF 530 6x2 Tractor Unit"
  }
}

job.application

{
  "event": "job.application",
  "timestamp": "2024-06-04T10:00:00.000Z",
  "data": {
    "jobId": "cljob789ghi"
  }
}