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

# Testing locally

> Receive Runcrate events on your own machine through an HTTPS tunnel, and send test events on demand.

Runcrate posts from the internet, so it cannot reach `localhost` on your laptop. To build and test a receiver, run it locally, expose it through an HTTPS tunnel, and point an endpoint at the tunnel's public URL.

## Addresses Runcrate will not post to

An endpoint URL must start with `https://` and lead to a public address. These are refused when you add the endpoint, and checked again on every delivery:

* `localhost`, and names ending in `.local`, `.internal`, `.home`, `.lan` or `.localdomain`, or with no dot at all.
* Private, loopback and link-local IP addresses: `10.x`, `172.16.x` to `172.31.x`, `192.168.x`, `127.x`, `169.254.x`, `100.64.x` to `100.127.x`, `::1`, `fc00::/7`, `fe80::/10` and similar.
* Any host name that resolves to one of those addresses.
* A URL with a user name or password in it. Put a secret in the path instead.

The sentence you see says why, for example "This address is only reachable inside your own network. Runcrate posts from the internet."

## 1. Run a receiver

This receiver has no dependencies. It checks the signature, prints the event and answers `200`. Save it as `receiver.mjs`:

```javascript receiver.mjs theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
import crypto from 'node:crypto';
import http from 'node:http';

const SECRET = process.env.RUNCRATE_WEBHOOK_SECRET; // whsec_...

function verify(rawBody, header) {
  const parts = (header ?? '').split(',').map(item => item.trim().split('='));
  const t = parts.find(([key]) => key === 't')?.[1];
  const signatures = parts.filter(([key, value]) => key === 'v1' && /^[0-9a-f]{64}$/.test(value ?? '')).map(([, value]) => value);
  if (!/^\d+$/.test(t ?? '') || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac('sha256', SECRET).update(`${t}.`).update(rawBody).digest();
  return signatures.some(sig => crypto.timingSafeEqual(expected, Buffer.from(sig, 'hex')));
}

http
  .createServer((req, res) => {
    const chunks = [];
    req.on('data', chunk => chunks.push(chunk));
    req.on('end', () => {
      const rawBody = Buffer.concat(chunks);
      if (!SECRET || !verify(rawBody, req.headers['runcrate-signature'])) {
        res.writeHead(400).end('bad signature');
        console.log('Rejected a request with a bad signature');
        return;
      }
      res.writeHead(200).end();
      const event = JSON.parse(rawBody.toString('utf8'));
      console.log(req.headers['runcrate-event'], req.headers['runcrate-delivery'], JSON.stringify(event.data, null, 2));
    });
  })
  .listen(3000, () => console.log('Listening on http://localhost:3000'));
```

You get the secret when you add the endpoint in step 3. Start the receiver after that:

```bash theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
RUNCRATE_WEBHOOK_SECRET=whsec_your_secret node receiver.mjs
```

To build a full handler, use one of the verifiers in [verifying signatures](/docs/webhooks/verifying-signatures) instead.

## 2. Open a tunnel

Any tool that gives `localhost:3000` a public `https://` URL works. Two common ones:

```bash ngrok theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
ngrok http 3000
# Forwarding  https://3f2a-198-51-100-7.ngrok-free.app -> http://localhost:3000
```

```bash Cloudflare Tunnel theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
cloudflared tunnel --url http://localhost:3000
# Your quick Tunnel has been created! Visit it at: https://example-words.trycloudflare.com
```

Copy the `https://` URL it prints. Free tunnel URLs usually change each time you start the tool, so update the endpoint when it does.

## 3. Add an endpoint and send a test event

**1. Add the endpoint**

In [Settings > Webhooks](https://runcrate.ai/dashboard/settings/webhooks), select **Add endpoint**, choose **Webhook** and paste the tunnel URL with your path, for example `https://3f2a-198-51-100-7.ngrok-free.app/runcrate/webhooks`.

**2. Copy the secret**

Copy the signing secret shown once, then start the receiver with it (step 1).

**3. Send a test event**

Select **Send a test event**. Your terminal prints `webhook.test`, and Runcrate shows that it was delivered.

**Send a test event** is in each endpoint's menu too, so you can send one whenever you change your receiver. It sends a [`webhook.test`](/docs/webhooks/events) event to that endpoint only, whatever events it subscribes to. Nothing happens in your workspace.

To see a real event type, trigger it: launch a small instance and terminate it to get `instance.ready` and `instance.stopped`. Or replay one from the [delivery log](/docs/webhooks/retries#the-delivery-log) with **Redeliver**. To post an event of any type without Runcrate, sign one by hand with the [openssl recipe](/docs/webhooks/verifying-signatures#compute-a-signature-by-hand).

## When the test fails

The result, and the delivery log, say what happened in one sentence. The usual ones while testing:

| What you see | What to do |
| - | - |
| Could not connect to this address. | The tunnel is not running, or the URL is from an earlier session. Start it and update the URL. |
| No answer within 10 seconds. | The receiver is not running on the port the tunnel forwards to, or it does work before answering. |
| The server behind this URL refused the message (it answered 400). | Usually a bad signature: the receiver has a different secret than this endpoint. |
| The address answered with a redirect (302). | Use the exact URL, including the path, that your receiver handles. Runcrate does not follow redirects. |

## Related

> **[Verifying signatures](/docs/webhooks/verifying-signatures)**
>
> Node.js, Python and Go verifiers.

> **[Retries and the delivery log](/docs/webhooks/retries)**
>
> What each attempt and failure looks like.


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