Meeting Bot Authentication: API Keys, OAuth, and Security
You pushed your API key to GitHub by accident. It was in a .env file that you thought was gitignored, but the gitignore had a typo. By the time you noticed, the key had been live in a public repo for six hours. You revoke it, generate a new one, and update three Lambda functions, two services, and a cron job and then spend an anxious weekend wondering whether anyone scraped it. This is not a hypothetical. It happens to careful engineers all the time.
Meeting bot authentication has more attack surface than a typical API integration because it spans multiple credential types. There's your MeetStream API key for creating bots. There's OAuth 2.0 for Zoom, which requires App Marketplace approval and has its own token refresh lifecycle. There's webhook signature verification to ensure events actually come from MeetStream. And there's whatever user authentication you're building on top, users authorizing your app to schedule bots on their meetings.
Each of these has different rotation requirements, different storage patterns, and different failure modes when compromised. Getting any one wrong can mean unauthorized bot creation (expensive and abusive), leaked meeting recordings (a serious privacy incident), or forged webhook events that corrupt your data pipeline.
In this guide, we'll cover MeetStream API key authentication, Zoom OAuth 2.0 with the required scopes, webhook HMAC-SHA256 verification, API key rotation strategies, and secrets management with environment variables and AWS Parameter Store. Let's get into it.
MeetStream API Key Authentication
Meeting bot authentication with MeetStream uses a token-based scheme. Every API request includes your API key in the Authorization header:
Authorization: Token YOUR_API_KEY
This applies to all endpoints, create_bot, fetching transcripts, checking bot status, everything. There's no session handshake, no OAuth flow, no token expiry to manage on the MeetStream side. The key is valid until you rotate it.
A minimal Python client that handles this correctly:

import urllib.request, json, os
class MeetStreamClient:
BASE_URL = "https://api.meetstream.ai/api/v1"
def __init__(self, api_key: str):
if not api_key:
raise ValueError("API key must not be empty")
self._api_key = api_key
def _request(self, method: str, path: str, body: dict = None) -> dict:
url = f"{self.BASE_URL}{path}"
data = json.dumps(body).encode() if body else None
req = urllib.request.Request(
url,
data=data,
headers={
"Authorization": f"Token {self._api_key}",
"Content-Type": "application/json"
},
method=method
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
def create_bot(self, meeting_link: str, bot_name: str, **kwargs) -> dict:
return self._request("POST", "/bots/create_bot", {
"meeting_link": meeting_link,
"bot_name": bot_name,
**kwargs
})
# Usage, key comes from environment, never hardcoded
client = MeetStreamClient(api_key=os.environ["MEETSTREAM_API_KEY"])
The key must come from the environment or a secrets manager, never from source code. Even in private repos, secrets in code become a liability when employees leave, repos are shared, or access controls change.
Zoom OAuth 2.0: App Marketplace Requirement
Zoom's OBF (OAuth Bot Framework) requires that any bot joining a Zoom meeting operates under an app registered in the Zoom App Marketplace and approved by the meeting host's organization (or published publicly). This is a platform-level security requirement, Zoom controls which bot apps can join meetings, not just which API keys are valid.
To join Zoom meetings, you need a Zoom OAuth app with the following scopes at minimum:
| Scope | Purpose |
|---|---|
meeting:read | Read meeting details and participant info |
meeting:write | Create and manage meetings |
user:read | Read user profile information |
zoomapp:inmeeting | Allow app to operate inside a meeting |
The OAuth 2.0 flow for a server-side application (no user interaction) uses the Client Credentials grant:
import base64, json, urllib.request
def get_zoom_access_token(client_id: str, client_secret: str, account_id: str) -> str:
"""
Server-to-Server OAuth for Zoom.
Returns a short-lived access token (valid ~1 hour).
"""
credentials = base64.b64encode(
f"{client_id}:{client_secret}".encode()
).decode()
req = urllib.request.Request(
f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={account_id}",
headers={
"Authorization": f"Basic {credentials}",
"Content-Type": "application/x-www-form-urlencoded"
},
method="POST"
)
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
return data["access_token"]
Zoom access tokens expire after one hour. Cache them and refresh when they expire, don't request a new token per API call, as repeated token requests will hit rate limits and are wasteful. Store the token expiry time alongside the token and refresh proactively 5 minutes before expiry.
Webhook Signature Verification
Webhook endpoints that accept data without verification are open to spoofed events. MeetStream signs each webhook delivery with an HMAC-SHA256 signature in the X-MeetStream-Signature header.
Verification in Node.js:

const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
// signatureHeader format: "sha256="
const [algo, receivedDigest] = signatureHeader.split('=', 2);
if (algo !== 'sha256') return false;
const expectedDigest = crypto
.createHmac('sha256', secret)
.update(rawBody) // must be raw bytes, not parsed JSON
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(receivedDigest, 'hex'),
Buffer.from(expectedDigest, 'hex')
);
}
// In Express: capture raw body BEFORE JSON parsing
app.use('/webhook', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.headers['x-meetstream-signature'];
if (!sig || !verifyWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Unauthorized');
}
// process event...
res.status(200).send('ok');
});
The most common implementation mistake is using express.json() middleware before the signature check. Once Express parses the JSON body, the original raw bytes are gone and you can't recompute the signature. Always capture the raw body first.
API Key Rotation Best Practices
Rotate your MeetStream API key when: an employee with access leaves, you suspect exposure (leaked in logs, accidentally committed, etc.), or on a scheduled basis (quarterly is a reasonable cadence for production keys).
A zero-downtime rotation procedure:
- Generate the new API key in the MeetStream dashboard at app.meetstream.ai.
- Update the key in your secrets manager (AWS Parameter Store, HashiCorp Vault, etc.) before touching application config.
- Deploy the configuration update to your services, they pull the key from the secrets manager at startup or via periodic refresh.
- Verify a test bot creation succeeds with the new key before revoking the old one.
- Revoke the old key from the dashboard.
- Monitor logs for any 401 errors from services that might have cached the old key.
Never skip step 4. Revoking the old key before confirming the new key works will take down production until you diagnose the failure.
Secrets Management: Environment Variables and AWS Parameter Store
For production deployments, environment variables are the minimum viable secrets management, better than hardcoded values, but they can leak via process listings, logs, and error messages that dump environment state.
AWS Systems Manager Parameter Store with encryption is the standard upgrade. Store secrets as SecureString parameters encrypted with KMS:

