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

# Verifying Signatures

> Check that a webhook really comes from meetergo

Every webhook meetergo sends is signed with your company's signing secret, following the [Standard Webhooks](https://www.standardwebhooks.com) specification. Verify the signature before you trust a payload.

One secret covers all of your company's webhooks: registered webhook endpoints, form webhook URLs, workflow webhook actions and e-signature completion webhooks.

<Note>
  Signing only adds headers. The request body and `Content-Type` are unchanged, so existing receivers keep working without changes.
</Note>

## Headers

| Header              | Content                                                                                                                               |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook-id`        | Unique id of the delivery, for example `msg_2f1c...`. It stays the same when the same delivery is retried. Use it to drop duplicates. |
| `webhook-timestamp` | Unix time in seconds when the request was sent                                                                                        |
| `webhook-signature` | One or more signatures separated by a space, each in the form `v1,<base64>`                                                           |

## Signed Content

The signature is an HMAC-SHA256 over this string:

```
{webhook-id}.{webhook-timestamp}.{raw request body}
```

* The HMAC key is the base64-decoded part of your secret after the `whsec_` prefix.
* The result is base64 encoded and prefixed with `v1,`.
* Use the **raw** request body exactly as received. Parsing and re-serializing the JSON changes the bytes and breaks the signature.

## Get Your Signing Secret

Open [Integrations & Apps](https://my.meetergo.com/integrations) in the dashboard, click the **Webhooks** tile and reveal the signing secret, or fetch it with an API key:

```bash theme={null}
curl -X GET "https://api.meetergo.com/webhooks/signing-secret" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-meetergo-api-user-id: {userId}"
```

```json theme={null}
{
  "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw",
  "previousSecretExpiresAt": null
}
```

Revealing or rotating the secret requires a company admin or an API key. Store it like a password.

## Verify in Node.js

```javascript theme={null}
const crypto = require('node:crypto');
const express = require('express');

const SECRET = process.env.MEETERGO_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 5 * 60;

function verifyMeetergoWebhook(rawBody, headers) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];
  if (!id || !timestamp || !signatures) return false;

  // Replay protection: reject anything older or newer than 5 minutes.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;

  const key = Buffer.from(SECRET.slice('whsec_'.length), 'base64');
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest();

  // Several signatures are sent while a rotated secret is still valid.
  return signatures.split(' ').some((entry) => {
    const [version, signature] = entry.split(',');
    if (version !== 'v1' || !signature) return false;
    const actual = Buffer.from(signature, 'base64');
    return (
      actual.length === expected.length &&
      crypto.timingSafeEqual(actual, expected)
    );
  });
}

const app = express();

// express.raw keeps the body as the exact bytes meetergo signed.
app.post(
  '/webhooks/meetergo',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const rawBody = req.body.toString('utf8');
    if (!verifyMeetergoWebhook(rawBody, req.headers)) {
      return res.status(401).send('Invalid signature');
    }

    const payload = JSON.parse(rawBody);
    console.log('Verified event:', payload.webhookType);
    res.status(200).send('OK');
  },
);

app.listen(3000);
```

Any [Standard Webhooks library](https://www.standardwebhooks.com/#resources) works as well. Pass it the secret, the raw body and the three headers.

## Replay Protection

Reject requests whose `webhook-timestamp` is more than 5 minutes away from your server time, as the sample does. Store recent `webhook-id` values if you also need to reject a replay inside that window.

## Rotating the Secret

Rotate the secret in the dashboard or with the API:

```bash theme={null}
curl -X POST "https://api.meetergo.com/webhooks/signing-secret/rotate" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-meetergo-api-user-id: {userId}"
```

The response contains the new secret and `previousSecretExpiresAt`. For 24 hours every delivery carries two signatures, one per secret, so a receiver on either secret accepts it. Deploy the new secret within that time. After 24 hours only the new secret signs.

## Troubleshooting

| Symptom                 | Likely cause                                                                                                                                                    |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Signature never matches | The body was parsed before verification. Verify against the raw bytes.                                                                                          |
| Signature never matches | The whole secret was used as the key. Decode the part after `whsec_` from base64.                                                                               |
| Valid requests rejected | Server clock is off by more than 5 minutes                                                                                                                      |
| No signature headers    | Rare. If the secret cannot be loaded, meetergo still delivers the event without a signature. Decide whether your receiver rejects or accepts unsigned requests. |
