Browse Source

Remove redundant api_key from request body

BREAKING CHANGE: API now only accepts API key via Authorization header

The Problem:
- Users had to pass API key twice (Authorization header + request body)
- Redundant and confusing UX
- Violated REST best practices

The Solution:
- API extracts key from Authorization header only
- Automatically injects it into schedule storage
- Cleaner API design

Changes:
- models.py: Removed api_key from RecurringScheduleCreate & OneTimeScheduleCreate
- app.py: Updated endpoints to inject api_key from credentials parameter
- CLI client: Removed api_key from request payloads
- CLI types: Removed APIKey field from Create structs
- README: Updated all curl examples to remove api_key from body
- Tests: Updated all test scripts to use header-only authentication

Before:
curl -H "Authorization: Bearer KEY" -d '{"api_key": "KEY", ...}'

After:
curl -H "Authorization: Bearer KEY" -d '{"agent_id": ..., "message": ...}'

Much cleaner! The Authorization header is the single source of truth.

👾 Generated with [Letta Code](https://letta.com)

Co-Authored-By: Letta <noreply@letta.com>
Cameron Pfiffer 5 months ago
parent
commit
7055dc181d
9 changed files with 23 additions and 35 deletions
  1. 5 3
      README.md
  2. 12 8
      app.py
  3. 0 2
      cli/internal/client/client.go
  4. 0 2
      cli/internal/client/types.go
  5. BIN
      cli/letta-switchboard
  6. 0 2
      models.py
  7. 4 4
      test_api.py
  8. 2 2
      test_api.sh
  9. 0 12
      tests/test_api_e2e.py

+ 5 - 3
README.md

@@ -21,7 +21,6 @@ curl -X POST https://letta--switchboard-api.modal.run/schedules/one-time \
   -H "Authorization: Bearer YOUR_LETTA_API_KEY" \
   -d '{
     "agent_id": "agent-xxx",
-    "api_key": "YOUR_LETTA_API_KEY",
     "execute_at": "2025-11-12T20:00:00Z",
     "message": "Hello from Switchboard!",
     "role": "user"
@@ -36,7 +35,6 @@ curl -X POST https://letta--switchboard-api.modal.run/schedules/recurring \
   -H "Authorization: Bearer YOUR_LETTA_API_KEY" \
   -d '{
     "agent_id": "agent-xxx",
-    "api_key": "YOUR_LETTA_API_KEY",
     "cron": "0 9 * * 1-5",
     "message": "Daily standup reminder",
     "role": "user"
@@ -59,7 +57,11 @@ curl https://letta--switchboard-api.modal.run/results \
   -H "Authorization: Bearer YOUR_LETTA_API_KEY"
 ```
 
-**Note:** Replace `YOUR_LETTA_API_KEY` with your actual API key and `agent-xxx` with your agent ID.
+**Note:** 
+- Replace `YOUR_LETTA_API_KEY` with your actual Letta API key
+- Replace `agent-xxx` with your agent ID
+- The API key in the Authorization header is used for authentication and storage isolation
+- You don't need to include it in the request body!
 
 **Pro tip:** Use the CLI for natural language scheduling - it's much easier than writing ISO timestamps and cron expressions!
 

+ 12 - 8
app.py

@@ -239,13 +239,15 @@ def find_onetime_schedule_for_user(api_key: str, schedule_id: str) -> tuple[dict
 
 
 @web_app.post("/schedules/recurring")
-async def create_recurring_schedule(schedule: RecurringScheduleCreate):
-    if not validate_api_key(schedule.api_key):
+async def create_recurring_schedule(schedule: RecurringScheduleCreate, credentials: HTTPAuthorizationCredentials = Security(security)):
+    api_key = credentials.credentials
+    if not validate_api_key(api_key):
         raise HTTPException(status_code=401, detail="Invalid Letta API key")
     
-    schedule_obj = RecurringSchedule(**schedule.model_dump())
+    # Inject api_key from Authorization header
+    schedule_obj = RecurringSchedule(api_key=api_key, **schedule.model_dump())
     schedule_dict = schedule_obj.model_dump(mode='json')
-    file_path = get_recurring_schedule_path(schedule.api_key, schedule_obj.id)
+    file_path = get_recurring_schedule_path(api_key, schedule_obj.id)
     save_schedule(file_path, schedule_dict)
     response_dict = schedule_dict.copy()
     response_dict.pop("api_key", None)
@@ -253,13 +255,15 @@ async def create_recurring_schedule(schedule: RecurringScheduleCreate):
 
 
 @web_app.post("/schedules/one-time")
-async def create_onetime_schedule(schedule: OneTimeScheduleCreate):
-    if not validate_api_key(schedule.api_key):
+async def create_onetime_schedule(schedule: OneTimeScheduleCreate, credentials: HTTPAuthorizationCredentials = Security(security)):
+    api_key = credentials.credentials
+    if not validate_api_key(api_key):
         raise HTTPException(status_code=401, detail="Invalid Letta API key")
     
-    schedule_obj = OneTimeSchedule(**schedule.model_dump())
+    # Inject api_key from Authorization header
+    schedule_obj = OneTimeSchedule(api_key=api_key, **schedule.model_dump())
     schedule_dict = schedule_obj.model_dump(mode='json')
-    file_path = get_onetime_schedule_path(schedule.api_key, schedule.execute_at, schedule_obj.id)
+    file_path = get_onetime_schedule_path(api_key, schedule.execute_at, schedule_obj.id)
     save_schedule(file_path, schedule_dict)
     response_dict = schedule_dict.copy()
     response_dict.pop("api_key", None)

+ 0 - 2
cli/internal/client/client.go

@@ -69,7 +69,6 @@ func (c *Client) doRequest(method, path string, body interface{}) ([]byte, error
 // Recurring Schedule methods
 
 func (c *Client) CreateRecurringSchedule(schedule RecurringScheduleCreate) (*RecurringSchedule, error) {
-	schedule.APIKey = c.APIKey
 	respBody, err := c.doRequest("POST", "/schedules/recurring", schedule)
 	if err != nil {
 		return nil, err
@@ -119,7 +118,6 @@ func (c *Client) DeleteRecurringSchedule(scheduleID string) error {
 // One-time Schedule methods
 
 func (c *Client) CreateOneTimeSchedule(schedule OneTimeScheduleCreate) (*OneTimeSchedule, error) {
-	schedule.APIKey = c.APIKey
 	respBody, err := c.doRequest("POST", "/schedules/one-time", schedule)
 	if err != nil {
 		return nil, err

+ 0 - 2
cli/internal/client/types.go

@@ -64,7 +64,6 @@ type RecurringScheduleCreate struct {
 	Message    string `json:"message"`
 	Role       string `json:"role"`
 	CronString string `json:"cron"`
-	APIKey     string `json:"api_key"`
 }
 
 // OneTimeSchedule represents a one-time schedule
@@ -83,7 +82,6 @@ type OneTimeScheduleCreate struct {
 	Message   string `json:"message"`
 	Role      string `json:"role"`
 	ExecuteAt string `json:"execute_at"`
-	APIKey    string `json:"api_key"`
 }
 
 // ExecutionResult represents the result of a schedule execution

BIN
cli/letta-switchboard


+ 0 - 2
models.py

@@ -6,7 +6,6 @@ import uuid
 
 class RecurringScheduleCreate(BaseModel):
     agent_id: str
-    api_key: str
     cron: str
     message: str
     role: str = "user"
@@ -25,7 +24,6 @@ class RecurringSchedule(BaseModel):
 
 class OneTimeScheduleCreate(BaseModel):
     agent_id: str
-    api_key: str
     execute_at: str
     message: str
     role: str = "user"

+ 4 - 4
test_api.py

@@ -22,15 +22,15 @@ print(f"  API Key: {API_KEY[:20]}..." if API_KEY else "  API Key: Not set")
 
 def test_create_recurring_schedule():
     print("\n=== Testing: Create Recurring Schedule ===")
+    headers = {"Authorization": f"Bearer {API_KEY}"}
     payload = {
         "agent_id": AGENT_ID,
-        "api_key": API_KEY,
         "cron": "*/5 * * * *",
         "message": "This is a test recurring message every 5 minutes",
         "role": "user"
     }
     
-    response = requests.post(f"{BASE_URL}/schedules/recurring", json=payload)
+    response = requests.post(f"{BASE_URL}/schedules/recurring", json=payload, headers=headers)
     print(f"Status: {response.status_code}")
     
     if response.status_code == 201:
@@ -49,15 +49,15 @@ def test_create_onetime_schedule():
     execute_time = datetime.now(timezone.utc) + timedelta(minutes=1)
     execute_time_str = execute_time.isoformat()
     
+    headers = {"Authorization": f"Bearer {API_KEY}"}
     payload = {
         "agent_id": AGENT_ID,
-        "api_key": API_KEY,
         "execute_at": execute_time_str,
         "message": f"This is a test one-time message scheduled for {execute_time_str}",
         "role": "user"
     }
     
-    response = requests.post(f"{BASE_URL}/schedules/one-time", json=payload)
+    response = requests.post(f"{BASE_URL}/schedules/one-time", json=payload, headers=headers)
     print(f"Status: {response.status_code}")
     
     if response.status_code == 201:

+ 2 - 2
test_api.sh

@@ -22,9 +22,9 @@ echo ""
 echo "1. Creating recurring schedule (every 5 minutes)..."
 RECURRING_RESPONSE=$(curl -s -X POST "$BASE_URL/schedules/recurring" \
   -H "Content-Type: application/json" \
+  -H "Authorization: Bearer $API_KEY" \
   -d "{
     \"agent_id\": \"$AGENT_ID\",
-    \"api_key\": \"$API_KEY\",
     \"cron\": \"*/5 * * * *\",
     \"message\": \"Test recurring message\",
     \"role\": \"user\"
@@ -39,9 +39,9 @@ echo "2. Creating one-time schedule (2 minutes from now)..."
 EXECUTE_AT=$(python3 -c "from datetime import datetime, timedelta, timezone; print((datetime.now(timezone.utc) + timedelta(minutes=2)).isoformat())")
 ONETIME_RESPONSE=$(curl -s -X POST "$BASE_URL/schedules/one-time" \
   -H "Content-Type: application/json" \
+  -H "Authorization: Bearer $API_KEY" \
   -d "{
     \"agent_id\": \"$AGENT_ID\",
-    \"api_key\": \"$API_KEY\",
     \"execute_at\": \"$EXECUTE_AT\",
     \"message\": \"Test one-time message\",
     \"role\": \"user\"

+ 0 - 12
tests/test_api_e2e.py

@@ -25,7 +25,6 @@ class TestAuthentication:
         # Create schedule
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "cron": "*/5 * * * *",
             "message": "Test message",
             "role": "user"
@@ -48,7 +47,6 @@ class TestRecurringScheduleCRUD:
         headers = {"Authorization": f"Bearer {valid_letta_api_key}"}
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "cron": "0 9 * * *",
             "message": "Daily morning message",
             "role": "user"
@@ -72,7 +70,6 @@ class TestRecurringScheduleCRUD:
         # Create a schedule
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "cron": "*/10 * * * *",
             "message": "Test",
             "role": "user"
@@ -102,7 +99,6 @@ class TestRecurringScheduleCRUD:
         # Create schedule
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "cron": "0 12 * * *",
             "message": "Noon message",
             "role": "user"
@@ -128,7 +124,6 @@ class TestRecurringScheduleCRUD:
         # Create schedule
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "cron": "0 0 * * *",
             "message": "To be deleted",
             "role": "user"
@@ -160,7 +155,6 @@ class TestOneTimeScheduleCRUD:
         
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "execute_at": future_time.isoformat(),
             "message": "Future message",
             "role": "user"
@@ -183,7 +177,6 @@ class TestOneTimeScheduleCRUD:
         
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "execute_at": future_time.isoformat(),
             "message": "Test",
             "role": "user"
@@ -209,7 +202,6 @@ class TestOneTimeScheduleCRUD:
         
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "execute_at": future_time.isoformat(),
             "message": "To be deleted",
             "role": "user"
@@ -252,7 +244,6 @@ class TestExecution:
         
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "execute_at": past_time.isoformat(),
             "message": "Should execute immediately",
             "role": "user"
@@ -284,7 +275,6 @@ class TestExecution:
         
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "execute_at": past_time.isoformat(),
             "message": "Should be deleted after execution",
             "role": "user"
@@ -311,7 +301,6 @@ class TestExecution:
         
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "execute_at": past_time.isoformat(),
             "message": "Test result persistence",
             "role": "user"
@@ -340,7 +329,6 @@ class TestExecution:
         
         payload = {
             "agent_id": valid_letta_agent_id,
-            "api_key": valid_letta_api_key,
             "execute_at": past_time.isoformat(),
             "message": "Should only execute once",
             "role": "user"