Webhook Notifications

December 4, 2025 ยท View on GitHub

Hemmelig can send HTTP POST requests to your webhook URL when secrets are viewed or burned. This allows you to integrate with external services like Slack, Discord, monitoring systems, or custom applications.

Configuration

Configure webhooks in the admin dashboard under Instance Settings โ†’ Webhooks.

SettingDescription
Enable WebhooksTurn webhook notifications on/off
Webhook URLThe endpoint where payloads are sent
Webhook SecretSecret key for HMAC-SHA256 payload signing
Secret ViewedSend webhook when a secret is viewed
Secret BurnedSend webhook when a secret is deleted

Webhook Payload

Webhooks are sent as HTTP POST requests with a JSON body:

Secret Events

{
    "event": "secret.viewed",
    "timestamp": "2024-12-04T10:30:00.000Z",
    "data": {
        "secretId": "abc123-def456",
        "hasPassword": true,
        "hasIpRestriction": false,
        "viewsRemaining": 2
    }
}

API Key Events

{
    "event": "apikey.created",
    "timestamp": "2024-12-04T10:30:00.000Z",
    "data": {
        "apiKeyId": "key-uuid-here",
        "name": "My Integration",
        "expiresAt": "2025-12-04T10:30:00.000Z",
        "userId": "user-uuid-here"
    }
}

Event Types

EventDescription
secret.viewedA secret was successfully viewed
secret.burnedA secret was deleted (manually or after last view)
apikey.createdA new API key was created

Headers

HeaderDescription
Content-Typeapplication/json
X-Hemmelig-EventEvent type (secret.viewed, secret.burned, or apikey.created)
X-Hemmelig-SignatureHMAC-SHA256 signature (if secret configured)

Verifying Webhook Signatures

If you configure a webhook secret, Hemmelig signs each payload using HMAC-SHA256. The signature is sent in the X-Hemmelig-Signature header as sha256=<hex>.

Always verify signatures to ensure webhooks are authentic and haven't been tampered with.

Node.js Example

const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
    const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');

    return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// Express.js middleware
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-hemmelig-signature'];
    const payload = req.body.toString();

    if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(payload);
    console.log(`Received ${event.event} for secret ${event.data.secretId}`);

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

Python Example

import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected)

# Flask example
@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Hemmelig-Signature')
    payload = request.get_data()

    if not verify_webhook(payload, signature, os.environ['WEBHOOK_SECRET']):
        return 'Invalid signature', 401

    event = request.get_json()
    print(f"Received {event['event']} for secret {event['data']['secretId']}")

    return 'OK', 200

Go Example

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "io"
    "net/http"
)

func verifyWebhook(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(signature), []byte(expected))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Hemmelig-Signature")
    payload, _ := io.ReadAll(r.Body)

    if !verifyWebhook(payload, signature, os.Getenv("WEBHOOK_SECRET")) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    // Process webhook...
    w.WriteHeader(http.StatusOK)
}

Integration Examples

Slack Notification

Send a message to Slack when a secret is viewed:

app.post('/webhook', async (req, res) => {
    const event = req.body;

    if (event.event === 'secret.viewed') {
        await fetch(process.env.SLACK_WEBHOOK_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                text: `๐Ÿ”“ Secret ${event.data.secretId} was viewed. ${event.data.viewsRemaining} views remaining.`,
            }),
        });
    }

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

Discord Notification

if (event.event === 'secret.burned') {
    await fetch(process.env.DISCORD_WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            embeds: [
                {
                    title: '๐Ÿ”ฅ Secret Burned',
                    description: `Secret \`${event.data.secretId}\` has been permanently deleted.`,
                    color: 0xff6b6b,
                    timestamp: event.timestamp,
                },
            ],
        }),
    });
}

Best Practices

  1. Always use HTTPS for your webhook endpoint
  2. Always verify signatures to prevent spoofed requests
  3. Respond quickly (< 5 seconds) to avoid timeouts
  4. Use a queue for heavy processing to avoid blocking
  5. Log webhook events for debugging and audit trails
  6. Handle retries gracefully (webhooks are fire-and-forget, no retries)

Troubleshooting

Webhooks not being received?

  • Check that webhooks are enabled in Instance Settings
  • Verify your webhook URL is accessible from the Hemmelig server
  • Check server logs for any error messages

Invalid signature errors?

  • Ensure you're using the raw request body (not parsed JSON) for verification
  • Check that your webhook secret matches exactly
  • Make sure you're comparing the full signature including the sha256= prefix