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

# Test Webhooks Locally

> Receive real Lamina execution callbacks on your machine, verify signatures, and reuse the same flow from the CLI or an agent.

## Why This Matters

Most production Lamina integrations should use webhooks instead of tight polling loops.

The local CLI can now do the same thing:

* start a local webhook receiver
* verify Lamina's ED25519 signature
* expose the listener through ngrok or another public tunnel
* run a real app against that callback URL

## Lamina Webhook Contract

When you start an execution with:

```http theme={null}
POST /v1/apps/{appId}/runs?webhook=https://your-server.com/callback
```

Lamina sends a signed POST when the execution finishes.

Headers:

* `X-Lamina-Webhook-Signature`
* `X-Lamina-Webhook-Timestamp`
* `X-Lamina-Webhook-Request-Id`

Message to verify:

```text theme={null}
<timestamp>.<raw_body>
```

Verification key source:

```http theme={null}
GET /v1/webhooks/signing-key
```

The CLI listener and MCP listener both verify this contract directly.

## Start A Local Listener

From the repository root:

```bash theme={null}
lamina webhook listen --port 8788
```

That starts a local receiver at:

```text theme={null}
http://127.0.0.1:8788/lamina/webhook
```

## Expose It Publicly

Use ngrok, cloudflared, or your preferred tunnel. Example with ngrok:

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

If the public URL is `https://example.ngrok.dev`, save it as the default Lamina webhook URL:

```bash theme={null}
lamina webhook listen \
  --port 8788 \
  --public-url https://example.ngrok.dev \
  --save-default
```

The CLI normalizes that to:

```text theme={null}
https://example.ngrok.dev/lamina/webhook
```

Check what is saved:

```bash theme={null}
lamina webhook status
```

Clear it if needed:

```bash theme={null}
lamina webhook clear
```

## Run An App Against The Saved Webhook URL

```bash theme={null}
lamina run <appId> --file inputs.json --webhook default
```

You can also pass the public URL directly:

```bash theme={null}
lamina run <appId> \
  --file inputs.json \
  --webhook https://example.ngrok.dev/lamina/webhook
```

## Expected Listener Output

For a successful callback, the listener prints a verified execution message like:

```text theme={null}
Verified webhook 1 for execution 99ebc7c6-24f3-4fc2-9d53-2e3f49f3bec1 (completed)
```

If verification fails, the listener rejects the callback and prints the verification error instead.

## Real End-To-End Flow

<Steps>
  <Step title="Authenticate the CLI">
    Run `lamina login` (browser OAuth) or `lamina login --api-key lma_...` for CI.
  </Step>

  <Step title="Start the local listener">
    Run `lamina webhook listen --port 8788`.
  </Step>

  <Step title="Expose the listener">
    Start ngrok or another tunnel and get a public HTTPS URL.
  </Step>

  <Step title="Save the public webhook URL">
    Start the listener with `--public-url ... --save-default`, or pass the full webhook URL directly when you run the app.
  </Step>

  <Step title="Run the app">
    Execute `lamina run <appId> --file inputs.json --webhook default`.
  </Step>

  <Step title="Receive and verify the callback">
    The local listener validates the Lamina signature and prints the completed execution.
  </Step>
</Steps>

## MCP Uses Polling Instead

The hosted and local MCP servers intentionally expose only five high-level creative tools. They do not expose webhook listener management tools.

For MCP clients, start work with `lamina_create` and retrieve results with `lamina_status`. Use REST webhooks when you are building a server-side integration that owns a public callback URL.

## Fallback

If you do not want to expose a callback during local development, omit `?webhook=` and use polling with:

```bash theme={null}
lamina run <appId> --file inputs.json --wait
```
