# Getting Started (/docs/developer-resources/guides/webhook/getting-started)

This page walks through setting up your first webhook, from creating an endpoint to verifying that events arrive correctly. By the end, you will have a working webhook receiving events from Monime.

## What you need before you start

Webhooks require an endpoint that Monime can reach over the public internet. Before you create a webhook in the dashboard, confirm that you have the following:

**An HTTPS endpoint.** Monime sends webhook deliveries only over HTTPS because payloads may contain sensitive data. The endpoint URL must start with `https://`. Plain HTTP endpoints are rejected.

**A publicly routable domain name.** The URL must use a domain name, not an IP address. URLs like `https://192.168.1.1/webhooks` or `https://[::1]/webhooks` are rejected. If you are testing locally, you need a tunnel service (see the local testing section below).

**POST request handling.** Your endpoint must accept HTTP POST requests. Monime sends event payloads in the request body as JSON.

**Timely responses.** Your endpoint must respond within Monime's timeout window. If processing takes longer than a few seconds, return a success response immediately and process the event asynchronously.

**Access to the Developer section of your Space.** Only users with developer or admin access can create webhooks. If you do not see the Developer menu item, contact your Space administrator.

## Create a minimal endpoint

Start with a simple endpoint that accepts POST requests and returns a 200 response. This lets you confirm that Monime can reach your server before you add verification and business logic.

The following example uses Node.js with Express:

```javascript
const express = require('express');
const app = express();

app.post('/webhooks/monime', express.raw({ type: 'application/json' }), (req, res) => {
  console.log('Received webhook:', req.body.toString());
  res.status(200).send('OK');
});

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});
```

Notice that the endpoint uses `express.raw()` middleware. This preserves the raw request body, which you will need later for signature verification. If you parse the body as JSON before verification, re-serializing it may produce different bytes, causing verification to fail.

The following example uses Python with Flask:

```python
from flask import Flask, request

app = Flask(__name__)

@app.route('/webhooks/monime', methods=['POST'])
def webhook():
    raw_body = request.get_data()
    print('Received webhook:', raw_body.decode())
    return '', 200

if __name__ == '__main__':
    app.run(port=3000)
```

Flask's `request.get_data()` returns the raw body bytes, which you preserve for signature verification.

## Expose your endpoint to the internet

If your server runs on a cloud provider with a public IP and domain, you can skip this section. If you are developing locally, Monime cannot reach `localhost` because that address refers to Monime's own servers, not yours.

Use a tunnel service to expose your local server. These services create a public URL that forwards requests to your local machine.

**ngrok** is a popular choice:

```bash
ngrok http 3000
```

After running this command, ngrok displays a public URL like `https://abc123.ngrok.io`. Use this URL as your webhook endpoint. Requests to `https://abc123.ngrok.io/webhooks/monime` are forwarded to `http://localhost:3000/webhooks/monime` on your machine.

**localtunnel** is an open-source alternative:

```bash
npx localtunnel --port 3000
```

Localtunnel provides a public URL in a similar fashion.

Keep the tunnel running while testing. When you stop the tunnel, the public URL becomes unreachable and webhook deliveries fail.

## Create the webhook in the dashboard

With your endpoint running and reachable, create the webhook:

1. **Open the Webhooks page**

   Navigate to Developer in your Space sidebar, then select Webhooks.

2. **Click Create webhook**

   The create dialog opens with configuration fields.

3. **Enter a name**

   Choose a name that identifies this webhook. For example, "Local Development Test" or "Production Payment Handler."

4. **Enter the endpoint URL**

   Paste the full URL to your endpoint, including the path. For example, `https://abc123.ngrok.io/webhooks/monime`.

5. **Select a verification method**

   Choose HMAC-SHA256 for now. Copy the generated secret and save it; you will need it to verify signatures.

6. **Add alert emails (optional)**

   Add up to two email addresses for delivery-failure notifications.

7. **Select an API release and events**

   Choose the latest API release. Select at least one event to subscribe to,
   such as `payment.processing_completed` or `checkout_session.completed`.

8. **Click Create**

   The webhook is created and enabled. Monime begins sending events immediately.

## Trigger a test event

Create activity in your Space that matches one of the subscribed events. For
example, if you subscribed to `payment.processing_completed`, initiate a test
payment and complete it.

After the event occurs, return to the webhook details page in the dashboard. The Event Deliveries table shows a new row for the delivery. Click the row to see the full payload that Monime sent.

Your server logs should show the received webhook. If you see the payload in the logs and the dashboard shows status Success, your webhook is working.

## Add signature verification

A working webhook should verify signatures before processing payloads. Without verification, anyone who discovers your endpoint URL can send forged requests.

