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

# Webhooks

> Receive real-time HTTP POST notifications when code changes and sync lifecycle events. Subscribe to push and repo sync events, verify HMAC signatures for security, and integrate Code Storage into your CI/CD pipelines, monitoring systems, and product workflows.

Code Storage provides a webhook system that allows you to receive real-time notifications about Git
events across your storage layer. This enables you to integrate Code Storage deeply within your
product, CI/CD pipelines, monitoring systems, and more.

## How webhooks work

Webhooks are HTTP POST requests sent to your specified endpoint whenever certain events occur in
your repositories. When you create a webhook subscription, Code Storage will:

1. **Monitor Events**: Watch for the events you've subscribed to (e.g., `push` or `repo.sync.*`
   events)
2. **Generate Payloads**: Create JSON payloads containing event details
3. **Sign Requests**: Add cryptographic signatures for security verification
4. **Deliver Webhooks**: Send HTTP POST requests to your endpoint with automatic retries

### Webhook headers

Every webhook request includes the following headers:

* `Content-Type: application/json`
* `User-Agent: Pierre-Webhook/1.0`
* `X-Pierre-Event: <event_type>` (e.g., `push`, `repo.sync.started`, `repo.sync.succeeded`,
  `repo.sync.failed`)
* `X-Pierre-Signature: t=1642678200,sha256=abc123...` (security signature)

### Custom headers

Add static headers in the webhook editor to authenticate requests or attach routing metadata, for
example `Authorization: Bearer <token>` or `X-Environment: production`.

* Each subscription supports up to **10 custom headers**, with **8 KiB total** across names, values,
  and the `: ` and line-ending separators (4 bytes per header).
* Custom headers require an **HTTPS** endpoint.
* Names may contain ASCII letters, digits, and hyphens. Names are case-insensitive and must be
  unique. Values must contain printable ASCII characters, without leading or trailing whitespace.
* Values are encrypted when stored. After saving, only names are returned; use **Replace value** to
  enter a new value, **Save value** to keep that edit in the form, **Cancel** to keep the saved
  value, or **Remove header** to delete the header. Save the webhook to apply the changes.
* Changing the webhook URL preserves its saved headers.
* If saved headers cannot be read, the editor shows a warning. You can still disable or delete the
  webhook, or clear or replace its saved headers. Other webhook settings preserve the saved values.

You cannot set reserved headers:

* Pierre's own headers: `X-Pierre*` and `X-Git3P*`.
* Connection and framing headers: `Host`, `User-Agent`, `Content-*`, `Connection`, `Keep-Alive`,
  `Transfer-Encoding`, `TE`, `Trailer`, `Upgrade`, `Expect`, `Accept-Encoding`, `HTTP2-Settings`,
  and `Proxy-*`.
* Client identity headers added by proxies and CDNs: `Forwarded`, `Via`, `X-Forwarded-*`,
  `X-Envoy-*`, `X-Real-IP`, `X-Client-IP`, `True-Client-IP`, `X-Cluster-Client-IP`,
  `CF-Connecting-IP`, `CF-Connecting-IPv6`, `CF-Ray`, `Fastly-Client-IP`, and `CDN-Loop`.
* Trace propagation headers: `Traceparent*`, `Tracestate*`, `Traceresponse*`, `Baggage*`,
  `X-Cloud-Trace-Context`, `X-Amzn-Trace-Id`, and `X-Amz-Cf-Id`.
* gRPC headers: `Grpc-*`.

Each delivery captures the subscription configuration before its first attempt. Changes to the URL,
headers, or enabled state affect deliveries that capture their configuration after the change;
existing deliveries, including retries, keep their original configuration. An event queued before an
edit can use the new configuration if its delivery has not captured a snapshot yet.

Custom headers do not change signature verification: the signature covers the timestamp and request
body. Endpoint response bodies continue to appear in delivery history.

## Event types

### Common fields

Use these fields to identify the event, repository, and organization:

| Field | Description |
| - | - |
| `type` | The event type. It matches the `X-Pierre-Event` header. |
| `repository.repo_name` | The repository name, for example `team/project-alpha`. |
| `org` | The organization name. It is the `iss` claim of the JWTs that your organization signs. |

### `push`

Triggered when commits are pushed to a repository.

