Fix Frappe API Token After Restart
Background
The CTMS platform uses KrakenD as its API gateway. Every request KrakenD forwards to Frappe is authenticated using a pre-shared API token (FRAPPE_API_TOKEN) stored in .env.production. The token is a <api_key>:<api_secret> pair tied to the Frappe Administrator user.
When the platform is first deployed, zynctl.sh deploy automatically extracts this token from Frappe and injects it into both the env file and the KrakenD container.
How the token flows
.env.production → docker compose (env var) → KrakenD entrypoint.sh (envsubst)
→ krakend.json (Lua pre-scripts)
→ Authorization: token <key>:<secret>
→ Frappe backend (port 8080)
Problem
After restarting the Frappe Docker stack (docker compose restart or docker compose up -d in the frappe-marley-health/ directory), all API calls through KrakenD suddenly return 401 AuthenticationError — even though nothing was changed manually.
Root Cause
Frappe's setup init container (frappe-marley-health-setup-1) can re-run on restart and silently call generate_keys for the Administrator user. This:
- ✅ Keeps the same
api_key(so it looks unchanged intabUser) - ❌ Regenerates the
api_secretin the database - ❌ Does not update
.env.productionor the running KrakenD container
The result: KrakenD keeps sending the old secret, while Frappe expects the new one → 401.
Symptoms
| Symptom | Where |
|---|---|
401 AuthenticationError on all /api/v1/* endpoints | KrakenD gateway (port 9080) |
{"exc_type": "AuthenticationError"} in response body | KrakenD → Frappe |
Direct curl to Frappe with the .env.production token returns 401 | Frappe backend (port 8080) |
| Frappe admin panel and site UI still work fine | Browser (session-based auth, unaffected) |
Solution
SSH into the server and run:
cd /opt/ctms-deployment
./zynctl.sh refresh-token
That's it. The command will:
- Test the current token from
.env.productionagainst Frappe - Regenerate the token if invalid (using the built-in helper script)
- Patch
.env.productionwith the new token - Recreate the KrakenD API gateway container (force-recreate, not restart)
- Verify both direct Frappe auth and the KrakenD gateway
═══════════════════════════════════════════════════════════════
Frappe API Token Refresh
═══════════════════════════════════════════════════════════════
▸ Step 1: Testing current token from .env.production...
⚠️ Token is invalid (HTTP 401). Refreshing...
▸ Step 2: Regenerating token via frappe-generate-token.py...
New token: 7b5ff5542d0120e:...
▸ Step 3: Patching .env.production...
✅ .env.production updated
▸ Step 4: Recreating API gateway (force-recreate)...
▸ Step 5: Verifying...
Direct Frappe auth: HTTP 200 ✅
KrakenD gateway: HTTP 200 ✅
Container token: 7b5ff5542d0120e:... ✅ matches
✅ All fixed! API gateway is operational.
Get the Current Token
After a refresh (or fresh deployment), extract the full token from the env file:
TOKEN=$(grep "^FRAPPE_API_TOKEN=" /opt/ctms-deployment/.env.production | cut -d= -f2)
echo "Token: ${TOKEN}"
The output should be a key:secret pair (e.g. 7b5ff5542d0120e:a1b2c3d4e5f6). If empty, re-run ./zynctl.sh refresh-token.
Manual Verification
If you want to confirm things are working without running the full refresh:
cd /opt/ctms-deployment
# Read current token
FRAPPE_TOKEN=$(grep "^FRAPPE_API_TOKEN=" .env.production | head -1 | cut -d= -f2-)
# Test direct Frappe auth
curl -s -w "\nHTTP: %{http_code}" \
-H "Authorization: token ${FRAPPE_TOKEN}" \
"http://127.0.0.1:8080/api/method/frappe.auth.get_logged_user"
# Test via KrakenD gateway (port 9080)
curl -s -w "\nHTTP: %{http_code}" \
"http://127.0.0.1:9080/api/v1/doctype/Study?limit_page_length=1&fields=[\"name\"]"
- HTTP 200 → Token is fine, no action needed.
- HTTP 401 → Run
./zynctl.sh refresh-token.
What the Command Does
The refresh-token command in zynctl.sh:
- Tests the current token from
.env.productionagainst Frappe's auth endpoint - Exits early if the token is still valid (safe to run anytime)
- Regenerates the token using the built-in
frappe-generate-token.pyhelper script (already mounted inside the backend container) - Patches
.env.production(and.env) with the newapi_key:api_secret - Force-recreates the KrakenD API gateway container so it picks up the new token
- Verifies both direct Frappe auth (port 8080) and the KrakenD gateway (port 9080)
--force-recreate and not restart?docker compose restart reuses the existing container with stale environment variables baked in at creation time. The token refresh requires docker compose up -d --force-recreate which destroys and rebuilds the container, injecting the updated env vars. The refresh-token command handles this automatically.
Affected Components
| Component | Role | Impact |
|---|---|---|
frappe-marley-health-setup-1 | Init container that runs Frappe wizard | Regenerates api_secret on restart |
.env.production | Stores FRAPPE_API_TOKEN | Becomes stale after secret rotation |
ctms-api-gateway (KrakenD) | Injects token via Lua pre-scripts | Sends stale token → 401 |
frappe-marley-health-backend-1 | Frappe backend API | Rejects stale token |
Related
- Debugging & Troubleshooting → Frappe API 401 after restart — full root-cause analysis
- Platform Runbook → Frappe post-restart checklist — operational checklist
- Environment Variables —
FRAPPE_API_TOKENreference - zynctl.sh source —
cmd_refresh_tokenimplementation