Skip to content

docs: reconcile API key permissions with what the API enforces - #194

Merged
shalim786 merged 1 commit into
mainfrom
fix/api-key-permissions-parity
Jul 28, 2026
Merged

docs: reconcile API key permissions with what the API enforces#194
shalim786 merged 1 commit into
mainfrom
fix/api-key-permissions-parity

Conversation

@shalim786

Copy link
Copy Markdown
Contributor

Why

Customer report (Tanasi Studio, 2026-07-15): they created an inbox-scoped key with thread_read: false and thread_delete: false set explicitly, and both came back true. They also found that a control key granted thread_delete: true could not delete a thread at all.

Both observations are explained by the same thing: thread_read and thread_delete do not exist on the server. They were removed at the end of March 2026 (9542d122, 0f4790b3) when thread authorization was consolidated onto the message permissions. This repo's definition was never updated, so we have been publishing two permissions the API does not implement for four months.

The API strips unrecognized fields inside the permissions object rather than rejecting them, so a key requesting thread_delete silently receives nothing and still gets a 200. That is what made the drift invisible to callers.

The customer's key was never actually at risk — thread deletion requires message_delete, which they had not granted. But message_delete was also undocumented, so the question was unanswerable from our reference.

What changed

fern/definition/api-keys.yml

  • Remove thread_read and thread_delete.
  • Add message_delete and label_unauthenticated_read, both enforced by the server but never published.

fern/pages/core-concepts/permissions.mdx

  • Replace the Threads permission table with an operation-to-permission mapping, plus a warning that no thread permission exists.
  • Add the missing permissions to the reference tables, including api_key_update.
  • Update all code examples and both Cursor/Claude blocks so no example teaches a permission that does nothing.

fern/pages/websockets.mdx

  • message.received.unauthenticated is gated by label_unauthenticated_read the same way spam and blocked are; the page claimed only spam and blocked required a permission.

Verification

The definition and agentmail-api/src/core/schemas/api-key.ts now agree on all 36 keys in the same order, every server permission appears in a reference table, and no phantom permission remains in any example. Checked programmatically against the current permissionShape.

⚠️ Breaking for generated SDKs

Removing the two fields drops them from the ApiKeyPermissions model. They never had any effect, so no behavior changes, but setting them explicitly becomes a type error on upgrade. The reporting customer sets both today. This needs a changelog entry with a breaking-changes section.

Not covered here

  • Nothing binds these two lists. They are hand-maintained in separate repos with no CI check, which is the actual root cause. Tracked separately.
  • Silent stripping. Worth deciding whether the nested permissions object should reject unknown keys. Flipping it straight to strict would 400 anyone who copy-pasted the old example on this page, which contained thread_read, so it needs a deprecation path.
  • Customer impact. Anyone who granted thread_read: true without message_read: true has had a thread-blind key since March. Findable by scanning api-key-table for keys created after 2026-03-30 with a permissions attribute and no message_read.

Testing

Not preview-built locally — the fern CLI is not installed on this machine. The new Threads section adds a <Warning> component; please confirm it renders before merge.

🤖 Generated with Claude Code

The published ApiKeyPermissions list had drifted from the server's since
thread permissions were consolidated onto the message permissions at the
end of March 2026. Reported by a customer who set thread_read and
thread_delete to false, saw them come back true, and could not delete a
thread with a key granted thread_delete.

The API strips unrecognized fields inside the permissions object rather
than rejecting them, so a key requesting thread_delete silently received
nothing. That made the drift invisible to callers.

Definition:
- Remove thread_read and thread_delete. Neither exists on the server;
  thread handlers gate on message_read, message_update, message_delete.
- Add message_delete and label_unauthenticated_read, both enforced but
  never published. message_delete is what actually governs thread
  deletion, so its absence made the customer's question unanswerable
  from the reference.

Permissions guide:
- Replace the Threads permission table with the operation-to-message-
  permission mapping, plus a warning that no thread permission exists.
- Add the missing permissions to the reference tables, including
  api_key_update, and update the code examples and the Cursor/Claude
  blocks so no example teaches a permission that does nothing.