For an HMAC-SHA256 webhook, verify the `monime-signature` header before
processing the event. The header contains a Unix timestamp (`t`) and a base64
signature (`v1`). Monime signs the timestamp, an underscore, and the raw request
body.

```javascript
const crypto = require('crypto');
const express = require('express');

const app = express();
const secret = process.env.WEBHOOK_SECRET; // The secret you copied from the dashboard

function parseSignatureHeader(header) {
  return Object.fromEntries(
    header.split(',').map((part) => {
      const [key, ...value] = part.split('=');
      return [key.trim(), value.join('=')];
    }),
  );
}

app.post('/webhooks/monime', express.raw({ type: 'application/json' }), (req, res) => {
  const signatureHeader = req.headers['monime-signature'];
  const rawBody = req.body;

  if (!signatureHeader) return res.status(401).send('Missing signature');

  const { t, v1 } = parseSignatureHeader(signatureHeader);
  if (!t || !v1) return res.status(401).send('Malformed signature');

  if (!/^\d+$/.test(t)) return res.status(401).send('Malformed timestamp');

  const timestamp = Number(t);
  const now = Math.floor(Date.now() / 1000);
  if (now - timestamp > 300 || timestamp - now > 60) {
    return res.status(401).send('Timestamp out of tolerance');
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}_`)
    .update(rawBody)
    .digest();

  const received = Buffer.from(v1, 'base64');

  if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
    return res.status(401).send('Invalid signature');
  }

  const payload = JSON.parse(rawBody.toString());
  console.log('Verified webhook:', payload.event.name, payload.event.id);

  // Process the event here

  res.status(200).send('OK');
});

app.listen(3000);
```

Key points:

Decode `v1` from base64 and use `crypto.timingSafeEqual()` only after checking
that both byte arrays have the same length.

Compute the signature over `${t}_` followed by the raw request bytes, not a
parsed and re-serialized JSON object. Parsing can reorder keys or change
whitespace, producing a different signature.

Return 401 if verification fails. This tells Monime the request was rejected and prevents processing of forged events.

## Handle idempotency

Monime may deliver the same event multiple times due to retries, network glitches, or manual replays. Your handler must be idempotent: processing the same event twice should not cause incorrect behavior.

The standard approach is to track event IDs. When you receive an event, check whether you have already processed an event with that ID. If so, return success without re-processing.

```javascript
const processedEvents = new Set(); // In production, use a database with TTL

app.post('/webhooks/monime', express.raw({ type: 'application/json' }), (req, res) => {
  // ... signature verification ...

  const payload = JSON.parse(rawBody.toString());
  const eventId = payload.event.id;

  if (processedEvents.has(eventId)) {
    console.log('Duplicate event, skipping:', eventId);
    return res.status(200).send('OK');
  }

  // Process the event
  handleEvent(payload);

  processedEvents.add(eventId);
  res.status(200).send('OK');
});
```

In production, store processed event IDs in a database with a time-to-live. The TTL should exceed Monime's retry window so that duplicates within that window are caught, but old entries are eventually cleaned up.

## Common mistakes

**Using HTTP instead of HTTPS.** The dashboard rejects URLs that do not start with `https://`. Double-check the protocol.

**Using localhost or an IP address.** Monime cannot reach these endpoints. Use a domain name or a tunnel for local development.

**Parsing the body before verification.** If you use `express.json()` middleware before computing the signature, the raw bytes are lost. Use `express.raw()` and parse manually after verification.

**Returning errors for events you do not care about.** If you subscribe to events your handler does not need, return 200 anyway. Returning an error triggers retries for events you intended to ignore.

**Slow handlers that cause timeouts.** If your handler does heavy work before responding, Monime may time out and retry. Return 200 quickly and process asynchronously if needed.

## Environment-specific webhooks

Create separate webhooks for each environment:

For **development**, use a tunnel URL pointing to your local machine. Enable only the events you are actively testing. Use a test secret that differs from production.

For **staging**, use your staging server's URL. Mirror the same event subscriptions as production so you can test realistic scenarios.

For **production**, use your production server's URL. Protect the secret carefully and monitor delivery health.

Separating environments prevents test events from polluting production systems and vice versa.

## Next steps

With your first webhook working, explore these topics:

- [Creating webhooks](/docs/developer-resources/guides/webhook/creating-webhooks) explains each configuration field in detail.
- [Verification and security](/docs/developer-resources/guides/webhook/hmac-verification) covers ECDSA P-256 and advanced security practices.
- [Pre-launch checklist](/docs/developer-resources/guides/webhook/pre-launch-checklist) helps you validate your setup before going live.
