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}