Skip to main content

claude_api/admin/
api_keys.rs

1//! API keys: retrieve / list / update.
2//!
3//! Note: the Admin API does **not** expose create / delete for API
4//! keys -- those live in the Console UI. Update can change the name or
5//! lifecycle status (`active` / `inactive` / `archived`).
6
7use serde::{Deserialize, Serialize};
8
9use crate::client::Client;
10use crate::error::Result;
11use crate::pagination::Paginated;
12
13use super::ListParams;
14
15/// Lifecycle status of an API key (response).
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18#[non_exhaustive]
19pub enum ApiKeyStatus {
20    /// In use.
21    Active,
22    /// Disabled but recoverable.
23    Inactive,
24    /// Archived (cannot be reactivated).
25    Archived,
26    /// Past its expiration timestamp.
27    Expired,
28}
29
30/// Subset of [`ApiKeyStatus`] valid as a write value (no `expired`).
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33#[non_exhaustive]
34pub enum WriteApiKeyStatus {
35    /// Mark active.
36    Active,
37    /// Disable.
38    Inactive,
39    /// Archive permanently.
40    Archived,
41}
42
43/// Actor (user or service account) that created an API key.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[non_exhaustive]
46pub struct ApiKeyCreator {
47    /// Stable actor ID.
48    pub id: String,
49    /// Actor type (e.g. `"user"`, `"service_account"`). Free-form
50    /// string; new types can appear without a wrapper enum needed.
51    #[serde(rename = "type")]
52    pub ty: String,
53}
54
55/// An API key record (the secret value itself is never returned).
56#[derive(Debug, Clone, Serialize, Deserialize)]
57#[non_exhaustive]
58pub struct ApiKey {
59    /// Stable key ID.
60    pub id: String,
61    /// Wire type tag (`"api_key"`).
62    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
63    pub ty: Option<String>,
64    /// Display name.
65    pub name: String,
66    /// Partially redacted hint (e.g. `sk-ant-…abcd`).
67    pub partial_key_hint: String,
68    /// Lifecycle status.
69    pub status: ApiKeyStatus,
70    /// Workspace this key belongs to. `None` for the default
71    /// workspace.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub workspace_id: Option<String>,
74    /// Who created the key.
75    pub created_by: ApiKeyCreator,
76    /// Creation timestamp.
77    pub created_at: String,
78    /// Expiration timestamp. `None` if the key never expires.
79    #[serde(default, skip_serializing_if = "Option::is_none")]
80    pub expires_at: Option<String>,
81}
82
83/// Request body for `POST /v1/organizations/api_keys/{id}` (update).
84/// Both fields are optional; pass `None` to leave a field unchanged.
85#[derive(Debug, Clone, Default, Serialize)]
86#[non_exhaustive]
87pub struct UpdateApiKeyRequest {
88    /// New name.
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub name: Option<String>,
91    /// New status.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub status: Option<WriteApiKeyStatus>,
94}
95
96impl UpdateApiKeyRequest {
97    /// Empty patch; chain setters to populate.
98    #[must_use]
99    pub fn new() -> Self {
100        Self::default()
101    }
102
103    /// Update the name.
104    #[must_use]
105    pub fn name(mut self, name: impl Into<String>) -> Self {
106        self.name = Some(name.into());
107        self
108    }
109
110    /// Update the status.
111    #[must_use]
112    pub fn status(mut self, status: WriteApiKeyStatus) -> Self {
113        self.status = Some(status);
114        self
115    }
116}
117
118/// Filters for [`ApiKeys::list`].
119#[derive(Debug, Clone, Default)]
120#[non_exhaustive]
121pub struct ListApiKeysParams {
122    /// Underlying pagination params.
123    pub paging: ListParams,
124    /// Filter by creator.
125    pub created_by_user_id: Option<String>,
126    /// Filter by status.
127    pub status: Option<ApiKeyStatus>,
128    /// Filter by workspace.
129    pub workspace_id: Option<String>,
130}
131
132impl ListApiKeysParams {
133    fn to_query(&self) -> Vec<(&'static str, String)> {
134        let mut q = self.paging.to_query();
135        if let Some(u) = &self.created_by_user_id {
136            q.push(("created_by_user_id", u.clone()));
137        }
138        if let Some(s) = self.status {
139            q.push((
140                "status",
141                match s {
142                    ApiKeyStatus::Active => "active".into(),
143                    ApiKeyStatus::Inactive => "inactive".into(),
144                    ApiKeyStatus::Archived => "archived".into(),
145                    ApiKeyStatus::Expired => "expired".into(),
146                },
147            ));
148        }
149        if let Some(w) = &self.workspace_id {
150            q.push(("workspace_id", w.clone()));
151        }
152        q
153    }
154}
155
156/// Namespace handle for API-key endpoints.
157pub struct ApiKeys<'a> {
158    client: &'a Client,
159}
160
161impl<'a> ApiKeys<'a> {
162    pub(crate) fn new(client: &'a Client) -> Self {
163        Self { client }
164    }
165
166    /// `GET /v1/organizations/api_keys/{id}`.
167    pub async fn retrieve(&self, key_id: &str) -> Result<ApiKey> {
168        let path = format!("/v1/organizations/api_keys/{key_id}");
169        self.client
170            .execute_with_retry(
171                || self.client.request_builder(reqwest::Method::GET, &path),
172                &[],
173            )
174            .await
175    }
176
177    /// `GET /v1/organizations/api_keys`.
178    pub async fn list(&self, params: ListApiKeysParams) -> Result<Paginated<ApiKey>> {
179        let query = params.to_query();
180        self.client
181            .execute_with_retry(
182                || {
183                    let mut req = self
184                        .client
185                        .request_builder(reqwest::Method::GET, "/v1/organizations/api_keys");
186                    for (k, v) in &query {
187                        req = req.query(&[(k, v)]);
188                    }
189                    req
190                },
191                &[],
192            )
193            .await
194    }
195
196    /// `POST /v1/organizations/api_keys/{id}` (update name/status).
197    pub async fn update(&self, key_id: &str, request: UpdateApiKeyRequest) -> Result<ApiKey> {
198        let path = format!("/v1/organizations/api_keys/{key_id}");
199        let body = &request;
200        self.client
201            .execute_with_retry(
202                || {
203                    self.client
204                        .request_builder(reqwest::Method::POST, &path)
205                        .json(body)
206                },
207                &[],
208            )
209            .await
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use pretty_assertions::assert_eq;
217    use serde_json::json;
218    use wiremock::matchers::{body_partial_json, method, path};
219    use wiremock::{Mock, MockServer, ResponseTemplate};
220
221    fn client_for(mock: &MockServer) -> Client {
222        Client::builder()
223            .api_key("sk-ant-admin-test")
224            .base_url(mock.uri())
225            .build()
226            .unwrap()
227    }
228
229    fn fake_api_key() -> serde_json::Value {
230        json!({
231            "id": "apikey_01",
232            "type": "api_key",
233            "name": "ci",
234            "partial_key_hint": "sk-ant-...abcd",
235            "status": "active",
236            "workspace_id": null,
237            "created_by": {"id": "user_01", "type": "user"},
238            "created_at": "2026-05-01T00:00:00Z",
239            "expires_at": null
240        })
241    }
242
243    #[tokio::test]
244    async fn retrieve_api_key_returns_typed_record() {
245        let mock = MockServer::start().await;
246        Mock::given(method("GET"))
247            .and(path("/v1/organizations/api_keys/apikey_01"))
248            .respond_with(ResponseTemplate::new(200).set_body_json(fake_api_key()))
249            .mount(&mock)
250            .await;
251        let client = client_for(&mock);
252        let k = client
253            .admin()
254            .api_keys()
255            .retrieve("apikey_01")
256            .await
257            .unwrap();
258        assert_eq!(k.id, "apikey_01");
259        assert_eq!(k.status, ApiKeyStatus::Active);
260        assert_eq!(k.created_by.ty, "user");
261    }
262
263    #[tokio::test]
264    async fn list_api_keys_filters_by_status_and_workspace() {
265        let mock = MockServer::start().await;
266        Mock::given(method("GET"))
267            .and(path("/v1/organizations/api_keys"))
268            .and(wiremock::matchers::query_param("status", "active"))
269            .and(wiremock::matchers::query_param("workspace_id", "ws_01"))
270            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
271                "data": [fake_api_key()],
272                "has_more": false,
273                "first_id": "apikey_01",
274                "last_id": "apikey_01"
275            })))
276            .mount(&mock)
277            .await;
278        let client = client_for(&mock);
279        let page = client
280            .admin()
281            .api_keys()
282            .list(ListApiKeysParams {
283                status: Some(ApiKeyStatus::Active),
284                workspace_id: Some("ws_01".into()),
285                ..Default::default()
286            })
287            .await
288            .unwrap();
289        assert_eq!(page.data.len(), 1);
290    }
291
292    #[tokio::test]
293    async fn update_api_key_can_change_name_and_status() {
294        let mock = MockServer::start().await;
295        Mock::given(method("POST"))
296            .and(path("/v1/organizations/api_keys/apikey_01"))
297            .and(body_partial_json(json!({
298                "name": "renamed",
299                "status": "archived"
300            })))
301            .respond_with(ResponseTemplate::new(200).set_body_json(fake_api_key()))
302            .mount(&mock)
303            .await;
304        let client = client_for(&mock);
305        client
306            .admin()
307            .api_keys()
308            .update(
309                "apikey_01",
310                UpdateApiKeyRequest::new()
311                    .name("renamed")
312                    .status(WriteApiKeyStatus::Archived),
313            )
314            .await
315            .unwrap();
316    }
317}