Skip to main content
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 TypeDescription
noneNo authentication. Lettr sends requests without credentials.
basicHTTP Basic Authentication. Lettr includes a username and password in each request.
oauth2OAuth 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.