Managing API keys and webhook signing secrets in FastAPI with Encore, pydantic-settings, AWS Secrets Manager or Google Secret Manager, with per-environment values and nothing in git.
FastAPI apps collect secrets quickly, such as a payment API key, a webhook signing secret and an email provider token. They usually start in a .env file, and production needs them encrypted, scoped to the right service and different per environment.
This guide adds secrets to a FastAPI billing API with Encore, pydantic-settings, AWS Secrets Manager and Google Secret Manager. The examples read a Stripe-style API key and verify an HMAC-signed webhook.
What you need to decide:
Encore combines the secrets your FastAPI code declares with storage per environment, reading local values from a git-ignored file under encore run and storing production values in AWS Secrets Manager or GCP Secret Manager in your own account. It is an infrastructure SDK added to an existing app, so routers and dependencies stay as they are.

Secrets are declared by name. A SharedSecret can be read from any service, while a Secret is declared by the service that reads it:
from contextlib import asynccontextmanager
import encore
from encore import secrets
from fastapi import FastAPI
# Shared across services; set per environment with `encore secret set`.
stripe_key = secrets.SharedSecret[str]("StripeSecretKey")
# Private to this service.
webhook_secret = secrets.Secret[str]("WebhookSigningSecret")
@asynccontextmanager
async def lifespan(app: FastAPI):
await encore.start()
yield
app = FastAPI(lifespan=lifespan)
Routes read .value where they need it. The webhook handler computes an HMAC over the raw body and compares it in constant time with hmac.compare_digest:
import hashlib
import hmac
from typing import Annotated
from fastapi import Header, HTTPException, Request
@app.get("/billing/mode")
async def billing_mode() -> dict:
# Read the value where it is used, never at import time into a log line.
key = stripe_key.value
return {"mode": "live" if key.startswith("sk_live_") else "test"}
@app.post("/webhooks/payments", status_code=204)
async def payment_webhook(
request: Request, x_signature: Annotated[str, Header()]
) -> None:
body = await request.body()
expected = hmac.new(webhook_secret.value.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, x_signature):
raise HTTPException(status_code=401, detail="bad signature")
Local values live in .secrets.user.toml, which is git-ignored, with shared secrets under [shared] and service secrets under the service name:
[shared]
StripeSecretKey = "sk_test_123"
[services.billing]
WebhookSigningSecret = "whsec_local"
Under encore run, a correctly signed webhook returns 204 and a wrong signature returns 401:
Production values are set from the CLI once per environment type:
On deploy, values are stored in Secrets Manager on AWS or Secret Manager on GCP in your own account, and Encore provisions an IAM role for the service.
An agent adding an integration declares the secret next to the client code and tests it with a local value, and production only needs the encore secret set call.
Never log .value or return it in a response; the mode check above only reveals the prefix. Stripe's own signature scheme adds a timestamp to stop replays, so use the provider's library when one exists and keep the HMAC check for your own webhooks.
pydantic-settings loads typed settings from environment variables and .env files, and SecretStr hides values in reprs:
from functools import lru_cache
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env")
stripe_secret_key: SecretStr
webhook_signing_secret: SecretStr
@lru_cache
def get_settings() -> Settings:
return Settings()
Routes take settings: Annotated[Settings, Depends(get_settings)] and call .get_secret_value(). pydantic-settings reads values but does not store them, so production still needs something that puts encrypted values into the environment.
On AWS, the app fetches values at startup with boto3:
import json
import boto3
client = boto3.client("secretsmanager")
values = json.loads(client.get_secret_value(SecretId="billing/production")["SecretString"])
stripe_key = values["StripeSecretKey"]
You create one secret per environment, grant the task role secretsmanager:GetSecretValue on it, and decide how local development gets values. ECS can also inject secrets as environment variables at task start, which avoids the SDK call.
Secret Manager on GCP has the same shape:
from google.cloud import secretmanager
client = secretmanager.SecretManagerServiceClient()
name = "projects/my-project/secrets/stripe-secret-key/versions/latest"
stripe_key = client.access_secret_version(name=name).payload.data.decode()
The service account needs the Secret Manager Secret Accessor role on each secret. Cloud Run can mount secrets as environment variables or files with --set-secrets, which avoids the client library in the app.
Python support in Encore is in beta, and you can sign up for access to try it with your FastAPI app.
Keep secret values out of the repository, load them per environment and read them where they are used. With Encore, declare stripe_key = secrets.SharedSecret[str]("StripeSecretKey") and webhook_secret = secrets.Secret[str]("WebhookSigningSecret"), read them with .value, set local values in .secrets.user.toml and production values with encore secret set.
Read the raw body with await request.body(), compute an HMAC-SHA256 over it with the signing secret, and compare it to the signature header with hmac.compare_digest. Parsing the JSON first can change the bytes and break the signature.
A .env file kept out of git is fine for local development. In production, values should come from a secrets manager or the platform's secret store, so they are encrypted, access is logged and rotation does not need a new image.
A shared secret is available to every service in the app, such as a payment provider key used in several places. A service secret is visible only to the service that declares it, such as the signing secret for one webhook endpoint.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.