> For the complete documentation index, see [llms.txt](https://docs.shortcutco.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.shortcutco.com/developers/webhooks/local-development.md).

# Local development

Receive Shortcut webhook events on your own computer while you build, through an HTTPS tunnel, and test them with test events and real changes.

After this page you can receive real, signed Shortcut events in a receiver running on your own computer, and test it end to end before you deploy.

Shortcut only sends events to a public `https` URL on port 443, and refuses `localhost` and private addresses (see [Limits and URL rules](/developers/webhooks/overview.md#limits-and-url-rules)). So while you build, put an HTTPS tunnel in front of your receiver: a service that gives you a public `https` address and forwards each request to a port on your computer. It takes about 10 minutes.

## 1. Run your receiver

Start a receiver on your computer, for example the Express server from [Verify signatures](/developers/webhooks/verify-signatures.md#node), which listens on port 3000 and accepts events at `/webhooks/shortcut`. Set `SHORTCUT_WEBHOOK_SECRET` to any placeholder value for now, so the receiver starts and answers `400` to unsigned requests; you get the real secret in step 3.

## 2. Start a tunnel

Any tunnel that gives you a public `https` URL on the standard port works, such as ngrok or Cloudflare Tunnel:

```bash
ngrok http 3000
# or
cloudflared tunnel --url http://localhost:3000
```

Each prints a public address such as `https://quiet-meadow-4821.example-tunnel.dev`. Keep the tunnel running while you test.

## 3. Create a development webhook

Create a webhook pointed at the tunnel, with the events you are building for. Use a token with `webhooks:manage` and the read scope of each family, here `leads:read`:

```bash
curl https://api.shortcutco.com/v1/webhooks \
  -X POST \
  -H "Authorization: Bearer $SHORTCUT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://quiet-meadow-4821.example-tunnel.dev/webhooks/shortcut",
    "description": "Development: laptop tunnel",
    "eventTypes": ["lead.created", "lead.updated"]
  }'
```

Copy `secret` from the response into `SHORTCUT_WEBHOOK_SECRET` and restart your receiver. Note the webhook's `id`.

A company admin can create the same webhook in **Settings → API & integrations** instead.

## 4. Send a test event

```bash
curl https://api.shortcutco.com/v1/webhooks/7d2f4b8e-1a3c-4e5f-9b6d-0c8a2e4f6b1d/test \
  -X POST \
  -H "Authorization: Bearer $SHORTCUT_TOKEN"
```

Shortcut sends a signed [`webhook.test` event](/developers/webhooks/events.md#webhooktest) at once and answers with the delivery, including what your receiver answered:

```json
{
  "data": {
    "id": "3a5c7e9b-1d2f-4a6c-8e0b-2d4f6a8c0e1b",
    "object": "webhook_delivery",
    "webhookId": "7d2f4b8e-1a3c-4e5f-9b6d-0c8a2e4f6b1d",
    "eventId": "evt_3f9a1c7e5b2d4f6a8c0e1b3d5f7a9c2e",
    "eventType": "webhook.test",
    "status": "exhausted",
    "attempts": 1,
    "nextAttemptAt": null,
    "deliveredAt": null,
    "lastResponseStatus": 400,
    "lastError": "http_400",
    "lastResponseExcerpt": "Invalid signature",
    "createdAt": "2026-10-17T15:04:05.000Z"
  }
}
```

This one failed: the receiver refused the signature, so `status` is `exhausted` (test events are never retried) and `lastResponseExcerpt` shows the receiver's answer. Check the secret and that nothing parses the body before the verifier runs ([When verification fails](/developers/webhooks/verify-signatures.md#when-verification-fails)), then send another. When your receiver answers `2xx`, `status` is `succeeded`.

`lastError` values such as unresolvable or connect\_timeout mean the request never reached your receiver: check that the tunnel is still running and that the webhook's URL matches it. See [Retries and monitoring](/developers/webhooks/retries-and-monitoring.md#see-what-was-sent) for every value.

## 5. Make a real change

Now cause the event you subscribed to. Create a lead, in the app or with the API (this needs `leads:write`):

```bash
curl https://api.shortcutco.com/v1/leads \
  -X POST \
  -H "Authorization: Bearer $SHORTCUT_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: webhook-dev-test-lead-1" \
  -d '{
    "firstName": "Webhook",
    "lastName": "Test",
    "email": "webhook.test@example.com",
    "address": "88 Willow Way, Bozeman, MT 59718"
  }'
```

Within about a minute your receiver gets a `lead.created` event with the lead in `data.object`. Edit the lead in the app and a `lead.updated` follows. Dismiss the test lead when you are done.

## 6. Clean up

* **When the tunnel's address changes**, as it does each time some tunnels restart, point the webhook at the new one with `PATCH /v1/webhooks/{id}` and `{ "url": "..." }`. The new URL is checked like the first.
* **When you stop for the day**, pause the webhook (`"status": "paused"`) so events wait for you instead of failing. A paused webhook keeps events for up to 30 days and sends them when you resume it.
* **When you are done**, delete the development webhook with `DELETE /v1/webhooks/{id}`. It counts toward the workspace's 20 webhooks, and an endpoint that stays down fails attempts until Shortcut turns the webhook off and emails your company admins.

## Tips for the real receiver

* **Answer fast.** Verify, store, and answer `200`; do the work from a queue. Shortcut waits 15 seconds at most, and slow answers come back as retries.
* **Expect duplicates and any order.** Skip an envelope `id` you have handled, and keep the newer copy of a record by `updatedAt`.
* **Log the `Shortcut-Delivery-Id`.** It is the delivery's `id` in the [deliveries list](/developers/webhooks/retries-and-monitoring.md#see-what-was-sent), so you can match your logs to what Shortcut recorded.
* **Use the deliveries list to debug,** and [retry a delivery](/developers/webhooks/retries-and-monitoring.md#retry-a-delivery) once your fix is deployed instead of waiting for the next change.
* **Keep the production webhook separate** from development ones, with its own secret, so rotating or deleting one never touches the other.