Websockets guide:
- message.received.unauthenticated is gated by label_unauthenticated_read
  the same way spam and blocked are; the page said only spam and blocked
  required a permission.

The definition and src/core/schemas/api-key.ts now agree on all 36 keys
in the same order. Nothing binds them, so this can drift again.

Note for the changelog: removing the two fields is breaking for generated
SDKs. They never had any effect, so no behavior changes, but setting them
explicitly becomes a type error on upgrade.

Co-Authored-By: Claude <[email protected]>
@github-actions

Copy link
Copy Markdown

📋 View the side-by-side API change review

The link expires in 7 days.

🔒 Your API specs are encrypted in CI before they're uploaded. The decryption key stays in this link's URL fragment (after the #), which browsers never send to a server, so oasdiff cannot read your specs. How it works →

Posted automatically by the oasdiff GitHub Action. To turn this off (no spec upload, no comment), set review: false on the action.

@github-actions

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown

✨ API Changes

# API Changelog n/a vs. n/a


## API Changes

### GET /v0/api-keys
- :warning: removed the optional property `api_keys/items/permissions/thread_delete` from the response with the `200` status
- :warning: removed the optional property `api_keys/items/permissions/thread_read` from the response with the `200` status
-  added the optional property `api_keys/items/permissions/label_unauthenticated_read` to the response with the `200` status
-  added the optional property `api_keys/items/permissions/message_delete` to the response with the `200` status


### POST /v0/api-keys
- :warning: removed the request property `permissions/thread_delete`
- :warning: removed the request property `permissions/thread_read`
- :warning: removed the optional property `permissions/thread_delete` from the response with the `200` status
- :warning: removed the optional property `permissions/thread_read` from the response with the `200` status
-  added the new optional request property `permissions/label_unauthenticated_read`
-  added the new optional request property `permissions/message_delete`
-  added the optional property `permissions/label_unauthenticated_read` to the response with the `200` status
-  added the optional property `permissions/message_delete` to the response with the `200` status


### GET /v0/inboxes/{inbox_id}/api-keys
- :warning: removed the optional property `api_keys/items/permissions/thread_delete` from the response with the `200` status
- :warning: removed the optional property `api_keys/items/permissions/thread_read` from the response with the `200` status
-  added the optional property `api_keys/items/permissions/label_unauthenticated_read` to the response with the `200` status
-  added the optional property `api_keys/items/permissions/message_delete` to the response with the `200` status


### POST /v0/inboxes/{inbox_id}/api-keys
- :warning: removed the request property `permissions/thread_delete`
- :warning: removed the request property `permissions/thread_read`
- :warning: removed the optional property `permissions/thread_delete` from the response with the `200` status
- :warning: removed the optional property `permissions/thread_read` from the response with the `200` status
-  added the new optional request property `permissions/label_unauthenticated_read`
-  added the new optional request property `permissions/message_delete`
-  added the optional property `permissions/label_unauthenticated_read` to the response with the `200` status
-  added the optional property `permissions/message_delete` to the response with the `200` status


### GET /v0/pods/{pod_id}/api-keys
- :warning: removed the optional property `api_keys/items/permissions/thread_delete` from the response with the `200` status
- :warning: removed the optional property `api_keys/items/permissions/thread_read` from the response with the `200` status
-  added the optional property `api_keys/items/permissions/label_unauthenticated_read` to the response with the `200` status
-  added the optional property `api_keys/items/permissions/message_delete` to the response with the `200` status


### POST /v0/pods/{pod_id}/api-keys
- :warning: removed the request property `permissions/thread_delete`
- :warning: removed the request property `permissions/thread_read`
- :warning: removed the optional property `permissions/thread_delete` from the response with the `200` status
- :warning: removed the optional property `permissions/thread_read` from the response with the `200` status
-  added the new optional request property `permissions/label_unauthenticated_read`
-  added the new optional request property `permissions/message_delete`
-  added the optional property `permissions/label_unauthenticated_read` to the response with the `200` status
-  added the optional property `permissions/message_delete` to the response with the `200` status

💡 Download api-changelog-diff artifact or tag @fern Writer in #github-prs for changelog.

@shalim786
shalim786 merged commit 2930e18 into main Jul 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants