---
title: "Receive and verify gscdump webhooks"
description: "Verify the raw body before processing a delivery."
canonical_url: "https://gscdump.com/gscdump-sdk/guides/build-integrations/webhooks"
last_updated: "2026-10-03T07:15:32.067Z"
---

# Receive and verify gscdump webhooks

Use a partner server endpoint for webhook delivery. Verify the signature before acting on an event.

::pre{tabindex="0" svg="<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1179.32 116.9" width="1179.32" height="116.9" style="--bg:var(--ui-bg-muted);--fg:var(--ui-text-highlighted);--accent:var(--ui-link)">
<style>
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;display=swap');
  text { font-family: 'Inter', system-ui, sans-serif; }
  svg {
    /* Derived from --bg and --fg (overridable via --line, --accent, etc.) */
    --_text:          var(--fg);
    --_text-sec:      var(--muted, color-mix(in srgb, var(--fg) 60%, var(--bg)));
    --_text-muted:    var(--muted, color-mix(in srgb, var(--fg) 40%, var(--bg)));
    --_text-faint:    color-mix(in srgb, var(--fg) 25%, var(--bg));
    --_line:          var(--line, color-mix(in srgb, var(--fg) 50%, var(--bg)));
    --_arrow:         var(--accent, color-mix(in srgb, var(--fg) 85%, var(--bg)));
    --_node-fill:     var(--surface, color-mix(in srgb, var(--fg) 3%, var(--bg)));
    --_node-stroke:   var(--border, color-mix(in srgb, var(--fg) 20%, var(--bg)));
    --_group-fill:    var(--bg);
    --_group-hdr:     color-mix(in srgb, var(--fg) 5%, var(--bg));
    --_inner-stroke:  color-mix(in srgb, var(--fg) 12%, var(--bg));
    --_key-badge:     color-mix(in srgb, var(--fg) 10%, var(--bg));
  }
</style>
<defs>
  <marker id="arrowhead" markerWidth="8" markerHeight="5" refX="7" refY="2.5" orient="auto">
    <polygon points="0 0, 8 2.5, 0 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
  </marker>
  <marker id="arrowhead-start" markerWidth="8" markerHeight="5" refX="1" refY="2.5" orient="auto-start-reverse">
    <polygon points="8 0, 0 2.5, 8 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
  </marker>
</defs>
<polyline class="edge" data-from="Delivery" data-to="Raw" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="132.338,58.45 180.338,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<polyline class="edge" data-from="Raw" data-to="Verify" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="309.726,58.45 357.726,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<polyline class="edge" data-from="Verify" data-to="Parse" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="533.797,58.45 581.797,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<polyline class="edge" data-from="Parse" data-to="Deduplicate" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="768.983,58.45 816.983,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<polyline class="edge" data-from="Deduplicate" data-to="Process" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="962.673,58.45 1010.673,58.45" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
<g class="node" data-id="Delivery" data-label="Delivery" data-shape="rectangle">
  <rect x="40" y="40" width="92.338" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="86.169" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Delivery</text>
</g>
<g class="node" data-id="Raw" data-label="Read raw body" data-shape="rectangle">
  <rect x="180.338" y="40" width="129.388" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="245.03199999999998" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Read raw body</text>
</g>
<g class="node" data-id="Verify" data-label="Verify HMAC signature" data-shape="rectangle">
  <rect x="357.726" y="40" width="176.07099999999997" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="445.76149999999996" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Verify HMAC signature</text>
</g>
<g class="node" data-id="Parse" data-label="Parse contract envelope" data-shape="rectangle">
  <rect x="581.797" y="40" width="187.18599999999998" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="675.39" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Parse contract envelope</text>
</g>
<g class="node" data-id="Deduplicate" data-label="Check delivery ID" data-shape="rectangle">
  <rect x="816.983" y="40" width="145.69" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="889.828" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Check delivery ID</text>
</g>
<g class="node" data-id="Process" data-label="Process event" data-shape="rectangle">
  <rect x="1010.673" y="40" width="128.647" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
  <text x="1074.9965" y="58.45" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">Process event</text>
</g>
</svg>"}
```mermaid
flowchart LR
  Delivery --> Raw[Read raw body]
  Raw --> Verify[Verify HMAC signature]
  Verify --> Parse[Parse contract envelope]
  Parse --> Deduplicate[Check delivery ID]
  Deduplicate --> Process[Process event]
```
::

## Before you start

Configure the signing secret for your webhook endpoint. Your endpoint verifies and processes each delivery. The [hosted HTTP contract](/gscdump-sdk/api/hosted-http#api-specifications) defines the installed event schema.

## Read the raw request body

Read `request.text()` before JSON parsing. Verification uses the exact signed bytes. Do not reconstruct JSON or change whitespace first.

## Verify the signature

::pre{tabindex="0"}
```ts
import { parseWebhookPayload, WEBHOOK_SIGNATURE_HEADER } from '@gscdump/sdk/webhook'

export async function handleWebhook(request: Request, secret: string) {
  const raw = await request.text()
  const envelope = await parseWebhookPayload(raw, {
    secret,
    signature: request.headers.get(WEBHOOK_SIGNATURE_HEADER),
  })
  return envelope
}
```
::

`parseWebhookPayload` verifies the signature and parses the contract envelope. An invalid signature rejects the request. If you need separate steps, use `verifyWebhookSignature` on the raw body first.

## Read a typed event

The parsed envelope is discriminated on `event`. Narrow on `event` before you read `data`:

::pre{tabindex="0"}
```ts
if (envelope.event === 'user.allowance.notice') {
  const { userId, meter, threshold, used, allowance, period } = envelope.data
  await sendAllowanceNotice({ userId, meter, threshold, used, allowance, period })
}
```
::

gscdump sends `user.allowance.notice` only to a metered partner, at 80% and 100% of a free allowance. gscdump sends no email to that partner's users, so the partner sends its own notice. `parseWebhookPayload` rejects a notice whose `data` does not match the contract.

## Process repeats and failures

Use `envelope.deliveryId` to deduplicate work. Make writes idempotent. Return success after durable processing or a durable queue accepts the work. A repeated delivery must not repeat a side effect.

## Check delivery

Test with a signed payload from your installed contract or a real test delivery. Check valid signature, changed body, missing signature, duplicate delivery, and handler failure. Log the delivery ID, never the signing secret. See [Errors and retry](/gscdump-sdk/guides/operate/errors-and-retry) for the hosted HTTP path.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
