Skip to main content

uarp_sdk/generated/api/
public.rs

1// Code generated by @uarp/codegen from spec/openapi.json. DO NOT EDIT.
2//!
3//! Unauthenticated public-facing endpoints
4
5#![allow(unused_imports, clippy::too_many_arguments)]
6
7use reqwest::Method;
8use serde::{Deserialize, Serialize};
9use futures_core::Stream;
10
11use crate::client::{Client, Request, NO_BODY, NO_QUERY};
12use crate::error::Result;
13use crate::generated::models;
14use crate::multipart::{field_text, FilePart};
15use crate::pagination::CursorGuard;
16use crate::sse::EventStream;
17use crate::util::encode_path;
18
19/// Query and header parameters for `getAndroidTestingStatus`.
20#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
21pub struct GetAndroidTestingStatusParams {
22    pub email: String,
23}
24
25/// Query and header parameters for `getLinkPreview`.
26#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
27pub struct GetLinkPreviewParams {
28    pub url: String,
29}
30
31/// Query and header parameters for `getLinkPreviewImage`.
32#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
33pub struct GetLinkPreviewImageParams {
34    pub url: String,
35}
36
37/// Query and header parameters for `listPublicBlogPosts`.
38#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
39pub struct ListPublicBlogPostsParams {
40    /// Exact tag match, case-insensitive.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub tag: Option<String>,
43    /// Free-text search across title, body and tags.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub q: Option<String>,
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub page: Option<i64>,
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub limit: Option<i64>,
50}
51
52/// Query and header parameters for `listPublicTenants`.
53#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
54pub struct ListPublicTenantsParams {
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub category: Option<String>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub sort: Option<String>,
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub search: Option<String>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub limit: Option<i64>,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub cursor: Option<String>,
65}
66
67/// Query and header parameters for `publicDomainLookup`.
68#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
69pub struct PublicDomainLookupParams {
70    pub domain: String,
71}
72
73/// Unauthenticated public-facing endpoints
74#[derive(Debug, Clone)]
75pub struct PublicApi {
76    pub(crate) client: Client,
77}
78
79impl Client {
80    /// Unauthenticated public-facing endpoints
81    pub fn public(&self) -> PublicApi {
82        PublicApi { client: self.clone() }
83    }
84}
85
86impl PublicApi {
87    /// Cancel a run of this chat
88    ///
89    /// Cancels a run that belongs to this public session. No body. A run id from outside the
90    /// session is 404; a run of this session that was not started publicly is 409. Forms measured
91    /// through the router with a seeded session (public-served-forms_test.ts, 2026-09-10).
92    ///
93    /// `POST /api/v1/public/sessions/{sessionId}/runs/{runId}/cancel`
94    pub async fn cancel_public_session_run(&self, session_id: &str, run_id: &str) -> Result<models::CancelPublicSessionRunResponse> {
95        self.client
96            .request_json(Request {
97                method: Method::POST,
98                path: format!("/api/v1/public/sessions/{}/runs/{}/cancel", encode_path(session_id), encode_path(run_id)),
99                query: NO_QUERY,
100                body: NO_BODY,
101                headers: Vec::new(),
102                idempotent: true,
103            })
104            .await
105    }
106
107    /// Create public session
108    ///
109    /// `POST /api/v1/public/sessions`
110    pub async fn create_public_session(&self, body: &models::CreatePublicSessionRequest) -> Result<models::CreatePublicSessionResponse> {
111        self.client
112            .request_json(Request {
113                method: Method::POST,
114                path: "/api/v1/public/sessions".to_string(),
115                query: NO_QUERY,
116                body: Some(body),
117                headers: Vec::new(),
118                idempotent: true,
119            })
120            .await
121    }
122
123    /// Report abusive content from a public chat session
124    ///
125    /// Anonymous abuse report filed by a public-chat visitor. Requires no authentication of any
126    /// kind — the session id in the path is the scope, and it must resolve in the public-session
127    /// index. Rate limited per session (5/hour) and per IP (10/hour). The report lands in the
128    /// moderation queue of the tenant that owns the agent and raises an in-product notification
129    /// there.
130    ///
131    /// `POST /api/v1/public/sessions/{sessionId}/reports`
132    pub async fn create_public_session_report(&self, session_id: &str, body: &models::ContentReportInput) -> Result<models::ContentReportAccepted> {
133        self.client
134            .request_json(Request {
135                method: Method::POST,
136                path: format!("/api/v1/public/sessions/{}/reports", encode_path(session_id)),
137                query: NO_QUERY,
138                body: Some(body),
139                headers: Vec::new(),
140                idempotent: true,
141            })
142            .await
143    }
144
145    /// Has THIS browser already signed up?
146    ///
147    /// Deliberately not an address oracle. The answer is `registered: true` only when the caller
148    /// carries the sign-up cookie this browser was given AND it matches the address asked about;
149    /// any other address, or the same address from a browser that did not sign up, answers
150    /// `registered: false`. So the route cannot be used to test whether an address is on the
151    /// roster.
152    ///
153    /// `GET /api/v1/public/testing/android/status`
154    pub async fn get_android_testing_status(&self, params: &GetAndroidTestingStatusParams) -> Result<models::GetAndroidTestingStatusResponse> {
155        self.client
156            .request_json(Request {
157                method: Method::GET,
158                path: "/api/v1/public/testing/android/status".to_string(),
159                query: Some(params),
160                body: NO_BODY,
161                headers: Vec::new(),
162                idempotent: false,
163            })
164            .await
165    }
166
167    /// Public landing overrides
168    ///
169    /// No authentication. Text overrides and partner logos for the landing page.
170    ///
171    /// `GET /api/v1/public/landing/overrides`
172    pub async fn get_landing_overrides(&self) -> Result<models::LandingOverrides> {
173        self.client
174            .request_json(Request {
175                method: Method::GET,
176                path: "/api/v1/public/landing/overrides".to_string(),
177                query: NO_QUERY,
178                body: NO_BODY,
179                headers: Vec::new(),
180                idempotent: false,
181            })
182            .await
183    }
184
185    /// Unfurl a cited link into card metadata
186    ///
187    /// Open Graph metadata for a URL an agent cited, so the client renders a card instead of a bare
188    /// link. SSRF-guarded: a private or loopback address is refused with 403 rather than fetched.
189    /// Every member of `preview` except `url` and `site` may be null.
190    ///
191    /// `GET /api/v1/public/link-preview`
192    pub async fn get_link_preview(&self, params: &GetLinkPreviewParams) -> Result<models::GetLinkPreviewResponse> {
193        self.client
194            .request_json(Request {
195                method: Method::GET,
196                path: "/api/v1/public/link-preview".to_string(),
197                query: Some(params),
198                body: NO_BODY,
199                headers: Vec::new(),
200                idempotent: false,
201            })
202            .await
203    }
204
205    /// Proxy the og:image
206    ///
207    /// Serves the preview image through this origin so the visitor never connects to the
208    /// third-party host. Answers the image bytes with the upstream content type, `Cache-Control:
209    /// public, max-age=86400, immutable`, `nosniff` and a `default-src 'none'` CSP. A target that
210    /// is not an image, or that the fetch could not complete, is 404 rather than a broken picture;
211    /// one over the size cap is 413, including when the responder simply had more to send — half an
212    /// image renders as our bug rather than their oversized file.
213    ///
214    /// `GET /api/v1/public/link-preview/image`
215    pub async fn get_link_preview_image(&self, params: &GetLinkPreviewImageParams) -> Result<bytes::Bytes> {
216        self.client
217            .request_bytes(Request {
218                method: Method::GET,
219                path: "/api/v1/public/link-preview/image".to_string(),
220                query: Some(params),
221                body: NO_BODY,
222                headers: Vec::new(),
223                idempotent: false,
224            })
225            .await
226    }
227
228    /// Maintenance state
229    ///
230    /// No authentication, by design: during maintenance the authenticated surface is exactly what a
231    /// client cannot reach, so asking “is it me or is it you” must not itself require a session.
232    ///
233    /// A read failure answers `enabled: false` — the platform is assumed open unless it is known to
234    /// be closed.
235    ///
236    /// `GET /api/v1/maintenance/status`
237    pub async fn get_maintenance_status(&self) -> Result<models::MaintenanceStatus> {
238        self.client
239            .request_json(Request {
240                method: Method::GET,
241                path: "/api/v1/maintenance/status".to_string(),
242                query: NO_QUERY,
243                body: NO_BODY,
244                headers: Vec::new(),
245                idempotent: false,
246            })
247            .await
248    }
249
250    /// Public deployment info
251    ///
252    /// No authentication. Contact addresses and the public base URL as the operator configured
253    /// them, plus enough setup state to render a “being set up” banner.
254    ///
255    /// `GET /api/v1/public/platform-info`
256    pub async fn get_platform_info(&self) -> Result<models::PlatformInfo> {
257        self.client
258            .request_json(Request {
259                method: Method::GET,
260                path: "/api/v1/public/platform-info".to_string(),
261                query: NO_QUERY,
262                body: NO_BODY,
263                headers: Vec::new(),
264                idempotent: false,
265            })
266            .await
267    }
268
269    /// Get public agent card
270    ///
271    /// `GET /api/v1/public/agents/{agentId}`
272    pub async fn get_public_agent_card(&self, agent_id: &str) -> Result<serde_json::Map<String, serde_json::Value>> {
273        self.client
274            .request_json(Request {
275                method: Method::GET,
276                path: format!("/api/v1/public/agents/{}", encode_path(agent_id)),
277                query: NO_QUERY,
278                body: NO_BODY,
279                headers: Vec::new(),
280                idempotent: false,
281            })
282            .await
283    }
284
285    /// One published post, with its body
286    ///
287    /// A post that exists but is not published is 404, the same as one that does not exist — a
288    /// draft must not be discoverable by its status.
289    ///
290    /// `GET /api/v1/public/blog/posts/{slug}`
291    pub async fn get_public_blog_post(&self, slug: &str) -> Result<models::GetPublicBlogPostResponse> {
292        self.client
293            .request_json(Request {
294                method: Method::GET,
295                path: format!("/api/v1/public/blog/posts/{}", encode_path(slug)),
296                query: NO_QUERY,
297                body: NO_BODY,
298                headers: Vec::new(),
299                idempotent: false,
300            })
301            .await
302    }
303
304    /// RSS 2.0 feed of published posts
305    ///
306    /// `/api/v1/public/blog/rss.xml` is the same feed under the extension readers expect; both
307    /// paths answer identically.
308    ///
309    /// `GET /api/v1/public/blog/rss`
310    pub async fn get_public_blog_rss(&self) -> Result<String> {
311        self.client
312            .request_text(Request {
313                method: Method::GET,
314                path: "/api/v1/public/blog/rss".to_string(),
315                query: NO_QUERY,
316                body: NO_BODY,
317                headers: Vec::new(),
318                idempotent: false,
319            })
320            .await
321    }
322
323    /// Get the public featured agent for the landing-page hero
324    ///
325    /// `GET /api/v1/public/landing/featured-agent`
326    pub async fn get_public_featured_agent(&self) -> Result<models::GetPublicFeaturedAgentResponse> {
327        self.client
328            .request_json(Request {
329                method: Method::GET,
330                path: "/api/v1/public/landing/featured-agent".to_string(),
331                query: NO_QUERY,
332                body: NO_BODY,
333                headers: Vec::new(),
334                idempotent: false,
335            })
336            .await
337    }
338
339    /// Get public file content
340    ///
341    /// `GET /api/v1/public/files/{fileId}/content`
342    pub async fn get_public_file_content(&self, file_id: &str) -> Result<bytes::Bytes> {
343        self.client
344            .request_bytes(Request {
345                method: Method::GET,
346                path: format!("/api/v1/public/files/{}/content", encode_path(file_id)),
347                query: NO_QUERY,
348                body: NO_BODY,
349                headers: Vec::new(),
350                idempotent: false,
351            })
352            .await
353    }
354
355    /// Get public session
356    ///
357    /// `GET /api/v1/public/sessions/{sessionId}`
358    pub async fn get_public_session(&self, session_id: &str) -> Result<serde_json::Map<String, serde_json::Value>> {
359        self.client
360            .request_json(Request {
361                method: Method::GET,
362                path: format!("/api/v1/public/sessions/{}", encode_path(session_id)),
363                query: NO_QUERY,
364                body: NO_BODY,
365                headers: Vec::new(),
366                idempotent: false,
367            })
368            .await
369    }
370
371    /// Get public state detail
372    ///
373    /// `GET /api/v1/public/states/{stateId}`
374    pub async fn get_public_state(&self, state_id: &str) -> Result<models::PublicState> {
375        self.client
376            .request_json(Request {
377                method: Method::GET,
378                path: format!("/api/v1/public/states/{}", encode_path(state_id)),
379                query: NO_QUERY,
380                body: NO_BODY,
381                headers: Vec::new(),
382                idempotent: false,
383            })
384            .await
385    }
386
387    /// Get public tenant profile
388    ///
389    /// `GET /api/v1/public/tenants/{slug}`
390    pub async fn get_public_tenant_profile(&self, slug: &str) -> Result<models::PublicTenant> {
391        self.client
392            .request_json(Request {
393                method: Method::GET,
394                path: format!("/api/v1/public/tenants/{}", encode_path(slug)),
395                query: NO_QUERY,
396                body: NO_BODY,
397                headers: Vec::new(),
398                idempotent: false,
399            })
400            .await
401    }
402
403    /// Tenant custom CSS stylesheet
404    ///
405    /// Serves tenant-configured branding CSS as `text/css`. Cached with ETag; safe for `\<link
406    /// rel=stylesheet\>`.
407    ///
408    /// `GET /api/v1/public/tenants/{slug}/style.css`
409    pub async fn get_public_tenant_stylesheet(&self, slug: &str) -> Result<String> {
410        self.client
411            .request_text(Request {
412                method: Method::GET,
413                path: format!("/api/v1/public/tenants/{}/style.css", encode_path(slug)),
414                query: NO_QUERY,
415                body: NO_BODY,
416                headers: Vec::new(),
417                idempotent: false,
418            })
419            .await
420    }
421
422    /// Is sign-up open
423    ///
424    /// No authentication; cached for 30 seconds.
425    ///
426    /// **Fails open.** If the setting cannot be read the answer is `registration_open: true`,
427    /// because the worst case of guessing open is a missing notice, while guessing closed would
428    /// turn a storage blip into a closed front door. A client cannot distinguish the two — this
429    /// endpoint is the state, not a health check.
430    ///
431    /// `GET /api/v1/public/registration-status`
432    pub async fn get_registration_status(&self) -> Result<models::GetRegistrationStatusResponse> {
433        self.client
434            .request_json(Request {
435                method: Method::GET,
436                path: "/api/v1/public/registration-status".to_string(),
437                query: NO_QUERY,
438                body: NO_BODY,
439                headers: Vec::new(),
440                idempotent: false,
441            })
442            .await
443    }
444
445    /// List published blog posts
446    ///
447    /// Published posts only, newest first, paged. `all_tags` is the distinct tag set across every
448    /// published post — the whole set, not just this page — so a filter UI can be built from one
449    /// call. `excerpt` is the body with its leading heading and markdown punctuation stripped, cut
450    /// to 240 characters. `total` and `total_pages` count posts AFTER `tag` and `q` are applied.
451    ///
452    /// `GET /api/v1/public/blog`
453    pub async fn list_public_blog_posts(&self, params: &ListPublicBlogPostsParams) -> Result<models::ListPublicBlogPostsResponse> {
454        self.client
455            .request_json(Request {
456                method: Method::GET,
457                path: "/api/v1/public/blog".to_string(),
458                query: Some(params),
459                body: NO_BODY,
460                headers: Vec::new(),
461                idempotent: false,
462            })
463            .await
464    }
465
466    /// List the integrations a visitor could connect
467    ///
468    /// The connectors the platform offers today: the registry MINUS anything an admin has switched
469    /// off, which is the same answer `GET /integrations/catalog` gives a signed-in tenant — both
470    /// call one function, so a page rendered from this cannot advertise what the product refuses.
471    ///
472    /// It exists because a hand-kept list drifted: a marketing page counted the connector registry
473    /// and said twenty, naming three integrations that are not in the catalogue at all, while a
474    /// tenant was served seven. A number a page keeps by hand is a number that can be wrong; this
475    /// one cannot.
476    ///
477    /// Deliberately thinner than the tenant catalogue — no `config_schema`, because a visitor
478    /// deciding whether to sign up does not need to know which credential fields a connector wants.
479    /// Anonymous, and it says nothing about any tenant.
480    ///
481    /// `GET /api/v1/public/integrations`
482    pub async fn list_public_integrations(&self) -> Result<models::ListPublicIntegrationsResponse> {
483        self.client
484            .request_json(Request {
485                method: Method::GET,
486                path: "/api/v1/public/integrations".to_string(),
487                query: NO_QUERY,
488                body: NO_BODY,
489                headers: Vec::new(),
490                idempotent: false,
491            })
492            .await
493    }
494
495    /// List public plans
496    ///
497    /// `GET /api/v1/public/plans`
498    pub async fn list_public_plans(&self) -> Result<models::ListPublicPlansResponse> {
499        self.client
500            .request_json(Request {
501                method: Method::GET,
502                path: "/api/v1/public/plans".to_string(),
503                query: NO_QUERY,
504                body: NO_BODY,
505                headers: Vec::new(),
506                idempotent: false,
507            })
508            .await
509    }
510
511    /// List public states
512    ///
513    /// `GET /api/v1/public/states`
514    pub async fn list_public_states(&self) -> Result<models::ListPublicStatesResponse> {
515        self.client
516            .request_json(Request {
517                method: Method::GET,
518                path: "/api/v1/public/states".to_string(),
519                query: NO_QUERY,
520                body: NO_BODY,
521                headers: Vec::new(),
522                idempotent: false,
523            })
524            .await
525    }
526
527    /// List public tenants
528    ///
529    /// `GET /api/v1/public/tenants`
530    pub async fn list_public_tenants(&self, params: &ListPublicTenantsParams) -> Result<models::ListPublicTenantsResponse> {
531        self.client
532            .request_json(Request {
533                method: Method::GET,
534                path: "/api/v1/public/tenants".to_string(),
535                query: Some(params),
536                body: NO_BODY,
537                headers: Vec::new(),
538                idempotent: false,
539            })
540            .await
541    }
542
543    /// Stream every item returned by `listPublicTenants`, following the `cursor` cursor until the
544    /// server reports no further pages.
545    pub fn list_public_tenants_all<'a>(&'a self, params: &'a ListPublicTenantsParams) -> impl Stream<Item = Result<models::PublicTenant>> + 'a {
546        async_stream::try_stream! {
547            let mut guard = CursorGuard::new();
548            let mut cursor = params.cursor.clone();
549            loop {
550                let mut page_params = params.clone();
551                page_params.cursor = cursor.clone();
552                let page = self.list_public_tenants(&page_params).await?;
553                let items = page.items.unwrap_or_default();
554                let was_empty = items.is_empty();
555                for item in items {
556                    yield item;
557                }
558                match guard.advance(page.cursor, page.has_more, was_empty) {
559                    Some(next) => cursor = Some(next),
560                    None => break,
561                }
562            }
563        }
564    }
565
566    /// Domain lookup
567    ///
568    /// `GET /api/v1/public/domain-lookup`
569    pub async fn public_domain_lookup(&self, params: &PublicDomainLookupParams) -> Result<models::PublicDomainLookupResponse> {
570        self.client
571            .request_json(Request {
572                method: Method::GET,
573                path: "/api/v1/public/domain-lookup".to_string(),
574                query: Some(params),
575                body: NO_BODY,
576                headers: Vec::new(),
577                idempotent: false,
578            })
579            .await
580    }
581
582    /// Anonymous analytics beacon
583    ///
584    /// Fire-and-forget event tracker for landing-page funnels. No auth, no PII required;
585    /// rate-limited by IP.
586    ///
587    /// `POST /api/v1/public/track`
588    pub async fn public_track_event(&self, body: &models::PublicTrackEventRequest) -> Result<()> {
589        self.client
590            .request_empty(Request {
591                method: Method::POST,
592                path: "/api/v1/public/track".to_string(),
593                query: NO_QUERY,
594                body: Some(body),
595                headers: Vec::new(),
596                idempotent: true,
597            })
598            .await
599    }
600
601    /// Respond to public HITL
602    ///
603    /// `POST /api/v1/public/sessions/{sessionId}/respond`
604    pub async fn respond_to_public_hitl(&self, session_id: &str, body: &models::RespondToPublicHitlRequest) -> Result<serde_json::Map<String, serde_json::Value>> {
605        self.client
606            .request_json(Request {
607                method: Method::POST,
608                path: format!("/api/v1/public/sessions/{}/respond", encode_path(session_id)),
609                query: NO_QUERY,
610                body: Some(body),
611                headers: Vec::new(),
612                idempotent: true,
613            })
614            .await
615    }
616
617    /// Send public message
618    ///
619    /// `POST /api/v1/public/sessions/{sessionId}/messages`
620    pub async fn send_public_message(&self, session_id: &str, body: &models::SendPublicMessageRequest) -> Result<models::SendPublicMessageResponse> {
621        self.client
622            .request_json(Request {
623                method: Method::POST,
624                path: format!("/api/v1/public/sessions/{}/messages", encode_path(session_id)),
625                query: NO_QUERY,
626                body: Some(body),
627                headers: Vec::new(),
628                idempotent: true,
629            })
630            .await
631    }
632
633    /// Publish a read-only copy of this chat
634    ///
635    /// Snapshots the last 60 user/assistant turns of the session into a share record that
636    /// `getPublicSharedChat` serves for a limited time, and returns its token. No body. A session
637    /// with no turns yet is 400 `Nothing to share yet`. Forms measured through the router with a
638    /// seeded session (public-served-forms_test.ts, 2026-09-10).
639    ///
640    /// `POST /api/v1/public/sessions/{sessionId}/share`
641    pub async fn share_public_session(&self, session_id: &str) -> Result<models::SharePublicSessionResponse> {
642        self.client
643            .request_json(Request {
644                method: Method::POST,
645                path: format!("/api/v1/public/sessions/{}/share", encode_path(session_id)),
646                query: NO_QUERY,
647                body: NO_BODY,
648                headers: Vec::new(),
649                idempotent: true,
650            })
651            .await
652    }
653
654    /// Sign up for Android closed testing
655    ///
656    /// Records the address and, when a testing URL is configured, mails the join link. A repeat
657    /// submit is NOT an error: the same address answers 200 with `already_registered: true` instead
658    /// of 201, and no second letter goes out.
659    ///
660    /// `POST /api/v1/public/testing/android`
661    pub async fn sign_up_for_android_testing(&self, body: &models::SignUpForAndroidTestingRequest) -> Result<models::AndroidTesterSignupResult> {
662        self.client
663            .request_json(Request {
664                method: Method::POST,
665                path: "/api/v1/public/testing/android".to_string(),
666                query: NO_QUERY,
667                body: Some(body),
668                headers: Vec::new(),
669                idempotent: true,
670            })
671            .await
672    }
673
674    /// SSE stream for public session
675    ///
676    /// `GET /api/v1/public/sessions/{sessionId}/events`
677    ///
678    /// Returns a server-sent event stream.
679    pub fn stream_public_session_events(&self, session_id: &str) -> EventStream {
680        self.client.request_stream(
681            &format!("/api/v1/public/sessions/{}/events", encode_path(session_id)),
682            NO_QUERY,
683            Vec::new(),
684        )
685    }
686
687    /// Attach an image to this chat
688    ///
689    /// The body is the raw image bytes — not multipart, not JSON — with its media type in
690    /// `Content-Type` (`image/*` only; anything else is 415). Empty is 400; over 8 MB is 413; more
691    /// uploads than the session allows is 429; a tenant whose storage quota is full gets 403. The
692    /// image is stored as one of the agent tenant's files and the returned `file_id` is what
693    /// `sendPublicMessage` attaches. Forms measured through the router with a seeded session
694    /// (public-served-forms_test.ts, 2026-09-10).
695    ///
696    /// `POST /api/v1/public/sessions/{sessionId}/upload`
697    pub async fn upload_public_session_image(&self, session_id: &str, body: &FilePart) -> Result<models::UploadPublicSessionImageResponse> {
698        self.client
699            .request_json(Request {
700                method: Method::POST,
701                path: format!("/api/v1/public/sessions/{}/upload", encode_path(session_id)),
702                query: NO_QUERY,
703                body: Some(body),
704                headers: Vec::new(),
705                idempotent: true,
706            })
707            .await
708    }
709}