libauth_rs/vendor/api/
clerk.rs

1/*!
2 * A rust library for interacting with the Clerk Backend API.
3 *
4 * For more information, the Clerk Backend API is documented at [clerk.com/docs/reference/backend-api](https://clerk.com/docs/reference/backend-api).
5 *
6 * Example:
7 *
8 * ```ignore
9 * use clerk_api::{ClerkClient, CreateUserRequest};
10 *
11 * async fn create_user() {
12 *     // Initialize the Clerk client
13 *     let clerk = ClerkClient::new("sk_test_...".to_string());
14 *
15 *     // Create a new user
16 *     let user = clerk
17 *         .create_user(CreateUserRequest {
18 *             email_address: vec!["user@example.com".to_string()],
19 *             first_name: Some("John".to_string()),
20 *             last_name: Some("Doe".to_string()),
21 *             ..Default::default()
22 *         })
23 *         .await
24 *         .unwrap();
25 *
26 *     println!("Created user: {}", user.id);
27 * }
28 * ```
29 */
30#![allow(clippy::field_reassign_with_default)]
31
32use reqwest::{header, Method, StatusCode, Url};
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::error::Error;
36use std::fmt;
37
38/// Endpoint for the Clerk Backend API
39const ENDPOINT: &str = "https://api.clerk.com/v1/";
40
41/// Custom error type for Clerk API operations
42#[derive(Debug)]
43pub enum ClerkClientError {
44    /// HTTP request error
45    RequestError(reqwest::Error),
46    /// API returned an error status
47    ApiError { status: StatusCode, message: String },
48    /// Invalid header value
49    HeaderError(reqwest::header::InvalidHeaderValue),
50    /// Invalid URL
51    InvalidUrl(String),
52}
53
54impl fmt::Display for ClerkClientError {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Self::RequestError(e) => write!(f, "Request error: {}", e),
58            Self::ApiError { status, message } => write!(f, "API error ({}): {}", status, message),
59            Self::HeaderError(e) => write!(f, "Header error: {}", e),
60            Self::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg),
61        }
62    }
63}
64
65impl Error for ClerkClientError {}
66
67impl From<reqwest::Error> for ClerkClientError {
68    fn from(err: reqwest::Error) -> Self {
69        Self::RequestError(err)
70    }
71}
72
73impl From<reqwest::header::InvalidHeaderValue> for ClerkClientError {
74    fn from(err: reqwest::header::InvalidHeaderValue) -> Self {
75        Self::HeaderError(err)
76    }
77}
78
79pub type Result<T> = std::result::Result<T, ClerkClientError>;
80
81/// Entrypoint for interacting with the Clerk Backend API
82pub struct ClerkClient {
83    secret_key: String,
84    client: reqwest::Client,
85}
86
87/// Get the secret key from the CLERK_SECRET_KEY env variable
88pub fn secret_key_from_env() -> String {
89    std::env::var("CLERK_SECRET_KEY").unwrap_or_default()
90}
91
92impl ClerkClient {
93    /// Create a new Clerk client struct. It takes a secret key (starts with sk_)
94    pub fn new<K>(secret_key: K) -> Result<Self>
95    where
96        K: ToString,
97    {
98        let client = reqwest::Client::builder()
99            .timeout(std::time::Duration::from_secs(30))
100            .build()?;
101
102        Ok(Self {
103            secret_key: secret_key.to_string(),
104            client,
105        })
106    }
107
108    /// Create a new Clerk client struct from environment variables
109    pub fn new_from_env() -> Result<Self> {
110        ClerkClient::new(secret_key_from_env())
111    }
112
113    /// Get the currently set secret key
114    pub fn get_key(&self) -> &str {
115        &self.secret_key
116    }
117
118    async fn request<B>(
119        &self,
120        method: Method,
121        path: String,
122        body: B,
123        query: Option<Vec<(&str, String)>>,
124    ) -> Result<reqwest::Response>
125    where
126        B: Serialize,
127    {
128        let base = Url::parse(ENDPOINT).map_err(|e| {
129            ClerkClientError::InvalidUrl(e.to_string())
130        })?;
131        let url = base.join(&path).map_err(|e| {
132            ClerkClientError::InvalidUrl(e.to_string())
133        })?;
134
135        let bt = format!("Bearer {}", self.secret_key);
136        let bearer = header::HeaderValue::from_str(&bt)?;
137
138        // Set the default headers per Clerk API docs
139        let mut headers = header::HeaderMap::new();
140        headers.append(header::AUTHORIZATION, bearer);
141        headers.append(
142            header::CONTENT_TYPE,
143            header::HeaderValue::from_static("application/json"),
144        );
145        headers.append(
146            header::USER_AGENT,
147            header::HeaderValue::from_static("libauth-rs/0.1.0"),
148        );
149
150        let mut rb = self.client.request(method.clone(), url).headers(headers);
151
152        if let Some(val) = query {
153            rb = rb.query(&val);
154        }
155
156        // Add the body for POST/PATCH/PUT
157        if method != Method::GET && method != Method::DELETE {
158            rb = rb.json(&body);
159        }
160
161        let resp = rb.send().await?;
162        Ok(resp)
163    }
164
165    /// List users
166    /// GET /users
167    pub async fn list_users(&self, params: ListUsersParams) -> Result<Vec<User>> {
168        let mut query_params = vec![];
169
170        if let Some(limit) = params.limit {
171            query_params.push(("limit", limit.to_string()));
172        }
173        if let Some(offset) = params.offset {
174            query_params.push(("offset", offset.to_string()));
175        }
176        if let Some(email_address) = params.email_address {
177            query_params.push(("email_address", email_address));
178        }
179        if let Some(phone_number) = params.phone_number {
180            query_params.push(("phone_number", phone_number));
181        }
182        if let Some(username) = params.username {
183            query_params.push(("username", username));
184        }
185        if let Some(user_id) = params.user_id {
186            query_params.push(("user_id", user_id));
187        }
188        if let Some(order_by) = params.order_by {
189            query_params.push(("order_by", order_by));
190        }
191
192        let resp = self.request(Method::GET, "users".to_string(), (), Some(query_params)).await?;
193
194        if !resp.status().is_success() {
195            let status = resp.status();
196            let body = resp.text().await?;
197            return Err(ClerkClientError::ApiError { status, message: body });
198        }
199
200        let users: Vec<User> = resp.json().await?;
201        Ok(users)
202    }
203
204    /// Get a user by ID
205    /// GET /users/{user_id}
206    pub async fn get_user(&self, user_id: &str) -> Result<User> {
207        let resp = self.request(Method::GET, format!("users/{}", user_id), (), None).await?;
208
209        if !resp.status().is_success() {
210            let status = resp.status();
211            let body = resp.text().await?;
212            return Err(ClerkClientError::ApiError { status, message: body });
213        }
214
215        let user: User = resp.json().await?;
216        Ok(user)
217    }
218
219    /// Create a new user
220    /// POST /users
221    pub async fn create_user(&self, req: CreateUserRequest) -> Result<User> {
222        let resp = self.request(Method::POST, "users".to_string(), req, None).await?;
223
224        if !resp.status().is_success() {
225            let status = resp.status();
226            let body = resp.text().await?;
227            return Err(ClerkClientError::ApiError { status, message: body });
228        }
229
230        let user: User = resp.json().await?;
231        Ok(user)
232    }
233
234    /// Update a user
235    /// PATCH /users/{user_id}
236    pub async fn update_user(&self, user_id: &str, req: UpdateUserRequest) -> Result<User> {
237        let resp = self.request(Method::PATCH, format!("users/{}", user_id), req, None).await?;
238
239        if !resp.status().is_success() {
240            let status = resp.status();
241            let body = resp.text().await?;
242            return Err(ClerkClientError::ApiError { status, message: body });
243        }
244
245        let user: User = resp.json().await?;
246        Ok(user)
247    }
248
249    /// Delete a user
250    /// DELETE /users/{user_id}
251    pub async fn delete_user(&self, user_id: &str) -> Result<DeletedObject> {
252        let resp = self.request(Method::DELETE, format!("users/{}", user_id), (), None).await?;
253
254        if !resp.status().is_success() {
255            let status = resp.status();
256            let body = resp.text().await?;
257            return Err(ClerkClientError::ApiError { status, message: body });
258        }
259
260        let result: DeletedObject = resp.json().await?;
261        Ok(result)
262    }
263
264    /// List sessions
265    /// GET /sessions
266    pub async fn list_sessions(&self, params: ListSessionsParams) -> Result<Vec<Session>> {
267        let mut query_params = vec![];
268
269        if let Some(limit) = params.limit {
270            query_params.push(("limit", limit.to_string()));
271        }
272        if let Some(offset) = params.offset {
273            query_params.push(("offset", offset.to_string()));
274        }
275        if let Some(client_id) = params.client_id {
276            query_params.push(("client_id", client_id));
277        }
278        if let Some(user_id) = params.user_id {
279            query_params.push(("user_id", user_id));
280        }
281        if let Some(status) = params.status {
282            query_params.push(("status", status));
283        }
284
285        let resp = self.request(Method::GET, "sessions".to_string(), (), Some(query_params)).await?;
286
287        if !resp.status().is_success() {
288            let status = resp.status();
289            let body = resp.text().await?;
290            return Err(ClerkClientError::ApiError { status, message: body });
291        }
292
293        let sessions: Vec<Session> = resp.json().await?;
294        Ok(sessions)
295    }
296
297    /// Get a session by ID
298    /// GET /sessions/{session_id}
299    pub async fn get_session(&self, session_id: &str) -> Result<Session> {
300        let resp = self.request(Method::GET, format!("sessions/{}", session_id), (), None).await?;
301
302        if !resp.status().is_success() {
303            let status = resp.status();
304            let body = resp.text().await?;
305            return Err(ClerkClientError::ApiError { status, message: body });
306        }
307
308        let session: Session = resp.json().await?;
309        Ok(session)
310    }
311
312    /// Verify a session token
313    /// POST /sessions/{session_id}/verify
314    pub async fn verify_session(&self, session_id: &str) -> Result<Session> {
315        let resp = self.request(Method::POST, format!("sessions/{}/verify", session_id), (), None).await?;
316
317        if !resp.status().is_success() {
318            let status = resp.status();
319            let body = resp.text().await?;
320            return Err(ClerkClientError::ApiError { status, message: body });
321        }
322
323        let session: Session = resp.json().await?;
324        Ok(session)
325    }
326
327    /// Revoke a session
328    /// POST /sessions/{session_id}/revoke
329    pub async fn revoke_session(&self, session_id: &str) -> Result<Session> {
330        let resp = self.request(Method::POST, format!("sessions/{}/revoke", session_id), (), None).await?;
331
332        if !resp.status().is_success() {
333            let status = resp.status();
334            let body = resp.text().await?;
335            return Err(ClerkClientError::ApiError { status, message: body });
336        }
337
338        let session: Session = resp.json().await?;
339        Ok(session)
340    }
341
342    /// List organizations
343    /// GET /organizations
344    pub async fn list_organizations(&self, params: ListOrganizationsParams) -> Result<Vec<Organization>> {
345        let mut query_params = vec![];
346
347        if let Some(limit) = params.limit {
348            query_params.push(("limit", limit.to_string()));
349        }
350        if let Some(offset) = params.offset {
351            query_params.push(("offset", offset.to_string()));
352        }
353        if let Some(query) = params.query {
354            query_params.push(("query", query));
355        }
356
357        let resp = self.request(Method::GET, "organizations".to_string(), (), Some(query_params)).await?;
358
359        if !resp.status().is_success() {
360            let status = resp.status();
361            let body = resp.text().await?;
362            return Err(ClerkClientError::ApiError { status, message: body });
363        }
364
365        let orgs: Vec<Organization> = resp.json().await?;
366        Ok(orgs)
367    }
368
369    /// Get an organization by ID
370    /// GET /organizations/{organization_id}
371    pub async fn get_organization(&self, organization_id: &str) -> Result<Organization> {
372        let resp = self.request(Method::GET, format!("organizations/{}", organization_id), (), None).await?;
373
374        if !resp.status().is_success() {
375            let status = resp.status();
376            let body = resp.text().await?;
377            return Err(ClerkClientError::ApiError { status, message: body });
378        }
379
380        let org: Organization = resp.json().await?;
381        Ok(org)
382    }
383
384    /// Create an organization
385    /// POST /organizations
386    pub async fn create_organization(&self, req: CreateOrganizationRequest) -> Result<Organization> {
387        let resp = self.request(Method::POST, "organizations".to_string(), req, None).await?;
388
389        if !resp.status().is_success() {
390            let status = resp.status();
391            let body = resp.text().await?;
392            return Err(ClerkClientError::ApiError { status, message: body });
393        }
394
395        let org: Organization = resp.json().await?;
396        Ok(org)
397    }
398
399    /// Update an organization
400    /// PATCH /organizations/{organization_id}
401    pub async fn update_organization(
402        &self,
403        organization_id: &str,
404        req: UpdateOrganizationRequest,
405    ) -> Result<Organization> {
406        let resp = self.request(Method::PATCH, format!("organizations/{}", organization_id), req, None).await?;
407
408        if !resp.status().is_success() {
409            let status = resp.status();
410            let body = resp.text().await?;
411            return Err(ClerkClientError::ApiError { status, message: body });
412        }
413
414        let org: Organization = resp.json().await?;
415        Ok(org)
416    }
417
418    /// Delete an organization
419    /// DELETE /organizations/{organization_id}
420    pub async fn delete_organization(&self, organization_id: &str) -> Result<DeletedObject> {
421        let resp = self.request(Method::DELETE, format!("organizations/{}", organization_id), (), None).await?;
422
423        if !resp.status().is_success() {
424            let status = resp.status();
425            let body = resp.text().await?;
426            return Err(ClerkClientError::ApiError { status, message: body });
427        }
428
429        let result: DeletedObject = resp.json().await?;
430        Ok(result)
431    }
432}
433
434// ============================================================================
435// User Types
436// ============================================================================
437
438/// Parameters for listing users
439#[derive(Debug, Default, Clone)]
440pub struct ListUsersParams {
441    pub limit: Option<u32>,
442    pub offset: Option<u32>,
443    pub email_address: Option<String>,
444    pub phone_number: Option<String>,
445    pub username: Option<String>,
446    pub user_id: Option<String>,
447    pub order_by: Option<String>,
448}
449
450/// Request to create a new user
451#[derive(Debug, Default, Clone, Serialize, Deserialize)]
452pub struct CreateUserRequest {
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub email_address: Option<Vec<String>>,
455    #[serde(skip_serializing_if = "Option::is_none")]
456    pub phone_number: Option<Vec<String>>,
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub username: Option<String>,
459    #[serde(skip_serializing_if = "Option::is_none")]
460    pub password: Option<String>,
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub first_name: Option<String>,
463    #[serde(skip_serializing_if = "Option::is_none")]
464    pub last_name: Option<String>,
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub external_id: Option<String>,
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub public_metadata: Option<HashMap<String, serde_json::Value>>,
469    #[serde(skip_serializing_if = "Option::is_none")]
470    pub private_metadata: Option<HashMap<String, serde_json::Value>>,
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub unsafe_metadata: Option<HashMap<String, serde_json::Value>>,
473}
474
475/// Request to update a user
476#[derive(Debug, Default, Clone, Serialize, Deserialize)]
477pub struct UpdateUserRequest {
478    #[serde(skip_serializing_if = "Option::is_none")]
479    pub first_name: Option<String>,
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub last_name: Option<String>,
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub username: Option<String>,
484    #[serde(skip_serializing_if = "Option::is_none")]
485    pub primary_email_address_id: Option<String>,
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub primary_phone_number_id: Option<String>,
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub public_metadata: Option<HashMap<String, serde_json::Value>>,
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub private_metadata: Option<HashMap<String, serde_json::Value>>,
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub unsafe_metadata: Option<HashMap<String, serde_json::Value>>,
494}
495
496/// A Clerk user object
497#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct User {
499    pub id: String,
500    #[serde(default, skip_serializing_if = "Option::is_none")]
501    pub object: Option<String>,
502    #[serde(default, skip_serializing_if = "Option::is_none")]
503    pub username: Option<String>,
504    #[serde(default, skip_serializing_if = "Option::is_none")]
505    pub first_name: Option<String>,
506    #[serde(default, skip_serializing_if = "Option::is_none")]
507    pub last_name: Option<String>,
508    #[serde(default, skip_serializing_if = "Option::is_none")]
509    pub image_url: Option<String>,
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    pub has_image: Option<bool>,
512    #[serde(default, skip_serializing_if = "Option::is_none")]
513    pub primary_email_address_id: Option<String>,
514    #[serde(default, skip_serializing_if = "Option::is_none")]
515    pub primary_phone_number_id: Option<String>,
516    #[serde(default, skip_serializing_if = "Option::is_none")]
517    pub primary_web3_wallet_id: Option<String>,
518    #[serde(default)]
519    pub email_addresses: Vec<EmailAddress>,
520    #[serde(default)]
521    pub phone_numbers: Vec<PhoneNumber>,
522    #[serde(default)]
523    pub web3_wallets: Vec<Web3Wallet>,
524    #[serde(default, skip_serializing_if = "Option::is_none")]
525    pub external_id: Option<String>,
526    #[serde(default)]
527    pub public_metadata: HashMap<String, serde_json::Value>,
528    #[serde(default)]
529    pub private_metadata: HashMap<String, serde_json::Value>,
530    #[serde(default)]
531    pub unsafe_metadata: HashMap<String, serde_json::Value>,
532    pub created_at: i64,
533    pub updated_at: i64,
534    #[serde(default, skip_serializing_if = "Option::is_none")]
535    pub last_sign_in_at: Option<i64>,
536    #[serde(default, skip_serializing_if = "Option::is_none")]
537    pub banned: Option<bool>,
538    #[serde(default, skip_serializing_if = "Option::is_none")]
539    pub locked: Option<bool>,
540}
541
542/// Email address object
543#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct EmailAddress {
545    pub id: String,
546    #[serde(default, skip_serializing_if = "Option::is_none")]
547    pub object: Option<String>,
548    pub email_address: String,
549    #[serde(default, skip_serializing_if = "Option::is_none")]
550    pub verification: Option<Verification>,
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    pub linked_to: Option<Vec<LinkedIdentity>>,
553}
554
555/// Phone number object
556#[derive(Debug, Clone, Serialize, Deserialize)]
557pub struct PhoneNumber {
558    pub id: String,
559    #[serde(default, skip_serializing_if = "Option::is_none")]
560    pub object: Option<String>,
561    pub phone_number: String,
562    #[serde(default, skip_serializing_if = "Option::is_none")]
563    pub verification: Option<Verification>,
564    #[serde(default, skip_serializing_if = "Option::is_none")]
565    pub linked_to: Option<Vec<LinkedIdentity>>,
566}
567
568/// Web3 wallet object
569#[derive(Debug, Clone, Serialize, Deserialize)]
570pub struct Web3Wallet {
571    pub id: String,
572    #[serde(default, skip_serializing_if = "Option::is_none")]
573    pub object: Option<String>,
574    pub web3_wallet: String,
575    #[serde(default, skip_serializing_if = "Option::is_none")]
576    pub verification: Option<Verification>,
577}
578
579/// Verification status
580#[derive(Debug, Clone, Serialize, Deserialize)]
581pub struct Verification {
582    pub status: String,
583    #[serde(default, skip_serializing_if = "Option::is_none")]
584    pub strategy: Option<String>,
585    #[serde(default, skip_serializing_if = "Option::is_none")]
586    pub attempts: Option<i32>,
587    #[serde(default, skip_serializing_if = "Option::is_none")]
588    pub expire_at: Option<i64>,
589}
590
591/// Linked identity
592#[derive(Debug, Clone, Serialize, Deserialize)]
593pub struct LinkedIdentity {
594    pub id: String,
595    #[serde(rename = "type")]
596    pub identity_type: String,
597}
598
599// ============================================================================
600// Session Types
601// ============================================================================
602
603/// Parameters for listing sessions
604#[derive(Debug, Default, Clone)]
605pub struct ListSessionsParams {
606    pub limit: Option<u32>,
607    pub offset: Option<u32>,
608    pub client_id: Option<String>,
609    pub user_id: Option<String>,
610    pub status: Option<String>,
611}
612
613/// A Clerk session object
614#[derive(Debug, Clone, Serialize, Deserialize)]
615pub struct Session {
616    pub id: String,
617    #[serde(default, skip_serializing_if = "Option::is_none")]
618    pub object: Option<String>,
619    pub client_id: String,
620    pub user_id: String,
621    pub status: String,
622    #[serde(default, skip_serializing_if = "Option::is_none")]
623    pub last_active_at: Option<i64>,
624    #[serde(default, skip_serializing_if = "Option::is_none")]
625    pub expire_at: Option<i64>,
626    #[serde(default, skip_serializing_if = "Option::is_none")]
627    pub abandon_at: Option<i64>,
628    pub created_at: i64,
629    pub updated_at: i64,
630}
631
632// ============================================================================
633// Organization Types
634// ============================================================================
635
636/// Parameters for listing organizations
637#[derive(Debug, Default, Clone)]
638pub struct ListOrganizationsParams {
639    pub limit: Option<u32>,
640    pub offset: Option<u32>,
641    pub query: Option<String>,
642}
643
644/// Request to create an organization
645#[derive(Debug, Default, Clone, Serialize, Deserialize)]
646pub struct CreateOrganizationRequest {
647    pub name: String,
648    #[serde(skip_serializing_if = "Option::is_none")]
649    pub slug: Option<String>,
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub created_by: Option<String>,
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub public_metadata: Option<HashMap<String, serde_json::Value>>,
654    #[serde(skip_serializing_if = "Option::is_none")]
655    pub private_metadata: Option<HashMap<String, serde_json::Value>>,
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub max_allowed_memberships: Option<i32>,
658}
659
660/// Request to update an organization
661#[derive(Debug, Default, Clone, Serialize, Deserialize)]
662pub struct UpdateOrganizationRequest {
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub name: Option<String>,
665    #[serde(skip_serializing_if = "Option::is_none")]
666    pub slug: Option<String>,
667    #[serde(skip_serializing_if = "Option::is_none")]
668    pub public_metadata: Option<HashMap<String, serde_json::Value>>,
669    #[serde(skip_serializing_if = "Option::is_none")]
670    pub private_metadata: Option<HashMap<String, serde_json::Value>>,
671    #[serde(skip_serializing_if = "Option::is_none")]
672    pub max_allowed_memberships: Option<i32>,
673}
674
675/// A Clerk organization object
676#[derive(Debug, Clone, Serialize, Deserialize)]
677pub struct Organization {
678    pub id: String,
679    #[serde(default, skip_serializing_if = "Option::is_none")]
680    pub object: Option<String>,
681    pub name: String,
682    pub slug: String,
683    #[serde(default, skip_serializing_if = "Option::is_none")]
684    pub image_url: Option<String>,
685    #[serde(default, skip_serializing_if = "Option::is_none")]
686    pub has_image: Option<bool>,
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub members_count: Option<i32>,
689    #[serde(default, skip_serializing_if = "Option::is_none")]
690    pub pending_invitations_count: Option<i32>,
691    #[serde(default, skip_serializing_if = "Option::is_none")]
692    pub max_allowed_memberships: Option<i32>,
693    #[serde(default)]
694    pub public_metadata: HashMap<String, serde_json::Value>,
695    #[serde(default)]
696    pub private_metadata: HashMap<String, serde_json::Value>,
697    pub created_at: i64,
698    pub updated_at: i64,
699}
700
701// ============================================================================
702// Common Types
703// ============================================================================
704
705/// Response when an object is deleted
706#[derive(Debug, Clone, Serialize, Deserialize)]
707pub struct DeletedObject {
708    #[serde(default, skip_serializing_if = "Option::is_none")]
709    pub object: Option<String>,
710    pub id: String,
711    #[serde(default, skip_serializing_if = "Option::is_none")]
712    pub deleted: Option<bool>,
713}
714
715/// Error response from Clerk API
716#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct ClerkError {
718    #[serde(default)]
719    pub errors: Vec<ClerkErrorDetail>,
720    #[serde(default, skip_serializing_if = "Option::is_none")]
721    pub clerk_trace_id: Option<String>,
722}
723
724/// Individual error detail
725#[derive(Debug, Clone, Serialize, Deserialize)]
726pub struct ClerkErrorDetail {
727    pub message: String,
728    #[serde(default, skip_serializing_if = "Option::is_none")]
729    pub long_message: Option<String>,
730    pub code: String,
731    #[serde(default, skip_serializing_if = "Option::is_none")]
732    pub meta: Option<HashMap<String, serde_json::Value>>,
733}