Securing your webhook endpoints ensures that only Lettr can send events to your server. Lettr supports multiple authentication methods for outbound webhook requests, which you configure when creating a webhook in the dashboard.
Authentication Types
When creating or editing a webhook in the dashboard, you choose an authentication type that Lettr uses when making requests to your endpoint.
| Auth Type | Description |
|---|
none | No authentication. Lettr sends requests without credentials. |
basic | HTTP Basic Authentication. Lettr includes a username and password in each request. |
oauth2 | OAuth 2.0 Client Credentials. Lettr obtains a token and includes it in each request. |
Basic Authentication
With basic auth, Lettr includes an Authorization: Basic ... header in every webhook request. You provide the username and password when creating the webhook in the dashboard.
Verifying Basic Auth
Verify the credentials in your webhook handler:
import express from 'express';
const app = express();
app.post('/webhooks/lettr', (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Basic ')) {
return res.sendStatus(401);
}
const credentials = Buffer.from(authHeader.slice(6), 'base64').toString();
const [username, password] = credentials.split(':');
if (username !== process.env.WEBHOOK_USERNAME || password !== process.env.WEBHOOK_PASSWORD) {
return res.sendStatus(401);
}
next();
}, express.json(), (req, res) => {
// Credentials verified - process the webhook
res.sendStatus(200);
setImmediate(() => {
processEvents(req.body).catch(console.error);
});
});
PHP (Laravel)
Route::post('/webhooks/lettr', function (Request $request) {
$username = $request->getUser();
$password = $request->getPassword();
if ($username !== config('services.lettr.webhook_username')
|| $password !== config('services.lettr.webhook_password')) {
return response('Unauthorized', 401);
}
// Credentials verified - process the webhook
$events = $request->all();
dispatch(new ProcessWebhookEvents($events));
return response('OK', 200);
});
Python (Flask)
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/lettr', methods=['POST'])
def webhook():
auth = request.authorization
if not auth or auth.username != os.environ['WEBHOOK_USERNAME'] \
or auth.password != os.environ['WEBHOOK_PASSWORD']:
abort(401)
events = request.get_json()
process_events(events)
return 'OK', 200
Store your webhook credentials in environment variables or a secrets manager. Never hardcode them in your source code.
OAuth 2.0 Authentication
With OAuth 2.0 client credentials, Lettr obtains an access token from your OAuth server before each webhook delivery and includes it as a Bearer token. You provide the client ID, client secret, and token URL when creating the webhook in the dashboard.
Verifying OAuth Tokens
Verify the Bearer token in your webhook handler:
import express from 'express';
const app = express();
app.post('/webhooks/lettr', async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.sendStatus(401);
}
const token = authHeader.slice(7);
try {
// Validate the token with your OAuth server
const isValid = await validateOAuthToken(token);
if (!isValid) {
return res.sendStatus(401);
}
next();
} catch (err) {
console.error('Token validation failed:', err);
return res.sendStatus(401);
}
}, express.json(), (req, res) => {
res.sendStatus(200);
setImmediate(() => {
processEvents(req.body).catch(console.error);
});
});
No Authentication
If you choose none, Lettr sends webhook requests without any authentication credentials. This is the simplest option but offers no protection against unauthorized requests.
Using no authentication means anyone who discovers your webhook URL can send fake events to your endpoint. If you choose this option, consider adding other security measures like IP allowlisting.
Additional Security Measures
IP Allowlisting
For additional security, restrict your webhook endpoint to only accept requests from Lettr’s IP addresses. Contact support for the current list.
const ALLOWED_IPS = ['203.0.113.10', '203.0.113.11', '203.0.113.12'];
app.post('/webhooks/lettr', (req, res, next) => {
const clientIp = req.ip || req.connection.remoteAddress;
if (!ALLOWED_IPS.includes(clientIp)) {
console.warn(`Webhook request from unauthorized IP: ${clientIp}`);
return res.sendStatus(403);
}
next();
});
If you’re behind a proxy or load balancer, make sure to configure your application to trust the proxy and extract the real client IP from the X-Forwarded-For header.
HTTPS
Always use HTTPS endpoints for your webhooks. This ensures that authentication credentials and event data are encrypted in transit.
Secret URL Paths
As an additional layer of defense, you can use a hard-to-guess URL path for your webhook endpoint:
https://example.com/webhooks/lettr/a1b2c3d4e5f6
This is not a substitute for proper authentication, but it adds an extra barrier for anyone scanning for webhook endpoints.
Checking Webhook Auth Status via API
You can check whether a webhook has authentication configured using the read-only API:
curl -X GET "https://app.lettr.com/api/webhooks/{webhookId}" \
-H "Authorization: Bearer lttr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
The response includes the auth type and whether credentials are configured:
{
"message": "Webhook retrieved successfully.",
"data": {
"id": "webhook-abc123",
"name": "Order Notifications",
"url": "https://example.com/webhook",
"enabled": true,
"event_types": ["message.delivery", "message.bounce"],
"auth_type": "basic",
"has_auth_credentials": true,
"last_successful_at": "2024-01-15T10:30:00+00:00",
"last_failure_at": null,
"last_status": "success"
}
}
The API response shows auth_type and has_auth_credentials but never exposes the actual credentials. To update authentication settings, use the Lettr dashboard.