Webhook Authentication
Securing your webhook endpoint ensures that you only act on requests that really come from Beedeez. This guide explains the authentication methods available for webhook endpoints.
Why Authentication Matters
Beedeez does not sign webhook requests: no signature is computed and no signature header is sent. The credential you configure on the endpoint is therefore the only proof of origin you get. Without it, anyone who knows your URL can send you:
- Spoofed certificate awards that appear to come from Beedeez
- Malicious or malformed payloads
- Floods of requests
Always configure an authentication method for a production endpoint, and treat the URL itself as a secret.
Authentication Methods
The back office offers five authentication methods. Each one adds at most one header to the request.
1. None (Not Recommended)
No authentication header is sent.
Configuration:
{
"auth": {
"authType": "none"
}
}
When to Use:
- Local testing only
Never use this method in production.
2. Bearer Token
Sends an Authorization header with a bearer token.
Configuration:
{
"auth": {
"authType": "bearer_token",
"token": "your-secret-bearer-token"
}
}
Request Header:
Authorization: Bearer your-secret-bearer-token
When to Use:
- REST APIs and gateways that already expect bearer tokens
Best Practices:
- Use long, randomly generated tokens (at least 32 characters)
- Rotate tokens periodically
- Store tokens in a secret manager, never in source code
- Use different tokens for different environments
Example Token Generation (Node.js):
const crypto = require('crypto');
const token = crypto.randomBytes(32).toString('hex');
If the token field is left empty, the request is sent without an Authorization header.
3. Basic Authentication
Sends credentials using HTTP Basic Authentication.
Configuration:
{
"auth": {
"authType": "basic_auth",
"username": "your-username",
"password": "your-secure-password"
}
}
Request Header:
Authorization: Basic eW91ci11c2VybmFtZTp5b3VyLXNlY3VyZS1wYXNzd29yZA==
(Base64 encoding of username:password)
When to Use:
- Systems with existing Basic Auth infrastructure
Best Practices:
- Use a strong, unique password for a dedicated service account
- Use an HTTPS endpoint, since Basic credentials are only encoded, not encrypted
- Rotate credentials regularly
If the username or the password is missing, the request is sent without an Authorization header.
4. API Key
Sends an API key in the X-API-Key header.
Configuration:
{
"auth": {
"authType": "api_key",
"token": "your-api-key"
}
}
Request Header:
X-API-Key: your-api-key
When to Use:
- API gateways and platforms that expect a key in
X-API-Key
Best Practices:
- Generate cryptographically secure keys
- Revoke and replace a key immediately if it leaks
If the key is left empty, the request is sent without an X-API-Key header.
5. Custom Header
Sends one header whose name and value you choose.
Configuration:
{
"auth": {
"authType": "custom_header",
"headerName": "X-Webhook-Secret",
"headerValue": "your-custom-auth-value"
}
}
Request Header:
X-Webhook-Secret: your-custom-auth-value
When to Use:
- Receivers that expect a shared secret in a specific header
Best Practices:
- Use a descriptive, non-standard header name (for example
X-Webhook-Secret) - Compare the value strictly on your endpoint
Only one name and value pair can be configured. If either is empty, no header is sent.
Headers Sent with Each Request
A webhook request carries only:
Content-Type: application/json- the header of your authentication method, if any
- custom headers, if Beedeez has set some on the endpoint (the back-office form has no field for them; see Configuration)
Authentication headers are applied last: a custom header named Authorization or X-API-Key is overwritten by the authentication method. Beedeez adds no source or signature header of its own.
Example with a bearer token:
POST /webhooks/beedeez HTTP/1.1
Content-Type: application/json
Authorization: Bearer your-secret-bearer-token
Validating Webhook Requests
On your webhook endpoint, validate every incoming request.
1. Verify Authentication
Check that the authentication header matches your expected credential, using a constant-time comparison:
// Example: Bearer Token validation (Node.js/Express)
const crypto = require('crypto');
function safeEqual(a, b) {
const bufA = Buffer.from(a || '');
const bufB = Buffer.from(b || '');
return bufA.length === bufB.length && crypto.timingSafeEqual(bufA, bufB);
}
app.post('/webhooks/beedeez', (req, res) => {
const expected = `Bearer ${process.env.WEBHOOK_BEARER_TOKEN}`;
if (!safeEqual(req.headers.authorization, expected)) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Process webhook...
res.status(200).json({ received: true });
});
2. Verify Content Type
if (!req.is('application/json')) {
return res.status(400).json({ error: 'Invalid content type' });
}
3. Validate Payload Structure
The body is the certificate award itself. Check the fields you rely on:
const award = req.body;
if (!award._id || !award._userId || !award._badgeId || !award._ownerId) {
return res.status(400).json({ error: 'Invalid payload structure' });
}
4. Verify the Account (Optional)
If several Beedeez accounts post to the same endpoint, check that _ownerId is one you expect:
const allowedOwnerIds = process.env.BEEDEEZ_OWNER_IDS.split(',');
if (!allowedOwnerIds.includes(award._ownerId)) {
return res.status(403).json({ error: 'Unknown account' });
}
A rejected request is not retried by Beedeez, so a misconfigured credential loses the notifications sent while it is wrong.
Security Best Practices
Use HTTPS
- Use an HTTPS endpoint with a valid TLS certificate (TLS 1.2 or higher). The back office does not enforce it, so check the URL you enter.
Implement Rate Limiting
Protect your endpoint from abuse, while leaving room for bursts: a catch-up pass can send one request per award in quick succession.
// Example: Simple rate limiting (Node.js)
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 1000,
});
app.post('/webhooks/beedeez', webhookLimiter, (req, res) => {
// Handle webhook...
});
A request rejected by your rate limiter is lost, since Beedeez does not retry.
Store Credentials Securely
- Use environment variables or a secret management system
- Never commit credentials to source control
- Use different credentials per environment
Monitor and Alert
- Log all webhook requests, including failed authentication
- Alert on authentication failures and unusual patterns
Rotate Credentials
- Rotate credentials periodically
- Update the credential in Beedeez and on your endpoint at the same time: deliveries rejected during the switch are not replayed. Accepting both the old and the new value for a short time avoids losing any.
IP Allowlisting
Beedeez does not restrict or publish the IP addresses webhooks are sent from, so you cannot rely on IP allowlisting to authenticate requests.
Troubleshooting Authentication
401 Unauthorized Errors
- Verify that the credential configured in Beedeez matches the one your endpoint expects
- Check that the authentication type is the one your endpoint validates
- Check that the credential is complete: an empty token, or Basic Auth without both username and password, sends no authentication header at all
- Check for typos and trailing spaces in tokens and passwords
403 Forbidden Errors
- Check firewall rules: Beedeez does not send from a fixed IP range
- Verify your custom validation logic
Testing Your Endpoint
Send a request with the same headers Beedeez uses:
curl -X POST https://api.example.com/webhooks/beedeez \
-H "Authorization: Bearer your-token" \
-H "Content-Type: application/json" \
-d '{"_id": "6712a9f4c3b5e10d2f8a4471", "_userId": "66e1b0447a2c9d0011f4e882", "_badgeId": "64c73f10ae5b220019d0a1c7", "_ownerId": "5f2c1ab8d9e4470012a3c001", "expiryReminderSent": false}'
Example Implementations
Node.js/Express with Bearer Token
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const EXPECTED_AUTH = `Bearer ${process.env.BEEDEEZ_WEBHOOK_TOKEN}`;
function safeEqual(a, b) {
const bufA = Buffer.from(a || '');
const bufB = Buffer.from(b || '');
return bufA.length === bufB.length && crypto.timingSafeEqual(bufA, bufB);
}
app.post('/webhooks/beedeez', (req, res) => {
if (!safeEqual(req.headers.authorization, EXPECTED_AUTH)) {
return res.status(401).json({ error: 'Unauthorized' });
}
const award = req.body;
// Acknowledge within 5 seconds
res.status(200).json({ received: true });
// Process asynchronously
processAwardAsync(award);
});
app.listen(3000);
Python/Flask with API Key
from flask import Flask, request, jsonify
import hmac
import os
app = Flask(__name__)
WEBHOOK_API_KEY = os.environ['BEEDEEZ_WEBHOOK_API_KEY']
@app.route('/webhooks/beedeez', methods=['POST'])
def handle_webhook():
api_key = request.headers.get('X-API-Key', '')
if not hmac.compare_digest(api_key, WEBHOOK_API_KEY):
return jsonify({'error': 'Unauthorized'}), 401
award = request.get_json()
print(f"Received award {award['_id']} (expiry: {award.get('expiryReminderSent')})")
# Acknowledge within 5 seconds, process afterwards
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
Next Steps
- Review the Configuration Guide to set up authentication
- See Payload Examples for the webhook body
- Read Event Types to understand what is emitted