import boto3, os
ssm = boto3.client('ssm', region_name='us-east-1')
def get_secret(param_name: str) -> str:
resp = ssm.get_parameter(
Name=param_name,
WithDecryption=True
)
return resp['Parameter']['Value']
# Fetch once at startup, not per request
MEETSTREAM_API_KEY = get_secret('/meetstream/prod/api-key')
ZOOM_CLIENT_SECRET = get_secret('/meetstream/prod/zoom-client-secret')
WEBHOOK_SECRET = get_secret('/meetstream/prod/webhook-secret')
Use IAM roles to grant your Lambda functions or EC2 instances access to specific Parameter Store paths. This eliminates the need to store AWS credentials alongside your application credentials, the IAM role is attached at the infrastructure level, not the application level.
How MeetStream Fits
MeetStream's secure meeting API uses a straightforward token authentication model, one API key, passed in every request header, with no expiry management on your side. For Zoom, MeetStream's App Marketplace registration handles the OBF requirement, so you don't need to register your own Zoom app to use MeetStream bots in Zoom meetings. See the API documentation for complete authentication details and webhook security configuration.
Conclusion
Meeting bot authentication spans four layers: MeetStream API key (token header, never in source), Zoom OAuth 2.0 (server-to-server Client Credentials, hourly expiry, cache with proactive refresh), webhook signature verification (HMAC-SHA256 over raw body bytes, constant-time comparison), and secrets management (Parameter Store over plain env vars for production). Each layer has a specific failure mode when mishandled, leaked keys, expired tokens, forged webhooks, or plaintext secrets in logs. The rotation procedure matters as much as initial setup: test the new key before revoking the old one. Get started at meetstream.ai.
Frequently Asked Questions
Can I use the MeetStream API key directly in frontend JavaScript?
No. API keys must never appear in client-side code, they are visible to any user who inspects network requests or JavaScript source. All MeetStream API calls must come from your backend. If your frontend needs to trigger a bot creation, it should call your own backend API endpoint, which then calls MeetStream.
Why does Zoom require App Marketplace approval for meeting bots?
Zoom's OBF (OAuth Bot Framework) requires that meeting bots operate under a registered application, giving Zoom and meeting hosts visibility into which bot is joining. This is a platform security requirement designed to prevent unauthorized or anonymous bots from joining meetings. MeetStream's App Marketplace registration covers this for bots created through the MeetStream API.
How often should I rotate my MeetStream API key?
Rotate quarterly as a baseline for production keys, and immediately whenever you suspect exposure, accidental commits, employee departures, or any log output containing the key. Use the zero-downtime rotation procedure: generate new key, update secrets manager, deploy, verify, then revoke the old key only after confirming the new one works.
What is the right way to store the webhook secret?
Store the webhook secret in your secrets manager (AWS Parameter Store as a SecureString, HashiCorp Vault, etc.), never in environment variable files committed to source control. Fetch it at application startup using an IAM role, avoid fetching per-request to reduce latency and API call volume against the secrets manager.
What happens if webhook signature verification fails?
Return HTTP 401 and do not process the event. Log the failure with enough context (source IP, bot_id if present) to detect whether it's a misconfiguration (your secret is wrong) or a genuine attack attempt. Do not return 200 for invalid signatures, your webhook endpoint should treat invalid signatures as hard failures, not soft warnings to log and ignore.