```json theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "type": "push",
  "repository": {
    "repo_name": "team/project-alpha"
  },
  "ref": "refs/heads/main",
  "before": "abc123def456...",
  "after": "def456abc123...",
  "org": "acme",
  "pushed_at": "2024-01-20T10:30:00Z"
}
```

### `repo.sync.started`

Triggered when a repository sync run begins, before the upstream fetch executes. Useful for tracking
sync progress or updating status indicators in your UI.

```json theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "type": "repo.sync.started",
  "repository": {
    "repo_name": "team/project-alpha"
  },
  "org": "acme",
  "run_count": 1,
  "is_first_sync": true,
  "started_at": "2026-03-17T10:00:00Z"
}
```

### `repo.sync.succeeded`

Triggered when a sync run completes successfully.

```json theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "type": "repo.sync.succeeded",
  "repository": {
    "repo_name": "team/project-alpha"
  },
  "org": "acme",
  "run_count": 1,
  "is_first_sync": true,
  "started_at": "2026-03-17T10:00:00Z",
  "completed_at": "2026-03-17T10:02:30Z"
}
```

### `repo.sync.failed`

Triggered when a sync run fails, including mirror activity failures (after retries are exhausted)
and infrastructure-level failures.

```json theme={"theme":{"light":"github-light","dark":"min-dark"}}
{
  "type": "repo.sync.failed",
  "repository": {
    "repo_name": "team/project-alpha"
  },
  "org": "acme",
  "run_count": 1,
  "is_first_sync": true,
  "started_at": "2026-03-17T10:00:00Z",
  "completed_at": "2026-03-17T10:02:30Z",
  "error": "authentication failed"
}
```

### Sync event fields

| Field | Description |
| - | - |
| `run_count` | For `started`, the number of previously completed runs. For `succeeded`/`failed`, includes the run that just finished. |
| `is_first_sync` | `true` if the repository had never successfully synced before this run began. Useful for detecting initial sync completion without comparing run counts. |
| `error` | Present only on `failed` events. Possible values: `"authentication failed"`, `"failed to clone repository"`, `"failed to push to storage"`, `"repository configuration error"`, `"upstream repository unreachable"`, `"internal error"`, `"session unavailable"`, `"sync failed"`. |

> [!NOTE]
>
> Cancellations triggered internally (e.g., when a repository is detached from its upstream) do not
> produce a `repo.sync.failed` event.

## Securing webhooks

To ensure the webhooks you receive are legitimate and from Code Storage, you **must** verify the
HMAC signature included with each webhook delivery.

### HMAC Signature Verification

Each webhook includes an `X-Pierre-Signature` header with the format:

```
X-Pierre-Signature: t=<unix_timestamp>,sha256=<hex_signature>
```

The signature is computed as:

```
HMAC-SHA256(webhook_secret, timestamp + "." + payload)
```

## Webhook SDK methods

The SDK provides a helper function to verify webhook events quickly: `verifyWebhook` in TypeScript,
`verify_webhook` in Python, and `webhooks.VerifyWebhook` in Go. It checks the signature and the
timestamp, then returns the event with its type.

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import {
  verifyWebhook,
  type KnownWebhookEvent,
  type UnknownWebhookEvent,
} from '@pierre/sdk/webhooks';

