> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gol.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Create signed webhook endpoints and verify every delivery with the SDK.

A webhook tells your server when to look. It does not tell you what is true: delivery is at least once, may arrive out of order, and carries no authority. `getGasExecution` remains the source of truth, and every integration should reconcile against it. [Webhooks](/guides/webhooks) covers delivery timing and retries; this page is the SDK surface.

## Create an endpoint

The returned `secret` is shown once. Store it where you can read it on every delivery, and nowhere else.

```ts theme={null}
import { GolApiClient } from "@gol/sdk/server";

declare const gol: GolApiClient;
declare const projectId: string;

const { endpoint, secret } = await gol.createWebhookEndpoint(projectId, {
  url: "https://example.com/gol/webhooks",
  description: "Agent action updates",
  eventTypes: [
    "gas_execution.updated",
    "gas_policy.confirmed",
    "gas_policy.paused",
    "gas_policy.resumed",
    "gas_policy.revoked",
  ],
});

// Store `secret` (a whsec_ value) in your secret store. It is never shown again.
// Later reads return `secretHint` instead, so an endpoint can be identified
// without exposing its secret.
console.log(endpoint.id, endpoint.status, secret ? "secret captured" : "missing");
```

The URL must be `https` and resolve to a public address, and redirects are not followed. A project environment can have up to 10 endpoints.

## Verify every delivery

`verifyWebhook` checks the signature against the exact raw request body. It uses a timing-safe comparison, rejects a timestamp outside the tolerance, and confirms the body parses and its `id` matches the `gol-webhook-id` header. It lives in the server entry point because it needs the Node.js `crypto` module and your secret.

The critical detail is the raw body. Parse JSON first and the signature no longer matches.

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
import { verifyWebhook } from "@gol/sdk/server";
import type { GolWebhookEvent } from "@gol/sdk/server";

const seen = new Set<string>();
const secret = process.env.GOL_WEBHOOK_SECRET!;

createServer((request, response) => {
  const chunks: Buffer[] = [];
  request.on("data", (chunk: Buffer) => chunks.push(chunk));
  request.on("end", () => {
    try {
      const event = verifyWebhook<Record<string, unknown>>(
        Buffer.concat(chunks).toString("utf8"),
        request.headers,
        secret,
      );
      if (!seen.has(event.id)) {
        seen.add(event.id);
        // Deliveries are at least once and may be out of order. Deduplicate by
        // event id, then reconcile against the API rather than trusting data.
      }
      response.writeHead(204).end();
    } catch {
      response.writeHead(400).end();
    }
  });
});
```

In a framework, configure a raw body parser for the webhook route only. A global JSON parser is the most common reason verification fails.

Every failure throws `GolWebhookVerificationError` with one of: missing signature headers, a timestamp outside the tolerance, an invalid signature, or a webhook id mismatch. Reject with a non-2xx status in all four cases and let GOL retry.

`verifyWebhook` is a constant-time HMAC-SHA256 over `timestamp.rawBody`, keyed by the endpoint secret, which is exactly what the signature header carries. You can compute it yourself if you need to, but the helper also enforces the tolerance window and the id check for you.

## Rotate without dropping events

During a rotation both secrets sign deliveries for 24 hours, and `verifyWebhook` accepts an array. Verify against both, then replace the old one once the overlap ends.

```ts theme={null}
import { verifyWebhook } from "@gol/sdk/server";
import type { GolWebhookEvent } from "@gol/sdk/server";

declare const rawBody: string;
declare const headers: Record<string, string | string[] | undefined>;

const current = process.env.GOL_WEBHOOK_SECRET!;
const previous = process.env.GOL_WEBHOOK_SECRET_PREVIOUS;

const event: GolWebhookEvent = verifyWebhook(
  rawBody,
  headers,
  previous ? [current, previous] : current,
);
```

Rotate from `rotateWebhookSecret`, which returns a new one-time `secret`. Store the new value, deploy it alongside the old, and delete the previous variable after the overlap.

## Event types

| Type                    | `data`                                           |
| ----------------------- | ------------------------------------------------ |
| `gas_execution.updated` | The full action, as `getGasExecution` returns it |
| `gas_policy.confirmed`  | The owner gas policy                             |
| `gas_policy.paused`     | The owner gas policy after an owner pause        |
| `gas_policy.resumed`    | The owner gas policy after an owner resume       |
| `gas_policy.revoked`    | The owner gas policy                             |
| `webhook.test`          | `{ endpointId }`                                 |

`GolWebhookEvent<Data>` is generic, so you can type the payload you expect for a handler. Every event also carries `id`, `type`, `createdAt`, `projectId`, and `environment`. The `type` union includes `(string & {})`, so an unknown type from a future release is still assignable and reaching your switch rather than failing to compile.

## Manage endpoints

```ts theme={null}
import { GolApiClient } from "@gol/sdk/server";

declare const gol: GolApiClient;
declare const projectId: string;
declare const endpointId: string;

const endpoints = await gol.listWebhookEndpoints(projectId);

// Prove the endpoint is reachable before relying on it.
const test = await gol.sendWebhookTest(projectId, endpointId);

const rotated = await gol.rotateWebhookSecret(projectId, endpointId);

const disabled = await gol.setWebhookEndpointEnabled(
  projectId,
  endpointId,
  false,
);

const deleted = await gol.deleteWebhookEndpoint(projectId, endpointId);
```

An endpoint carries a `status` of `enabled` or `disabled`, and `setWebhookEndpointEnabled` returns it updated. A disabled endpoint keeps its pending deliveries for 72 hours rather than dropping them instantly.

`listWebhookDeliveries(projectId, endpointId, limit?)` returns recent attempts with their status and response code, which is the fastest way to tell a signature bug from a delivery problem.

## Type a handler

```ts theme={null}
import type { GasExecutionResponse } from "@gol/sdk";
import type { GolWebhookEvent } from "@gol/sdk/server";

type ActionEvent = GolWebhookEvent<GasExecutionResponse>;

export const isActionUpdate = (
  event: GolWebhookEvent,
): event is ActionEvent => event.type === "gas_execution.updated";
```

`GasExecutionResponse` comes from the browser-safe root entry point, so a shared handler module can import it without pulling in your secret.
