Skip to main content

claude_api/admin/
invites.rs

1//! Invites: create / retrieve / list / delete.
2
3use serde::{Deserialize, Serialize};
4
5use crate::client::Client;
6use crate::error::Result;
7use crate::pagination::Paginated;
8
9use super::{InviteStatus, ListParams, OrganizationRole, WriteOrganizationRole};
10
11/// A pending or completed invite.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13#[non_exhaustive]
14pub struct Invite {
15    /// Stable invite ID.
16    pub id: String,
17    /// Wire type tag (`"invite"`).
18    #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
19    pub ty: Option<String>,
20    /// Email of the invited user.
21    pub email: String,
22    /// Role assigned on accept.
23    pub role: OrganizationRole,
24    /// Lifecycle status.
25    pub status: InviteStatus,
26    /// RFC3339 timestamp when the invite was created.
27    pub invited_at: String,
28    /// RFC3339 timestamp when the invite expires.
29    pub expires_at: String,
30}
31
32/// Response shape for `DELETE /v1/organizations/invites/{id}`.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[non_exhaustive]
35pub struct InviteDeleted {
36    /// ID of the deleted invite.
37    pub id: String,
38    /// Wire type tag (`"invite_deleted"`).
39    #[serde(rename = "type")]
40    pub ty: String,
41}
42
43/// Request body for `POST /v1/organizations/invites`.
44#[derive(Debug, Clone, Serialize)]
45#[non_exhaustive]
46pub struct CreateInviteRequest {
47    /// Email to invite.
48    pub email: String,
49    /// Role to assign on accept. `admin` is not a valid value.
50    pub role: WriteOrganizationRole,
51}
52
53impl CreateInviteRequest {
54    /// Build a request.
55    #[must_use]
56    pub fn new(email: impl Into<String>, role: WriteOrganizationRole) -> Self {
57        Self {
58            email: email.into(),
59            role,
60        }
61    }
62}
63
64/// Namespace handle for invite endpoints.
65pub struct Invites<'a> {
66    client: &'a Client,
67}
68
69impl<'a> Invites<'a> {
70    pub(crate) fn new(client: &'a Client) -> Self {
71        Self { client }
72    }
73
74    /// `POST /v1/organizations/invites`.
75    pub async fn create(&self, request: CreateInviteRequest) -> Result<Invite> {
76        let body = &request;
77        self.client
78            .execute_with_retry(
79                || {
80                    self.client
81                        .request_builder(reqwest::Method::POST, "/v1/organizations/invites")
82                        .json(body)
83                },
84                &[],
85            )
86            .await
87    }
88
89    /// `GET /v1/organizations/invites/{id}`.
90    pub async fn retrieve(&self, invite_id: &str) -> Result<Invite> {
91        let path = format!("/v1/organizations/invites/{invite_id}");
92        self.client
93            .execute_with_retry(
94                || self.client.request_builder(reqwest::Method::GET, &path),
95                &[],
96            )
97            .await
98    }
99
100    /// `GET /v1/organizations/invites`.
101    pub async fn list(&self, params: ListParams) -> Result<Paginated<Invite>> {
102        let query = params.to_query();
103        self.client
104            .execute_with_retry(
105                || {
106                    let mut req = self
107                        .client
108                        .request_builder(reqwest::Method::GET, "/v1/organizations/invites");
109                    for (k, v) in &query {
110                        req = req.query(&[(k, v)]);
111                    }
112                    req
113                },
114                &[],
115            )
116            .await
117    }
118
119    /// `DELETE /v1/organizations/invites/{id}`.
120    pub async fn delete(&self, invite_id: &str) -> Result<InviteDeleted> {
121        let path = format!("/v1/organizations/invites/{invite_id}");
122        self.client
123            .execute_with_retry(
124                || self.client.request_builder(reqwest::Method::DELETE, &path),
125                &[],
126            )
127            .await
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use pretty_assertions::assert_eq;
135    use serde_json::json;
136    use wiremock::matchers::{body_partial_json, method, path};
137    use wiremock::{Mock, MockServer, ResponseTemplate};
138
139    fn client_for(mock: &MockServer) -> Client {
140        Client::builder()
141            .api_key("sk-ant-admin-test")
142            .base_url(mock.uri())
143            .build()
144            .unwrap()
145    }
146
147    fn fake_invite() -> serde_json::Value {
148        json!({
149            "id": "invite_01",
150            "type": "invite",
151            "email": "u@example.com",
152            "role": "user",
153            "status": "pending",
154            "invited_at": "2026-05-01T00:00:00Z",
155            "expires_at": "2026-05-08T00:00:00Z"
156        })
157    }
158
159    #[test]
160    fn organization_role_unknown_falls_through_to_other() {
161        let raw = json!("future_role");
162        let r: OrganizationRole = serde_json::from_value(raw).unwrap();
163        assert_eq!(r, OrganizationRole::Other("future_role".into()));
164    }
165
166    #[test]
167    fn organization_role_round_trips_known_variants() {
168        for v in ["user", "developer", "billing", "admin", "claude_code_user"] {
169            let r: OrganizationRole = serde_json::from_value(json!(v)).unwrap();
170            let s = serde_json::to_value(&r).unwrap();
171            assert_eq!(s, json!(v));
172        }
173    }
174
175    #[tokio::test]
176    async fn create_invite_posts_email_and_role() {
177        let mock = MockServer::start().await;
178        Mock::given(method("POST"))
179            .and(path("/v1/organizations/invites"))
180            .and(body_partial_json(json!({
181                "email": "u@example.com",
182                "role": "developer"
183            })))
184            .respond_with(ResponseTemplate::new(200).set_body_json(fake_invite()))
185            .mount(&mock)
186            .await;
187        let client = client_for(&mock);
188        let inv = client
189            .admin()
190            .invites()
191            .create(CreateInviteRequest::new(
192                "u@example.com",
193                WriteOrganizationRole::Developer,
194            ))
195            .await
196            .unwrap();
197        assert_eq!(inv.id, "invite_01");
198    }
199
200    #[tokio::test]
201    async fn retrieve_invite_returns_typed_record() {
202        let mock = MockServer::start().await;
203        Mock::given(method("GET"))
204            .and(path("/v1/organizations/invites/invite_01"))
205            .respond_with(ResponseTemplate::new(200).set_body_json(fake_invite()))
206            .mount(&mock)
207            .await;
208        let client = client_for(&mock);
209        let inv = client
210            .admin()
211            .invites()
212            .retrieve("invite_01")
213            .await
214            .unwrap();
215        assert_eq!(inv.email, "u@example.com");
216    }
217
218    #[tokio::test]
219    async fn list_invites_passes_pagination_query() {
220        let mock = MockServer::start().await;
221        Mock::given(method("GET"))
222            .and(path("/v1/organizations/invites"))
223            .and(wiremock::matchers::query_param("limit", "50"))
224            .and(wiremock::matchers::query_param("after_id", "invite_x"))
225            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
226                "data": [fake_invite()],
227                "has_more": false,
228                "first_id": "invite_01",
229                "last_id": "invite_01"
230            })))
231            .mount(&mock)
232            .await;
233        let client = client_for(&mock);
234        let page = client
235            .admin()
236            .invites()
237            .list(ListParams {
238                after_id: Some("invite_x".into()),
239                limit: Some(50),
240                ..Default::default()
241            })
242            .await
243            .unwrap();
244        assert_eq!(page.data.len(), 1);
245    }
246
247    #[tokio::test]
248    async fn delete_invite_returns_deleted_response() {
249        let mock = MockServer::start().await;
250        Mock::given(method("DELETE"))
251            .and(path("/v1/organizations/invites/invite_01"))
252            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
253                "id": "invite_01",
254                "type": "invite_deleted"
255            })))
256            .mount(&mock)
257            .await;
258        let client = client_for(&mock);
259        let r = client.admin().invites().delete("invite_01").await.unwrap();
260        assert_eq!(r.ty, "invite_deleted");
261    }
262}