async function handleWebhookRequest(request: Request) {
  // Get the raw request body as text
  const payload = await request.text();

  // Verify the webhook using the SDK
  let event: KnownWebhookEvent | UnknownWebhookEvent;
  try {
    event = await verifyWebhook({
      payload,
      headers: request.headers, // Headers object with x-pierre-signature
      secret: process.env.WEBHOOK_SECRET!,
    });
  } catch (err) {
    console.error('Invalid webhook:', err);
    return new Response('Invalid webhook', { status: 401 });
  }

  // Route by event type
  switch (event.type) {
    case 'push':
      console.log(`Push to ${event.ref}`);
      console.log(`Commit: ${event.before} -> ${event.after}`);
      break;
    case 'repo.sync.started':
    case 'repo.sync.succeeded':
      console.log(`${event.type} for ${event.repository.repo_name}`);
      break;
    case 'repo.sync.failed':
      console.log(`${event.type} for ${event.repository.repo_name}`);
      console.log(`Error: ${event.error}`);
      break;
    default:
      // An event type that this SDK version does not know
      console.log(`Unhandled event type: ${event.event_type}`);
  }

  return new Response('OK', { status: 200 });
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from collections.abc import Mapping

from pierre import PierreError
from pierre.webhooks import verify_webhook


def handle_webhook_request(payload: bytes, headers: Mapping[str, str]) -> tuple[str, int]:
    # Verify webhook signature
    try:
        event = verify_webhook(
            payload=payload,  # Raw payload bytes or string
            headers=headers,  # Request headers with X-Pierre-Signature
            secret="your-webhook-secret",
            tolerance_seconds=300,  # 5 minutes
        )
    except PierreError as err:
        print(f"Invalid webhook: {err}")
        return "Invalid webhook", 401

    if event["type"] == "push":
        print(f"Push to {event['ref']}")
        print(f"Commit: {event['before']} -> {event['after']}")
    elif event["type"] == "repo.sync.started" or event["type"] == "repo.sync.succeeded":
        print(f"{event['type']} for {event['repository'].get('repo_name')}")
    elif event["type"] == "repo.sync.failed":
        print(f"{event['type']} for {event['repository'].get('repo_name')}")
        print(f"Error: {event['error']}")
    elif event["type"] == "unknown":
        # An event type that this SDK version does not know
        print(f"Unhandled event type: {event['event_type']}")

    return "OK", 200
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import (
	"fmt"
	"io"
	"net/http"
	"time"

	pierre "github.com/pierrecomputer/sdk/go"
	"github.com/pierrecomputer/sdk/go/webhooks"
)

func handleWebhook(w http.ResponseWriter, r *http.Request) {
	// Read the raw request body
	payload, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Cannot read body", http.StatusBadRequest)
		return
	}

	// Verify the webhook signature
	event, err := webhooks.VerifyWebhook(webhooks.VerifyWebhookParams{
		Payload:   payload,
		Header:    r.Header, // Request headers with X-Pierre-Signature
		Secret:    "your-webhook-secret",
		Tolerance: pierre.Value(5 * time.Minute),
	})
	if err != nil {
		fmt.Printf("Invalid webhook: %s\n", err)
		http.Error(w, "Invalid webhook", http.StatusUnauthorized)
		return
	}

	// Route by event type
	switch e := event.(type) {
	case *pierre.GitPushEvent:
		fmt.Printf("Push to %s\n", e.Ref)
		fmt.Printf("Commit: %s -> %s\n", e.Before, e.After)
	case *pierre.GitSyncStartedEvent:
		repoName, _ := e.Repository.RepoName.Get()
		fmt.Printf("%s for %s\n", e.EventType(), repoName)
	case *pierre.GitSyncSucceededEvent:
		repoName, _ := e.Repository.RepoName.Get()
		fmt.Printf("%s for %s\n", e.EventType(), repoName)
	case *pierre.GitSyncFailedEvent:
		repoName, _ := e.Repository.RepoName.Get()
		fmt.Printf("%s for %s\n", e.EventType(), repoName)
		fmt.Printf("Error: %s\n", e.Error)
	case *webhooks.UnknownEvent:
		// An event type that this SDK version does not know
		fmt.Printf("Unhandled event type: %s\n", e.Type)
	}

	w.WriteHeader(http.StatusOK)
}
```

### Advanced SDK usage

Custom Validation Options:

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
async function verifyWithTolerance(payload: string, headers: Headers, webhookSecret: string) {
  return verifyWebhook({
    payload,
    headers,
    secret: webhookSecret,
    tolerance_seconds: 600, // Allow webhooks up to 10 minutes old (default: 300)
  });
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from pierre.webhooks import KnownWebhookEvent, UnknownWebhookEvent


def verify_with_tolerance(
    payload: bytes, headers: Mapping[str, str], webhook_secret: str
) -> KnownWebhookEvent | UnknownWebhookEvent:
    return verify_webhook(
        payload=payload,
        headers=headers,
        secret=webhook_secret,
        tolerance_seconds=600,  # 10 minutes (default: 300)
    )
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
func verifyWithTolerance(payload []byte, header http.Header, webhookSecret string) (webhooks.Event, error) {
	return webhooks.VerifyWebhook(webhooks.VerifyWebhookParams{
		Payload:   payload,
		Header:    header,
		Secret:    webhookSecret,
		Tolerance: pierre.Value(10 * time.Minute), // default: 5 minutes
	})
}
```

