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

# Quick Start

> Register a webhook endpoint, subscribe to events, and verify your first delivery

This guide walks through the full webhook setup: creating an endpoint in the Paxos Labs Console, writing a minimal receiver, and verifying the connection with a test event.

## Prerequisites

* A Paxos Labs organization with admin access
* A publicly reachable HTTPS server (or a tunnel like [ngrok](https://ngrok.com) for local development)

## Step 1: Create a Webhook Endpoint

<Steps>
  <Step title="Open the Paxos Labs Console">
    Navigate to your organization in the [Paxos Labs Console](https://console.paxoslabs.com) and select the **Webhooks** tab.
  </Step>

  <Step title="Add an endpoint">
    Click **Add Endpoint**. Enter your HTTPS URL, an optional display name, and select the event types you want to receive.
  </Step>

  <Step title="Save your signing secret">
    After creation, a signing secret starting with `pxlwh_` is displayed **once**. Copy it immediately and store it in your secrets manager (e.g., AWS Secrets Manager, Vault, or `.env` for local development).
  </Step>
</Steps>

<Warning>
  The signing secret is shown only at creation time. If you lose it, delete the endpoint and create a new one.
</Warning>

## Step 2: Build a Receiver

In the route for your registered HTTPS handler, verify each request (signature and timestamp) and return a `2xx`-status response within 3 seconds. Slow or failed responses may trigger retries, resulting in your server receiving duplicate events (use the `id` field to process idempotently).

<Tabs>
  <Tab title="Node.js" icon="node-js">
    ```js theme={null}
    import { createServer } from 'node:http'
    import { createHmac, timingSafeEqual } from 'node:crypto'

    const SECRET = process.env.WEBHOOK_SECRET       // pxlwh_...
    const PORT   = process.env.PORT || 4000
    const TOLERANCE_SECONDS = 300                    // 5-minute replay window

    function verify(rawBody, signatureHeader, timestampHeader) {
      const timestamp = new Date(timestampHeader)
      if (Math.abs(Date.now() - timestamp.getTime()) / 1000 > TOLERANCE_SECONDS) {
        throw new Error('Timestamp outside tolerance')
      }

      const expected = createHmac('sha256', SECRET)
        .update(`${timestampHeader}.${rawBody}`)
        .digest('hex')

      const received = Buffer.from(signatureHeader, 'hex')
      if (!timingSafeEqual(received, Buffer.from(expected, 'hex'))) {
        throw new Error('Invalid signature')
      }
    }

    const server = createServer((req, res) => {
      if (req.method !== 'POST') {
        res.writeHead(405).end()
        return
      }

      const chunks = []
      req.on('data', (c) => chunks.push(c))
      req.on('end', () => {
        const rawBody = Buffer.concat(chunks).toString('utf-8')
        try {
          verify(
            rawBody,
            req.headers['x-paxos-labs-signature'],
            req.headers['x-paxos-labs-timestamp'],
          )
          const event = JSON.parse(rawBody)
          console.log(`Received ${event.type} — ${event.id}`)
          res.writeHead(200).end('ok')
        } catch (err) {
          console.error('Verification failed:', err.message)
          res.writeHead(401).end('unauthorized')
        }
      })
    })

    server.listen(PORT, () => console.log(`Listening on :${PORT}`))
    ```
  </Tab>

  <Tab title="Python" icon="python">
    ```python theme={null}
    import hashlib, hmac, json, os
    from datetime import datetime, timezone
    from http.server import HTTPServer, BaseHTTPRequestHandler

    SECRET = os.environ["WEBHOOK_SECRET"].encode()   # pxlwh_...
    TOLERANCE = 300                                   # 5-minute replay window

    class Handler(BaseHTTPRequestHandler):
        def do_POST(self):
            length = int(self.headers.get("Content-Length", 0))
            raw_body = self.rfile.read(length).decode()

            sig_header = self.headers.get("X-PAXOS-LABS-SIGNATURE", "")
            ts_header  = self.headers.get("X-PAXOS-LABS-TIMESTAMP", "")

            try:
                self._verify(raw_body, sig_header, ts_header)
            except Exception as e:
                self.send_response(401)
                self.end_headers()
                self.wfile.write(str(e).encode())
                return

            event = json.loads(raw_body)
            print(f"Received {event['type']} — {event['id']}")
            self.send_response(200)
            self.end_headers()
            self.wfile.write(b"ok")

        def _verify(self, raw_body, sig_header, ts_header):
            ts = datetime.fromisoformat(ts_header)
            now = datetime.now(timezone.utc)

            if abs((now - ts).total_seconds()) > TOLERANCE:
                raise ValueError("Timestamp outside tolerance")

            expected = hmac.new(
                SECRET, f"{ts_header}.{raw_body}".encode(), hashlib.sha256
            ).hexdigest()

            if not hmac.compare_digest(sig_header, expected):
                raise ValueError("Invalid signature")

    HTTPServer(("", 4000), Handler).serve_forever()
    ```
  </Tab>

  <Tab title="Go" icon="code">
    ```go theme={null}
    package main

    import (
    	"crypto/hmac"
    	"crypto/sha256"
    	"encoding/hex"
    	"encoding/json"
    	"fmt"
    	"io"
    	"math"
    	"net/http"
    	"os"
    	"time"
    )

    const tolerance = 300 // seconds

    func verify(body []byte, sigHeader, tsHeader, secret string) error {
    	ts, err := time.Parse(time.RFC3339, tsHeader)
    	if err != nil {
    		return fmt.Errorf("bad timestamp: %w", err)
    	}
    	if math.Abs(time.Since(ts).Seconds()) > tolerance {
    		return fmt.Errorf("timestamp outside tolerance")
    	}

    	mac := hmac.New(sha256.New, []byte(secret))
    	fmt.Fprintf(mac, "%s.%s", tsHeader, body)
    	expected := hex.EncodeToString(mac.Sum(nil))

    	if !hmac.Equal([]byte(sigHeader), []byte(expected)) {
    		return fmt.Errorf("invalid signature")
    	}

    	return nil
    }

    func main() {
    	secret := os.Getenv("WEBHOOK_SECRET")

    	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    		body, _ := io.ReadAll(r.Body)
    		if err := verify(
    			body,
    			r.Header.Get("X-Paxos-Labs-Signature"),
    			r.Header.Get("X-Paxos-Labs-Timestamp"),
    			secret,
    		); err != nil {
    			http.Error(w, err.Error(), 401)
    			return
    		}
    		var evt map[string]any
    		json.Unmarshal(body, &evt)
    		fmt.Printf("Received %s — %s\n", evt["type"], evt["id"])
    		w.WriteHeader(200)
    	})

    	fmt.Println("Listening on :4000")
    	http.ListenAndServe(":4000", nil)
    }
    ```
  </Tab>
</Tabs>

## Step 3: Expose Your Local Server

For local development, use a tunnel so Paxos Labs can reach your machine:

```bash theme={null}
ngrok http 4000
```

Copy the generated `https://` URL and use it as your webhook endpoint URL in the Paxos Labs Console.

## Step 4: Send a Test Event

Back in the Paxos Labs Console, expand your endpoint row and click **Send Test**. Choose **ping** to fire a lightweight connectivity check.

Your server should log output like:

```
Received ping — evt_test_a1b2c3d4-e5f6-7890-abcd-ef1234567890
```

If the delivery succeeds, the endpoint's delivery history shows a **Success** badge with the HTTP status code. You're ready to subscribe to real events.

## Step 5: Subscribe to Events

Edit your endpoint to add event types for the ones you care about. When those events occur in your organization, your server will receive them automatically.

## Next Steps

<CardGroup cols={2}>
  <Card title="Signature Verification" icon="shield-check" href="/v1.0.0/intro/webhooks/common-implementation-patterns#signature-verification">
    Deep dive into the HMAC-SHA256 signature scheme, replay protection, and production best practices.
  </Card>

  <Card title="Event Catalog" icon="list" href="/v1.0.0/intro/webhooks/event-catalog">
    Full reference of every event type with payload schemas and field descriptions.
  </Card>
</CardGroup>