Unknown event types: for an event type that this SDK version does not know, such as a type from a
newer API version, the SDK returns the type that Pierre sent and the payload:

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
async function handleUnknownEvent(payload: string, headers: Headers, webhookSecret: string) {
  const event = await verifyWebhook({ payload, headers, secret: webhookSecret });

  if (event.type === 'unknown') {
    // Process the payload as needed
    const eventType = event.event_type;
    const webhookData = event.body;
    // ... custom processing logic
  }
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
def handle_unknown_event(payload: bytes, headers: Mapping[str, str], webhook_secret: str) -> None:
    event = verify_webhook(payload=payload, headers=headers, secret=webhook_secret)

    if event["type"] == "unknown":
        # Process the payload as needed
        event_type = event["event_type"]
        webhook_data = event["body"]
        # ... custom processing logic
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
func handleUnknownEvent(payload []byte, header http.Header, webhookSecret string) error {
	event, err := webhooks.VerifyWebhook(webhooks.VerifyWebhookParams{
		Payload: payload,
		Header:  header,
		Secret:  webhookSecret,
	})
	if err != nil {
		return err
	}

	if unknown, ok := event.(*webhooks.UnknownEvent); ok {
		// Process the payload as needed
		eventType := unknown.Type
		webhookData := unknown.Body // the parsed payload; numbers are json.Number
		fmt.Println(eventType, len(webhookData))
		// ... custom processing logic
	}
	return nil
}
```

### Common verification errors

When the check fails, the SDK raises `WebhookVerificationError`. In Go, `webhooks.VerifyWebhook`
returns a `*pierre.WebhookVerificationError`. Its `reason` field (`Reason` in Go) names the failure:

* **Missing signature components**: `malformed_signature`
* **Timestamp too old**: `timestamp_out_of_range`
* **Future timestamp**: `timestamp_out_of_range`
* **Signature mismatch**: `signature_mismatch`
* **Invalid JSON**: `invalid_payload` (also for a payload with no event type)
* **Missing headers**: `missing_signature` (no `X-Pierre-Signature` header)

A bad call, such as an empty secret, raises a `PierreError` with no reason (`*pierre.PierreError` in
Go).

**Error Handling Best Practices:**

```typescript TypeScript theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import { WebhookVerificationError } from '@pierre/sdk/webhooks';

async function verifyOrReject(payload: string, headers: Headers, secret: string) {
  try {
    await verifyWebhook({ payload, headers, secret });
  } catch (err) {
    // Log the error for debugging (don't expose to clients)
    // Don't log payload or secret for security
    console.error(
      'Webhook verification failed:',
      err instanceof WebhookVerificationError ? err.reason : err,
    );

    // Return appropriate HTTP status codes
    if (err instanceof WebhookVerificationError && err.reason === 'timestamp_out_of_range') {
      return new Response('Request too old', { status: 408 }); // Request Timeout
    }

    return new Response('Invalid webhook signature', { status: 401 });
  }

  return new Response('OK', { status: 200 });
}
```

```python Python theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
from pierre.webhooks import WebhookVerificationError


def verify_or_reject(payload: bytes, headers: Mapping[str, str], secret: str) -> tuple[str, int]:
    try:
        verify_webhook(payload=payload, headers=headers, secret=secret)
    except PierreError as err:
        # Log the error for debugging (don't expose to clients)
        print("Webhook verification failed:", err)

        # Return appropriate HTTP status codes
        if isinstance(err, WebhookVerificationError) and err.reason == "timestamp_out_of_range":
            return "Request too old", 408

        return "Invalid webhook signature", 401

    return "OK", 200
```

```go Go theme={null} theme={"theme":{"light":"github-light","dark":"min-dark"}}
import "errors"

func verifyOrReject(w http.ResponseWriter, payload []byte, header http.Header, secret string) bool {
	_, err := webhooks.VerifyWebhook(webhooks.VerifyWebhookParams{
		Payload: payload,
		Header:  header,
		Secret:  secret,
	})
	if err == nil {
		return true
	}

	// Log the error for debugging (don't expose to clients)
	fmt.Printf("Webhook verification failed: %s\n", err)

	// Return appropriate HTTP status codes
	var verifyErr *pierre.WebhookVerificationError
	if errors.As(err, &verifyErr) && verifyErr.Reason == pierre.WebhookTimestampOutOfRange {
		// Request Timeout
		http.Error(w, "Request too old", http.StatusRequestTimeout)
	} else {
		// Unauthorized
		http.Error(w, "Invalid webhook signature", http.StatusUnauthorized)
	}
	return false
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.