scrapfly-sdk 0.3.1

Async Rust client for the Scrapfly web scraping, screenshot, extraction and crawler APIs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="generator" content="rustdoc"><meta name="description" content="Source of the Rust file `src/cloud_browser.rs`."><title>cloud_browser.rs - source</title><script>if(window.location.protocol!=="file:")document.head.insertAdjacentHTML("beforeend","SourceSerif4-Regular-6b053e98.ttf.woff2,FiraSans-Italic-81dc35de.woff2,FiraSans-Regular-0fe48ade.woff2,FiraSans-MediumItalic-ccf7e434.woff2,FiraSans-Medium-e1aa3f0a.woff2,SourceCodePro-Regular-8badfe75.ttf.woff2,SourceCodePro-Semibold-aa29a496.ttf.woff2".split(",").map(f=>`<link rel="preload" as="font" type="font/woff2"href="../../static.files/${f}">`).join(""))</script><link rel="stylesheet" href="../../static.files/normalize-9960930a.css"><link rel="stylesheet" href="../../static.files/rustdoc-b7b9f40b.css"><meta name="rustdoc-vars" data-root-path="../../" data-static-root-path="../../static.files/" data-current-crate="scrapfly_sdk" data-themes="" data-resource-suffix="" data-rustdoc-version="1.95.0 (59807616e 2026-04-14)" data-channel="1.95.0" data-search-js="search-63369b7b.js" data-stringdex-js="stringdex-b897f86f.js" data-settings-js="settings-170eb4bf.js" ><script src="../../static.files/storage-41dd4d93.js"></script><script defer src="../../static.files/src-script-813739b1.js"></script><script defer src="../../src-files.js"></script><script defer src="../../static.files/main-5013f961.js"></script><noscript><link rel="stylesheet" href="../../static.files/noscript-f7c3ffd8.css"></noscript><link rel="alternate icon" type="image/png" href="../../static.files/favicon-32x32-eab170b8.png"><link rel="icon" type="image/svg+xml" href="../../static.files/favicon-044be391.svg"></head><body class="rustdoc src"><a class="skip-main-content" href="#main-content">Skip to main content</a><!--[if lte IE 11]><div class="warning">This old browser is unsupported and will most likely display funky things.</div><![endif]--><nav class="sidebar"><div class="src-sidebar-title"><h2>Files</h2></div></nav><div class="sidebar-resizer" title="Drag to resize sidebar"></div><main><section id="main-content" class="content" tabindex="-1"><div class="main-heading"><h1><div class="sub-heading">scrapfly_sdk/</div>cloud_browser.rs</h1><rustdoc-toolbar></rustdoc-toolbar></div><div class="example-wrap digits-3"><pre class="rust"><code><a href=#1 id=1 data-nosnippet>1</a><span class="doccomment">//! Cloud Browser API — port of `sdk/go/cloud_browser.go`.
<a href=#2 id=2 data-nosnippet>2</a>
<a href=#3 id=3 data-nosnippet>3</a></span><span class="kw">use </span>reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
<a href=#4 id=4 data-nosnippet>4</a><span class="kw">use </span>reqwest::{Method, Url};
<a href=#5 id=5 data-nosnippet>5</a><span class="kw">use </span>serde::{Deserialize, Serialize};
<a href=#6 id=6 data-nosnippet>6</a>
<a href=#7 id=7 data-nosnippet>7</a><span class="kw">use </span><span class="kw">crate</span>::client::Client;
<a href=#8 id=8 data-nosnippet>8</a><span class="kw">use </span><span class="kw">crate</span>::error::{from_response, ScrapflyError};
<a href=#9 id=9 data-nosnippet>9</a>
<a href=#10 id=10 data-nosnippet>10</a><span class="doccomment">/// Configuration for a Cloud Browser WebSocket session (passed to
<a href=#11 id=11 data-nosnippet>11</a>/// [`cloud_browser_url`]).
<a href=#12 id=12 data-nosnippet>12</a></span><span class="attr">#[derive(Debug, Clone, Default, Serialize)]
<a href=#13 id=13 data-nosnippet>13</a></span><span class="kw">pub struct </span>BrowserConfig {
<a href=#14 id=14 data-nosnippet>14</a>    <span class="doccomment">/// Proxy pool.
<a href=#15 id=15 data-nosnippet>15</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#16 id=16 data-nosnippet>16</a>    </span><span class="kw">pub </span>proxy_pool: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#17 id=17 data-nosnippet>17</a>    <span class="doccomment">/// OS fingerprint.
<a href=#18 id=18 data-nosnippet>18</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#19 id=19 data-nosnippet>19</a>    </span><span class="kw">pub </span>os: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#20 id=20 data-nosnippet>20</a>    <span class="doccomment">/// Proxy country.
<a href=#21 id=21 data-nosnippet>21</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#22 id=22 data-nosnippet>22</a>    </span><span class="kw">pub </span>country: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#23 id=23 data-nosnippet>23</a>    <span class="doccomment">/// Session name.
<a href=#24 id=24 data-nosnippet>24</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#25 id=25 data-nosnippet>25</a>    </span><span class="kw">pub </span>session: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#26 id=26 data-nosnippet>26</a>    <span class="doccomment">/// Session timeout (seconds).
<a href=#27 id=27 data-nosnippet>27</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#28 id=28 data-nosnippet>28</a>    </span><span class="kw">pub </span>timeout: <span class="prelude-ty">Option</span>&lt;u32&gt;,
<a href=#29 id=29 data-nosnippet>29</a>    <span class="doccomment">/// Block images.
<a href=#30 id=30 data-nosnippet>30</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#31 id=31 data-nosnippet>31</a>    </span><span class="kw">pub </span>block_images: bool,
<a href=#32 id=32 data-nosnippet>32</a>    <span class="doccomment">/// Block stylesheets.
<a href=#33 id=33 data-nosnippet>33</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#34 id=34 data-nosnippet>34</a>    </span><span class="kw">pub </span>block_styles: bool,
<a href=#35 id=35 data-nosnippet>35</a>    <span class="doccomment">/// Block fonts.
<a href=#36 id=36 data-nosnippet>36</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#37 id=37 data-nosnippet>37</a>    </span><span class="kw">pub </span>block_fonts: bool,
<a href=#38 id=38 data-nosnippet>38</a>    <span class="doccomment">/// Block media.
<a href=#39 id=39 data-nosnippet>39</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#40 id=40 data-nosnippet>40</a>    </span><span class="kw">pub </span>block_media: bool,
<a href=#41 id=41 data-nosnippet>41</a>    <span class="doccomment">/// Enable screenshot capability.
<a href=#42 id=42 data-nosnippet>42</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#43 id=43 data-nosnippet>43</a>    </span><span class="kw">pub </span>screenshot: bool,
<a href=#44 id=44 data-nosnippet>44</a>    <span class="doccomment">/// Enable cache.
<a href=#45 id=45 data-nosnippet>45</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#46 id=46 data-nosnippet>46</a>    </span><span class="kw">pub </span>cache: bool,
<a href=#47 id=47 data-nosnippet>47</a>    <span class="doccomment">/// Enable blacklist.
<a href=#48 id=48 data-nosnippet>48</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#49 id=49 data-nosnippet>49</a>    </span><span class="kw">pub </span>blacklist: bool,
<a href=#50 id=50 data-nosnippet>50</a>    <span class="doccomment">/// Debug.
<a href=#51 id=51 data-nosnippet>51</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#52 id=52 data-nosnippet>52</a>    </span><span class="kw">pub </span>debug: bool,
<a href=#53 id=53 data-nosnippet>53</a>    <span class="doccomment">/// Resolution.
<a href=#54 id=54 data-nosnippet>54</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#55 id=55 data-nosnippet>55</a>    </span><span class="kw">pub </span>resolution: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#56 id=56 data-nosnippet>56</a>    <span class="doccomment">/// Browser brand.
<a href=#57 id=57 data-nosnippet>57</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#58 id=58 data-nosnippet>58</a>    </span><span class="kw">pub </span>browser_brand: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#59 id=59 data-nosnippet>59</a>    <span class="doccomment">/// BYOP proxy URL.
<a href=#60 id=60 data-nosnippet>60</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#61 id=61 data-nosnippet>61</a>    </span><span class="kw">pub </span>byop_proxy: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#62 id=62 data-nosnippet>62</a>    <span class="doccomment">/// Enable MCP (Model Context Protocol) support.
<a href=#63 id=63 data-nosnippet>63</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#64 id=64 data-nosnippet>64</a>    </span><span class="kw">pub </span>enable_mcp: bool,
<a href=#65 id=65 data-nosnippet>65</a>    <span class="doccomment">/// Arm Scrapium's built-in captcha detector + solver on the first page attach.
<a href=#66 id=66 data-nosnippet>66</a>    /// Turnstile, DataDome slider, reCAPTCHA, GeeTest, PerimeterX hold, and
<a href=#67 id=67 data-nosnippet>67</a>    /// puzzle captchas are handled automatically. Billed per solve; failures
<a href=#68 id=68 data-nosnippet>68</a>    /// cost nothing. See &lt;https://scrapfly.io/docs/cloud-browser-api/captcha-solver&gt;.
<a href=#69 id=69 data-nosnippet>69</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#70 id=70 data-nosnippet>70</a>    </span><span class="kw">pub </span>solve_captcha: bool,
<a href=#71 id=71 data-nosnippet>71</a>    <span class="doccomment">/// Cloud Browser credential vault NAME to attach to the session — the
<a href=#72 id=72 data-nosnippet>72</a>    /// alphanumeric name given at create time. The server resolves the name
<a href=#73 id=73 data-nosnippet>73</a>    /// to the vault scoped to the api-key's project and environment, decrypts
<a href=#74 id=74 data-nosnippet>74</a>    /// its items with the caller-supplied [`BrowserConfig::vault_key`], and
<a href=#75 id=75 data-nosnippet>75</a>    /// pushes them via CDP before the customer takes over. Pair with
<a href=#76 id=76 data-nosnippet>76</a>    /// `vault_key`. See &lt;https://scrapfly.io/docs/cloud-browser-api/vault&gt;.
<a href=#77 id=77 data-nosnippet>77</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#78 id=78 data-nosnippet>78</a>    </span><span class="kw">pub </span>vault: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#79 id=79 data-nosnippet>79</a>    <span class="doccomment">/// Customer-held base64-encoded 32-byte vault key. Forwarded transiently
<a href=#80 id=80 data-nosnippet>80</a>    /// on the WebSocket query string and zeroed by the server after items
<a href=#81 id=81 data-nosnippet>81</a>    /// are decrypted. The SDK never logs or echoes this value — see the
<a href=#82 id=82 data-nosnippet>82</a>    /// E2EE boundary documentation in the project's HIPAA evidence pack.
<a href=#83 id=83 data-nosnippet>83</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#84 id=84 data-nosnippet>84</a>    </span><span class="kw">pub </span>vault_key: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#85 id=85 data-nosnippet>85</a>
<a href=#86 id=86 data-nosnippet>86</a>    <span class="doccomment">/// Enable the human-in-the-loop VNC channel.
<a href=#87 id=87 data-nosnippet>87</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#88 id=88 data-nosnippet>88</a>    </span><span class="kw">pub </span>enable_vnc: bool,
<a href=#89 id=89 data-nosnippet>89</a>    <span class="doccomment">/// Customer-chosen VNC password. Required when `enable_vnc` is true and
<a href=#90 id=90 data-nosnippet>90</a>    /// `hitl_allowed_networks` is empty.
<a href=#91 id=91 data-nosnippet>91</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#92 id=92 data-nosnippet>92</a>    </span><span class="kw">pub </span>vnc_password: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#93 id=93 data-nosnippet>93</a>    <span class="doccomment">/// Enable the human-in-the-loop WebRTC channel.
<a href=#94 id=94 data-nosnippet>94</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#95 id=95 data-nosnippet>95</a>    </span><span class="kw">pub </span>enable_rtc: bool,
<a href=#96 id=96 data-nosnippet>96</a>    <span class="doccomment">/// Optional WebRTC username (defaults to "scrapfly" server-side).
<a href=#97 id=97 data-nosnippet>97</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#98 id=98 data-nosnippet>98</a>    </span><span class="kw">pub </span>rtc_username: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#99 id=99 data-nosnippet>99</a>    <span class="doccomment">/// Customer-chosen WebRTC password. Required when `enable_rtc` is true
<a href=#100 id=100 data-nosnippet>100</a>    /// and `hitl_allowed_networks` is empty.
<a href=#101 id=101 data-nosnippet>101</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#102 id=102 data-nosnippet>102</a>    </span><span class="kw">pub </span>rtc_password: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#103 id=103 data-nosnippet>103</a>    <span class="doccomment">/// Source IPs / CIDRs trusted to attach to HITL channels (VNC + WebRTC +
<a href=#104 id=104 data-nosnippet>104</a>    /// downloads) without credentials. Sent on the wire as a comma-separated
<a href=#105 id=105 data-nosnippet>105</a>    /// string.
<a href=#106 id=106 data-nosnippet>106</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Vec::is_empty"</span>)]
<a href=#107 id=107 data-nosnippet>107</a>    </span><span class="kw">pub </span>hitl_allowed_networks: Vec&lt;String&gt;,
<a href=#108 id=108 data-nosnippet>108</a>}
<a href=#109 id=109 data-nosnippet>109</a>
<a href=#110 id=110 data-nosnippet>110</a><span class="doccomment">/// Return the deterministic project salt for an api key (`sha256(api_key)[:8]`).
<a href=#111 id=111 data-nosnippet>111</a>/// Matches the `X-Browser-Project-Salt` response header returned on a
<a href=#112 id=112 data-nosnippet>112</a>/// successful Cloud Browser WebSocket upgrade.
<a href=#113 id=113 data-nosnippet>113</a></span><span class="kw">pub fn </span>project_salt(api_key: <span class="kw-2">&amp;</span>str) -&gt; String {
<a href=#114 id=114 data-nosnippet>114</a>    <span class="kw">use </span>sha2::{Digest, Sha256};
<a href=#115 id=115 data-nosnippet>115</a>    <span class="kw">let </span>digest = Sha256::digest(api_key.as_bytes());
<a href=#116 id=116 data-nosnippet>116</a>    hex::encode(<span class="kw-2">&amp;</span>digest[..<span class="number">4</span>])
<a href=#117 id=117 data-nosnippet>117</a>}
<a href=#118 id=118 data-nosnippet>118</a>
<a href=#119 id=119 data-nosnippet>119</a><span class="kw">fn </span>is_false(v: <span class="kw-2">&amp;</span>bool) -&gt; bool {
<a href=#120 id=120 data-nosnippet>120</a>    !<span class="kw-2">*</span>v
<a href=#121 id=121 data-nosnippet>121</a>}
<a href=#122 id=122 data-nosnippet>122</a>
<a href=#123 id=123 data-nosnippet>123</a><span class="doccomment">/// Normalize an arbitrary Cloud Browser host to a `wss://` URL, regardless of
<a href=#124 id=124 data-nosnippet>124</a>/// the scheme the caller configured. Accepted input schemes: `https://`
<a href=#125 id=125 data-nosnippet>125</a>/// (default), `wss://`, `ws://`, `http://`, and bare `host[:port]`. Mirrors
<a href=#126 id=126 data-nosnippet>126</a>/// `sdk/go/cloud_browser.go::wsBase`.
<a href=#127 id=127 data-nosnippet>127</a></span><span class="kw">fn </span>ws_base(host: <span class="kw-2">&amp;</span>str) -&gt; String {
<a href=#128 id=128 data-nosnippet>128</a>    <span class="kw">if let </span><span class="prelude-val">Some</span>(rest) = host.strip_prefix(<span class="string">"wss://"</span>) {
<a href=#129 id=129 data-nosnippet>129</a>        <span class="macro">format!</span>(<span class="string">"wss://{}"</span>, rest)
<a href=#130 id=130 data-nosnippet>130</a>    } <span class="kw">else if let </span><span class="prelude-val">Some</span>(rest) = host.strip_prefix(<span class="string">"ws://"</span>) {
<a href=#131 id=131 data-nosnippet>131</a>        <span class="macro">format!</span>(<span class="string">"ws://{}"</span>, rest)
<a href=#132 id=132 data-nosnippet>132</a>    } <span class="kw">else if let </span><span class="prelude-val">Some</span>(rest) = host.strip_prefix(<span class="string">"https://"</span>) {
<a href=#133 id=133 data-nosnippet>133</a>        <span class="macro">format!</span>(<span class="string">"wss://{}"</span>, rest)
<a href=#134 id=134 data-nosnippet>134</a>    } <span class="kw">else if let </span><span class="prelude-val">Some</span>(rest) = host.strip_prefix(<span class="string">"http://"</span>) {
<a href=#135 id=135 data-nosnippet>135</a>        <span class="macro">format!</span>(<span class="string">"ws://{}"</span>, rest)
<a href=#136 id=136 data-nosnippet>136</a>    } <span class="kw">else </span>{
<a href=#137 id=137 data-nosnippet>137</a>        <span class="macro">format!</span>(<span class="string">"wss://{}"</span>, host)
<a href=#138 id=138 data-nosnippet>138</a>    }
<a href=#139 id=139 data-nosnippet>139</a>}
<a href=#140 id=140 data-nosnippet>140</a>
<a href=#141 id=141 data-nosnippet>141</a><span class="doccomment">/// Normalize an arbitrary Cloud Browser host to its REST form (`https://` or
<a href=#142 id=142 data-nosnippet>142</a>/// `http://`). Callers typically configure a `wss://` / `ws://` host (the CDP
<a href=#143 id=143 data-nosnippet>143</a>/// entry point); the REST endpoints (`/unblock`, `/session/.../stop`) live on
<a href=#144 id=144 data-nosnippet>144</a>/// the HTTP-equivalent origin. Mirrors `sdk/go/cloud_browser.go::restBase`.
<a href=#145 id=145 data-nosnippet>145</a></span><span class="kw">fn </span>rest_base(host: <span class="kw-2">&amp;</span>str) -&gt; String {
<a href=#146 id=146 data-nosnippet>146</a>    <span class="kw">if let </span><span class="prelude-val">Some</span>(rest) = host.strip_prefix(<span class="string">"wss://"</span>) {
<a href=#147 id=147 data-nosnippet>147</a>        <span class="macro">format!</span>(<span class="string">"https://{}"</span>, rest)
<a href=#148 id=148 data-nosnippet>148</a>    } <span class="kw">else if let </span><span class="prelude-val">Some</span>(rest) = host.strip_prefix(<span class="string">"ws://"</span>) {
<a href=#149 id=149 data-nosnippet>149</a>        <span class="macro">format!</span>(<span class="string">"http://{}"</span>, rest)
<a href=#150 id=150 data-nosnippet>150</a>    } <span class="kw">else if </span>host.starts_with(<span class="string">"https://"</span>) || host.starts_with(<span class="string">"http://"</span>) {
<a href=#151 id=151 data-nosnippet>151</a>        host.to_string()
<a href=#152 id=152 data-nosnippet>152</a>    } <span class="kw">else </span>{
<a href=#153 id=153 data-nosnippet>153</a>        <span class="macro">format!</span>(<span class="string">"https://{}"</span>, host)
<a href=#154 id=154 data-nosnippet>154</a>    }
<a href=#155 id=155 data-nosnippet>155</a>}
<a href=#156 id=156 data-nosnippet>156</a>
<a href=#157 id=157 data-nosnippet>157</a><span class="doccomment">/// Unblock request body.
<a href=#158 id=158 data-nosnippet>158</a></span><span class="attr">#[derive(Debug, Clone, Default, Serialize)]
<a href=#159 id=159 data-nosnippet>159</a></span><span class="kw">pub struct </span>UnblockConfig {
<a href=#160 id=160 data-nosnippet>160</a>    <span class="doccomment">/// Target URL.
<a href=#161 id=161 data-nosnippet>161</a>    </span><span class="kw">pub </span>url: String,
<a href=#162 id=162 data-nosnippet>162</a>    <span class="doccomment">/// Proxy country.
<a href=#163 id=163 data-nosnippet>163</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#164 id=164 data-nosnippet>164</a>    </span><span class="kw">pub </span>country: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#165 id=165 data-nosnippet>165</a>    <span class="doccomment">/// Fingerprint OS: `linux`, `windows`, `macos`.
<a href=#166 id=166 data-nosnippet>166</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#167 id=167 data-nosnippet>167</a>    </span><span class="kw">pub </span>os: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#168 id=168 data-nosnippet>168</a>    <span class="doccomment">/// Fingerprint browser brand: `chrome`, `edge`, `brave`, `opera`.
<a href=#169 id=169 data-nosnippet>169</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#170 id=170 data-nosnippet>170</a>    </span><span class="kw">pub </span>browser_brand: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#171 id=171 data-nosnippet>171</a>    <span class="doccomment">/// Named session for reconnection — reuses an existing ASP session and
<a href=#172 id=172 data-nosnippet>172</a>    /// disables auto-close on disconnect.
<a href=#173 id=173 data-nosnippet>173</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#174 id=174 data-nosnippet>174</a>    </span><span class="kw">pub </span>session: <span class="prelude-ty">Option</span>&lt;String&gt;,
<a href=#175 id=175 data-nosnippet>175</a>    <span class="doccomment">/// Navigation timeout.
<a href=#176 id=176 data-nosnippet>176</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#177 id=177 data-nosnippet>177</a>    </span><span class="kw">pub </span>timeout: <span class="prelude-ty">Option</span>&lt;u32&gt;,
<a href=#178 id=178 data-nosnippet>178</a>    <span class="doccomment">/// Browser session timeout.
<a href=#179 id=179 data-nosnippet>179</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"Option::is_none"</span>)]
<a href=#180 id=180 data-nosnippet>180</a>    </span><span class="kw">pub </span>browser_timeout: <span class="prelude-ty">Option</span>&lt;u32&gt;,
<a href=#181 id=181 data-nosnippet>181</a>    <span class="doccomment">/// Enable MCP support in the browser.
<a href=#182 id=182 data-nosnippet>182</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#183 id=183 data-nosnippet>183</a>    </span><span class="kw">pub </span>enable_mcp: bool,
<a href=#184 id=184 data-nosnippet>184</a>    <span class="doccomment">/// Record the session for replay via [`Client::cloud_browser_playback`] /
<a href=#185 id=185 data-nosnippet>185</a>    /// [`Client::cloud_browser_video`].
<a href=#186 id=186 data-nosnippet>186</a>    </span><span class="attr">#[serde(skip_serializing_if = <span class="string">"is_false"</span>)]
<a href=#187 id=187 data-nosnippet>187</a>    </span><span class="kw">pub </span>debug: bool,
<a href=#188 id=188 data-nosnippet>188</a>}
<a href=#189 id=189 data-nosnippet>189</a>
<a href=#190 id=190 data-nosnippet>190</a><span class="doccomment">/// Response from `POST /unblock`.
<a href=#191 id=191 data-nosnippet>191</a></span><span class="attr">#[derive(Debug, Clone, Deserialize, Default)]
<a href=#192 id=192 data-nosnippet>192</a></span><span class="kw">pub struct </span>UnblockResult {
<a href=#193 id=193 data-nosnippet>193</a>    <span class="doccomment">/// WebSocket URL to connect to.
<a href=#194 id=194 data-nosnippet>194</a>    </span><span class="attr">#[serde(default)]
<a href=#195 id=195 data-nosnippet>195</a>    </span><span class="kw">pub </span>ws_url: String,
<a href=#196 id=196 data-nosnippet>196</a>    <span class="doccomment">/// Session id.
<a href=#197 id=197 data-nosnippet>197</a>    </span><span class="attr">#[serde(default)]
<a href=#198 id=198 data-nosnippet>198</a>    </span><span class="kw">pub </span>session_id: String,
<a href=#199 id=199 data-nosnippet>199</a>    <span class="doccomment">/// Run id.
<a href=#200 id=200 data-nosnippet>200</a>    </span><span class="attr">#[serde(default)]
<a href=#201 id=201 data-nosnippet>201</a>    </span><span class="kw">pub </span>run_id: String,
<a href=#202 id=202 data-nosnippet>202</a>    <span class="doccomment">/// MCP endpoint (only when enable_mcp was set).
<a href=#203 id=203 data-nosnippet>203</a>    </span><span class="attr">#[serde(default)]
<a href=#204 id=204 data-nosnippet>204</a>    </span><span class="kw">pub </span>mcp_endpoint: String,
<a href=#205 id=205 data-nosnippet>205</a>}
<a href=#206 id=206 data-nosnippet>206</a>
<a href=#207 id=207 data-nosnippet>207</a><span class="kw">impl </span>Client {
<a href=#208 id=208 data-nosnippet>208</a>    <span class="doccomment">/// Build the WebSocket URL for a new Cloud Browser session.
<a href=#209 id=209 data-nosnippet>209</a>    ///
<a href=#210 id=210 data-nosnippet>210</a>    /// On rejection the server sends a JSON error frame then a close frame
<a href=#211 id=211 data-nosnippet>211</a>    /// with code 1008/1011/1013 and a "ERR::BROWSER::CODE: reason" string.
<a href=#212 id=212 data-nosnippet>212</a>    /// See &lt;https://scrapfly.io/docs/cloud-browser-api/errors#websocket-close-frame&gt;
<a href=#213 id=213 data-nosnippet>213</a>    </span><span class="kw">pub fn </span>cloud_browser_url(<span class="kw-2">&amp;</span><span class="self">self</span>, config: <span class="kw-2">&amp;</span>BrowserConfig) -&gt; String {
<a href=#214 id=214 data-nosnippet>214</a>        <span class="kw">let </span>ws_host = ws_base(<span class="self">self</span>.cloud_browser_host());
<a href=#215 id=215 data-nosnippet>215</a>        <span class="kw">let </span><span class="kw-2">mut </span>pairs: Vec&lt;(String, String)&gt; = <span class="macro">vec!</span>[(<span class="string">"api_key"</span>.into(), <span class="self">self</span>.api_key().into())];
<a href=#216 id=216 data-nosnippet>216</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.proxy_pool {
<a href=#217 id=217 data-nosnippet>217</a>            pairs.push((<span class="string">"proxy_pool"</span>.into(), v.clone()));
<a href=#218 id=218 data-nosnippet>218</a>        }
<a href=#219 id=219 data-nosnippet>219</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.os {
<a href=#220 id=220 data-nosnippet>220</a>            pairs.push((<span class="string">"os"</span>.into(), v.clone()));
<a href=#221 id=221 data-nosnippet>221</a>        }
<a href=#222 id=222 data-nosnippet>222</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.country {
<a href=#223 id=223 data-nosnippet>223</a>            pairs.push((<span class="string">"country"</span>.into(), v.clone()));
<a href=#224 id=224 data-nosnippet>224</a>        }
<a href=#225 id=225 data-nosnippet>225</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.session {
<a href=#226 id=226 data-nosnippet>226</a>            pairs.push((<span class="string">"session"</span>.into(), v.clone()));
<a href=#227 id=227 data-nosnippet>227</a>        }
<a href=#228 id=228 data-nosnippet>228</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = config.timeout {
<a href=#229 id=229 data-nosnippet>229</a>            pairs.push((<span class="string">"timeout"</span>.into(), v.to_string()));
<a href=#230 id=230 data-nosnippet>230</a>        }
<a href=#231 id=231 data-nosnippet>231</a>        <span class="kw">if </span>config.block_images {
<a href=#232 id=232 data-nosnippet>232</a>            pairs.push((<span class="string">"block_images"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#233 id=233 data-nosnippet>233</a>        }
<a href=#234 id=234 data-nosnippet>234</a>        <span class="kw">if </span>config.block_styles {
<a href=#235 id=235 data-nosnippet>235</a>            pairs.push((<span class="string">"block_styles"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#236 id=236 data-nosnippet>236</a>        }
<a href=#237 id=237 data-nosnippet>237</a>        <span class="kw">if </span>config.block_fonts {
<a href=#238 id=238 data-nosnippet>238</a>            pairs.push((<span class="string">"block_fonts"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#239 id=239 data-nosnippet>239</a>        }
<a href=#240 id=240 data-nosnippet>240</a>        <span class="kw">if </span>config.block_media {
<a href=#241 id=241 data-nosnippet>241</a>            pairs.push((<span class="string">"block_media"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#242 id=242 data-nosnippet>242</a>        }
<a href=#243 id=243 data-nosnippet>243</a>        <span class="kw">if </span>config.screenshot {
<a href=#244 id=244 data-nosnippet>244</a>            pairs.push((<span class="string">"screenshot"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#245 id=245 data-nosnippet>245</a>        }
<a href=#246 id=246 data-nosnippet>246</a>        <span class="kw">if </span>config.cache {
<a href=#247 id=247 data-nosnippet>247</a>            pairs.push((<span class="string">"cache"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#248 id=248 data-nosnippet>248</a>        }
<a href=#249 id=249 data-nosnippet>249</a>        <span class="kw">if </span>config.blacklist {
<a href=#250 id=250 data-nosnippet>250</a>            pairs.push((<span class="string">"blacklist"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#251 id=251 data-nosnippet>251</a>        }
<a href=#252 id=252 data-nosnippet>252</a>        <span class="kw">if </span>config.debug {
<a href=#253 id=253 data-nosnippet>253</a>            pairs.push((<span class="string">"debug"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#254 id=254 data-nosnippet>254</a>        }
<a href=#255 id=255 data-nosnippet>255</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.resolution {
<a href=#256 id=256 data-nosnippet>256</a>            pairs.push((<span class="string">"resolution"</span>.into(), v.clone()));
<a href=#257 id=257 data-nosnippet>257</a>        }
<a href=#258 id=258 data-nosnippet>258</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.browser_brand {
<a href=#259 id=259 data-nosnippet>259</a>            pairs.push((<span class="string">"browser_brand"</span>.into(), v.clone()));
<a href=#260 id=260 data-nosnippet>260</a>        }
<a href=#261 id=261 data-nosnippet>261</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.byop_proxy {
<a href=#262 id=262 data-nosnippet>262</a>            pairs.push((<span class="string">"byop_proxy"</span>.into(), v.clone()));
<a href=#263 id=263 data-nosnippet>263</a>        }
<a href=#264 id=264 data-nosnippet>264</a>        <span class="kw">if </span>config.enable_mcp {
<a href=#265 id=265 data-nosnippet>265</a>            pairs.push((<span class="string">"enable_mcp"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#266 id=266 data-nosnippet>266</a>        }
<a href=#267 id=267 data-nosnippet>267</a>        <span class="kw">if </span>config.solve_captcha {
<a href=#268 id=268 data-nosnippet>268</a>            pairs.push((<span class="string">"solve_captcha"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#269 id=269 data-nosnippet>269</a>        }
<a href=#270 id=270 data-nosnippet>270</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.vault {
<a href=#271 id=271 data-nosnippet>271</a>            pairs.push((<span class="string">"vault"</span>.into(), v.clone()));
<a href=#272 id=272 data-nosnippet>272</a>        }
<a href=#273 id=273 data-nosnippet>273</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.vault_key {
<a href=#274 id=274 data-nosnippet>274</a>            pairs.push((<span class="string">"vault_key"</span>.into(), v.clone()));
<a href=#275 id=275 data-nosnippet>275</a>        }
<a href=#276 id=276 data-nosnippet>276</a>        <span class="kw">if </span>config.enable_vnc {
<a href=#277 id=277 data-nosnippet>277</a>            pairs.push((<span class="string">"enable_vnc"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#278 id=278 data-nosnippet>278</a>        }
<a href=#279 id=279 data-nosnippet>279</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.vnc_password {
<a href=#280 id=280 data-nosnippet>280</a>            pairs.push((<span class="string">"vnc_password"</span>.into(), v.clone()));
<a href=#281 id=281 data-nosnippet>281</a>        }
<a href=#282 id=282 data-nosnippet>282</a>        <span class="kw">if </span>config.enable_rtc {
<a href=#283 id=283 data-nosnippet>283</a>            pairs.push((<span class="string">"enable_rtc"</span>.into(), <span class="string">"true"</span>.into()));
<a href=#284 id=284 data-nosnippet>284</a>        }
<a href=#285 id=285 data-nosnippet>285</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.rtc_username {
<a href=#286 id=286 data-nosnippet>286</a>            pairs.push((<span class="string">"rtc_username"</span>.into(), v.clone()));
<a href=#287 id=287 data-nosnippet>287</a>        }
<a href=#288 id=288 data-nosnippet>288</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = <span class="kw-2">&amp;</span>config.rtc_password {
<a href=#289 id=289 data-nosnippet>289</a>            pairs.push((<span class="string">"rtc_password"</span>.into(), v.clone()));
<a href=#290 id=290 data-nosnippet>290</a>        }
<a href=#291 id=291 data-nosnippet>291</a>        <span class="kw">if </span>!config.hitl_allowed_networks.is_empty() {
<a href=#292 id=292 data-nosnippet>292</a>            pairs.push((
<a href=#293 id=293 data-nosnippet>293</a>                <span class="string">"hitl_allowed_networks"</span>.into(),
<a href=#294 id=294 data-nosnippet>294</a>                config.hitl_allowed_networks.join(<span class="string">","</span>),
<a href=#295 id=295 data-nosnippet>295</a>            ));
<a href=#296 id=296 data-nosnippet>296</a>        }
<a href=#297 id=297 data-nosnippet>297</a>        <span class="kw">let </span>qs = serde_urlencoded::to_string(<span class="kw-2">&amp;</span>pairs).unwrap_or_default();
<a href=#298 id=298 data-nosnippet>298</a>        <span class="macro">format!</span>(<span class="string">"{}?{}"</span>, ws_host, qs)
<a href=#299 id=299 data-nosnippet>299</a>    }
<a href=#300 id=300 data-nosnippet>300</a>
<a href=#301 id=301 data-nosnippet>301</a>    <span class="doccomment">/// Return the deterministic project salt for this client's api key.
<a href=#302 id=302 data-nosnippet>302</a>    /// Matches the `X-Browser-Project-Salt` response header.
<a href=#303 id=303 data-nosnippet>303</a>    </span><span class="kw">pub fn </span>cloud_browser_project_salt(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; String {
<a href=#304 id=304 data-nosnippet>304</a>        project_salt(<span class="self">self</span>.api_key())
<a href=#305 id=305 data-nosnippet>305</a>    }
<a href=#306 id=306 data-nosnippet>306</a>
<a href=#307 id=307 data-nosnippet>307</a>    <span class="doccomment">/// Call `POST /unblock` to bypass anti-bot protection.
<a href=#308 id=308 data-nosnippet>308</a>    </span><span class="kw">pub async fn </span>cloud_browser_unblock(
<a href=#309 id=309 data-nosnippet>309</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#310 id=310 data-nosnippet>310</a>        config: <span class="kw-2">&amp;</span>UnblockConfig,
<a href=#311 id=311 data-nosnippet>311</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;UnblockResult, ScrapflyError&gt; {
<a href=#312 id=312 data-nosnippet>312</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#313 id=313 data-nosnippet>313</a>            <span class="string">"{}/unblock?key={}"</span>,
<a href=#314 id=314 data-nosnippet>314</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#315 id=315 data-nosnippet>315</a>            <span class="self">self</span>.api_key()
<a href=#316 id=316 data-nosnippet>316</a>        );
<a href=#317 id=317 data-nosnippet>317</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#318 id=318 data-nosnippet>318</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid unblock url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#319 id=319 data-nosnippet>319</a>        <span class="kw">let </span>body = serde_json::to_vec(config)<span class="question-mark">?</span>;
<a href=#320 id=320 data-nosnippet>320</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#321 id=321 data-nosnippet>321</a>        headers.insert(CONTENT_TYPE, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#322 id=322 data-nosnippet>322</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#323 id=323 data-nosnippet>323</a>            </span>.send_with_retry(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(body))
<a href=#324 id=324 data-nosnippet>324</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#325 id=325 data-nosnippet>325</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#326 id=326 data-nosnippet>326</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#327 id=327 data-nosnippet>327</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#328 id=328 data-nosnippet>328</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#329 id=329 data-nosnippet>329</a>        }
<a href=#330 id=330 data-nosnippet>330</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#331 id=331 data-nosnippet>331</a>    }
<a href=#332 id=332 data-nosnippet>332</a>
<a href=#333 id=333 data-nosnippet>333</a>    <span class="doccomment">/// List browser extensions for the account.
<a href=#334 id=334 data-nosnippet>334</a>    </span><span class="kw">pub async fn </span>cloud_browser_extension_list(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#335 id=335 data-nosnippet>335</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#336 id=336 data-nosnippet>336</a>            <span class="string">"{}/extension?key={}"</span>,
<a href=#337 id=337 data-nosnippet>337</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#338 id=338 data-nosnippet>338</a>            <span class="self">self</span>.api_key()
<a href=#339 id=339 data-nosnippet>339</a>        );
<a href=#340 id=340 data-nosnippet>340</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#341 id=341 data-nosnippet>341</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid extension url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#342 id=342 data-nosnippet>342</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#343 id=343 data-nosnippet>343</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#344 id=344 data-nosnippet>344</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#345 id=345 data-nosnippet>345</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#346 id=346 data-nosnippet>346</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#347 id=347 data-nosnippet>347</a>        }
<a href=#348 id=348 data-nosnippet>348</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#349 id=349 data-nosnippet>349</a>    }
<a href=#350 id=350 data-nosnippet>350</a>
<a href=#351 id=351 data-nosnippet>351</a>    <span class="doccomment">/// Get details of a specific browser extension.
<a href=#352 id=352 data-nosnippet>352</a>    </span><span class="kw">pub async fn </span>cloud_browser_extension_get(
<a href=#353 id=353 data-nosnippet>353</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#354 id=354 data-nosnippet>354</a>        extension_id: <span class="kw-2">&amp;</span>str,
<a href=#355 id=355 data-nosnippet>355</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#356 id=356 data-nosnippet>356</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#357 id=357 data-nosnippet>357</a>            <span class="string">"{}/extension/{}?key={}"</span>,
<a href=#358 id=358 data-nosnippet>358</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#359 id=359 data-nosnippet>359</a>            extension_id,
<a href=#360 id=360 data-nosnippet>360</a>            <span class="self">self</span>.api_key()
<a href=#361 id=361 data-nosnippet>361</a>        );
<a href=#362 id=362 data-nosnippet>362</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#363 id=363 data-nosnippet>363</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid extension url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#364 id=364 data-nosnippet>364</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#365 id=365 data-nosnippet>365</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#366 id=366 data-nosnippet>366</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#367 id=367 data-nosnippet>367</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#368 id=368 data-nosnippet>368</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#369 id=369 data-nosnippet>369</a>        }
<a href=#370 id=370 data-nosnippet>370</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#371 id=371 data-nosnippet>371</a>    }
<a href=#372 id=372 data-nosnippet>372</a>
<a href=#373 id=373 data-nosnippet>373</a>    <span class="doccomment">/// Upload a browser extension from a local file (.zip or .crx).
<a href=#374 id=374 data-nosnippet>374</a>    </span><span class="kw">pub async fn </span>cloud_browser_extension_upload(
<a href=#375 id=375 data-nosnippet>375</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#376 id=376 data-nosnippet>376</a>        file_path: <span class="kw-2">&amp;</span>std::path::Path,
<a href=#377 id=377 data-nosnippet>377</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#378 id=378 data-nosnippet>378</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#379 id=379 data-nosnippet>379</a>            <span class="string">"{}/extension?key={}"</span>,
<a href=#380 id=380 data-nosnippet>380</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#381 id=381 data-nosnippet>381</a>            <span class="self">self</span>.api_key()
<a href=#382 id=382 data-nosnippet>382</a>        );
<a href=#383 id=383 data-nosnippet>383</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#384 id=384 data-nosnippet>384</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid extension url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#385 id=385 data-nosnippet>385</a>        <span class="kw">let </span>file_bytes = std::fs::read(file_path)
<a href=#386 id=386 data-nosnippet>386</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"failed to read extension file: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#387 id=387 data-nosnippet>387</a>        <span class="kw">let </span>file_name = file_path
<a href=#388 id=388 data-nosnippet>388</a>            .file_name()
<a href=#389 id=389 data-nosnippet>389</a>            .and_then(|n| n.to_str())
<a href=#390 id=390 data-nosnippet>390</a>            .unwrap_or(<span class="string">"extension.zip"</span>)
<a href=#391 id=391 data-nosnippet>391</a>            .to_string();
<a href=#392 id=392 data-nosnippet>392</a>        <span class="comment">// Build multipart body manually (reqwest multipart feature not enabled)
<a href=#393 id=393 data-nosnippet>393</a>        </span><span class="kw">let </span>boundary = <span class="macro">format!</span>(
<a href=#394 id=394 data-nosnippet>394</a>            <span class="string">"----ScrapflyBoundary{}"</span>,
<a href=#395 id=395 data-nosnippet>395</a>            std::time::SystemTime::now()
<a href=#396 id=396 data-nosnippet>396</a>                .duration_since(std::time::UNIX_EPOCH)
<a href=#397 id=397 data-nosnippet>397</a>                .unwrap_or_default()
<a href=#398 id=398 data-nosnippet>398</a>                .as_millis()
<a href=#399 id=399 data-nosnippet>399</a>        );
<a href=#400 id=400 data-nosnippet>400</a>        <span class="kw">let </span><span class="kw-2">mut </span>body = Vec::new();
<a href=#401 id=401 data-nosnippet>401</a>        body.extend_from_slice(<span class="macro">format!</span>(<span class="string">"--{}\r\n"</span>, boundary).as_bytes());
<a href=#402 id=402 data-nosnippet>402</a>        body.extend_from_slice(
<a href=#403 id=403 data-nosnippet>403</a>            <span class="macro">format!</span>(
<a href=#404 id=404 data-nosnippet>404</a>                <span class="string">"Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n\
<a href=#405 id=405 data-nosnippet>405</a>                 Content-Type: application/octet-stream\r\n\r\n"</span>,
<a href=#406 id=406 data-nosnippet>406</a>                file_name
<a href=#407 id=407 data-nosnippet>407</a>            )
<a href=#408 id=408 data-nosnippet>408</a>            .as_bytes(),
<a href=#409 id=409 data-nosnippet>409</a>        );
<a href=#410 id=410 data-nosnippet>410</a>        body.extend_from_slice(<span class="kw-2">&amp;</span>file_bytes);
<a href=#411 id=411 data-nosnippet>411</a>        body.extend_from_slice(<span class="macro">format!</span>(<span class="string">"\r\n--{}--\r\n"</span>, boundary).as_bytes());
<a href=#412 id=412 data-nosnippet>412</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#413 id=413 data-nosnippet>413</a>        headers.insert(
<a href=#414 id=414 data-nosnippet>414</a>            CONTENT_TYPE,
<a href=#415 id=415 data-nosnippet>415</a>            HeaderValue::from_str(<span class="kw-2">&amp;</span><span class="macro">format!</span>(<span class="string">"multipart/form-data; boundary={}"</span>, boundary))
<a href=#416 id=416 data-nosnippet>416</a>                .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid content-type: {}"</span>, e)))<span class="question-mark">?</span>,
<a href=#417 id=417 data-nosnippet>417</a>        );
<a href=#418 id=418 data-nosnippet>418</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#419 id=419 data-nosnippet>419</a>            </span>.send_with_retry(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(body))
<a href=#420 id=420 data-nosnippet>420</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#421 id=421 data-nosnippet>421</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#422 id=422 data-nosnippet>422</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#423 id=423 data-nosnippet>423</a>        <span class="kw">if </span>status != <span class="number">200 </span>&amp;&amp; status != <span class="number">201 </span>{
<a href=#424 id=424 data-nosnippet>424</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#425 id=425 data-nosnippet>425</a>        }
<a href=#426 id=426 data-nosnippet>426</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#427 id=427 data-nosnippet>427</a>    }
<a href=#428 id=428 data-nosnippet>428</a>
<a href=#429 id=429 data-nosnippet>429</a>    <span class="doccomment">/// Delete a browser extension by ID.
<a href=#430 id=430 data-nosnippet>430</a>    </span><span class="kw">pub async fn </span>cloud_browser_extension_delete(
<a href=#431 id=431 data-nosnippet>431</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#432 id=432 data-nosnippet>432</a>        extension_id: <span class="kw-2">&amp;</span>str,
<a href=#433 id=433 data-nosnippet>433</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#434 id=434 data-nosnippet>434</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#435 id=435 data-nosnippet>435</a>            <span class="string">"{}/extension/{}?key={}"</span>,
<a href=#436 id=436 data-nosnippet>436</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#437 id=437 data-nosnippet>437</a>            extension_id,
<a href=#438 id=438 data-nosnippet>438</a>            <span class="self">self</span>.api_key()
<a href=#439 id=439 data-nosnippet>439</a>        );
<a href=#440 id=440 data-nosnippet>440</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#441 id=441 data-nosnippet>441</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid extension url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#442 id=442 data-nosnippet>442</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#443 id=443 data-nosnippet>443</a>            </span>.send_with_retry(Method::DELETE, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>)
<a href=#444 id=444 data-nosnippet>444</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#445 id=445 data-nosnippet>445</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#446 id=446 data-nosnippet>446</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#447 id=447 data-nosnippet>447</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#448 id=448 data-nosnippet>448</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#449 id=449 data-nosnippet>449</a>        }
<a href=#450 id=450 data-nosnippet>450</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#451 id=451 data-nosnippet>451</a>    }
<a href=#452 id=452 data-nosnippet>452</a>
<a href=#453 id=453 data-nosnippet>453</a>    <span class="comment">// ------------------------------------------------------------------
<a href=#454 id=454 data-nosnippet>454</a>    // Cloud Browser credential vault — E2EE customer-held keys.
<a href=#455 id=455 data-nosnippet>455</a>    //
<a href=#456 id=456 data-nosnippet>456</a>    // The vault key returned by `cloud_browser_vault_create` /
<a href=#457 id=457 data-nosnippet>457</a>    // `cloud_browser_vault_rotate` is the ONLY copy. The SDK forwards it
<a href=#458 id=458 data-nosnippet>458</a>    // transiently in the `X-Vault-Key` header on the few endpoints that
<a href=#459 id=459 data-nosnippet>459</a>    // need it (item create/update with secret rotation, vault rotate)
<a href=#460 id=460 data-nosnippet>460</a>    // and never logs, prints, formats, or otherwise persists it. Loud
<a href=#461 id=461 data-nosnippet>461</a>    // rule documented at:
<a href=#462 id=462 data-nosnippet>462</a>    //   /root/.claude/projects/-root-scrapfly-apps/memory/agent_secret_tokenization_boundary.md
<a href=#463 id=463 data-nosnippet>463</a>    // ------------------------------------------------------------------
<a href=#464 id=464 data-nosnippet>464</a>
<a href=#465 id=465 data-nosnippet>465</a>    </span><span class="doccomment">/// Create a new credential vault. The server returns a freshly
<a href=#466 id=466 data-nosnippet>466</a>    /// generated vault key under the `key` field of the response — this
<a href=#467 id=467 data-nosnippet>467</a>    /// is the only time the key is shown. Save it locally; the server
<a href=#468 id=468 data-nosnippet>468</a>    /// cannot recover it.
<a href=#469 id=469 data-nosnippet>469</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_create(
<a href=#470 id=470 data-nosnippet>470</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#471 id=471 data-nosnippet>471</a>        name: <span class="kw-2">&amp;</span>str,
<a href=#472 id=472 data-nosnippet>472</a>        description: <span class="prelude-ty">Option</span>&lt;<span class="kw-2">&amp;</span>str&gt;,
<a href=#473 id=473 data-nosnippet>473</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#474 id=474 data-nosnippet>474</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#475 id=475 data-nosnippet>475</a>            <span class="string">"{}/vault?key={}"</span>,
<a href=#476 id=476 data-nosnippet>476</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#477 id=477 data-nosnippet>477</a>            <span class="self">self</span>.api_key()
<a href=#478 id=478 data-nosnippet>478</a>        );
<a href=#479 id=479 data-nosnippet>479</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#480 id=480 data-nosnippet>480</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#481 id=481 data-nosnippet>481</a>        <span class="kw">let </span>body = <span class="macro">serde_json::json!</span>({
<a href=#482 id=482 data-nosnippet>482</a>            <span class="string">"name"</span>: name,
<a href=#483 id=483 data-nosnippet>483</a>            <span class="string">"description"</span>: description.unwrap_or(<span class="string">""</span>),
<a href=#484 id=484 data-nosnippet>484</a>        });
<a href=#485 id=485 data-nosnippet>485</a>        <span class="kw">let </span>body_bytes = serde_json::to_vec(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>;
<a href=#486 id=486 data-nosnippet>486</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#487 id=487 data-nosnippet>487</a>        headers.insert(CONTENT_TYPE, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#488 id=488 data-nosnippet>488</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#489 id=489 data-nosnippet>489</a>            </span>.send_with_retry(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(body_bytes))
<a href=#490 id=490 data-nosnippet>490</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#491 id=491 data-nosnippet>491</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#492 id=492 data-nosnippet>492</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#493 id=493 data-nosnippet>493</a>        <span class="kw">if </span>status != <span class="number">200 </span>&amp;&amp; status != <span class="number">201 </span>{
<a href=#494 id=494 data-nosnippet>494</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#495 id=495 data-nosnippet>495</a>        }
<a href=#496 id=496 data-nosnippet>496</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#497 id=497 data-nosnippet>497</a>    }
<a href=#498 id=498 data-nosnippet>498</a>
<a href=#499 id=499 data-nosnippet>499</a>    <span class="doccomment">/// List all credential vaults for the account.
<a href=#500 id=500 data-nosnippet>500</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_list(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#501 id=501 data-nosnippet>501</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#502 id=502 data-nosnippet>502</a>            <span class="string">"{}/vault?key={}"</span>,
<a href=#503 id=503 data-nosnippet>503</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#504 id=504 data-nosnippet>504</a>            <span class="self">self</span>.api_key()
<a href=#505 id=505 data-nosnippet>505</a>        );
<a href=#506 id=506 data-nosnippet>506</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#507 id=507 data-nosnippet>507</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#508 id=508 data-nosnippet>508</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#509 id=509 data-nosnippet>509</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#510 id=510 data-nosnippet>510</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#511 id=511 data-nosnippet>511</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#512 id=512 data-nosnippet>512</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#513 id=513 data-nosnippet>513</a>        }
<a href=#514 id=514 data-nosnippet>514</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#515 id=515 data-nosnippet>515</a>    }
<a href=#516 id=516 data-nosnippet>516</a>
<a href=#517 id=517 data-nosnippet>517</a>    <span class="doccomment">/// Get vault metadata. Does NOT include any secret material.
<a href=#518 id=518 data-nosnippet>518</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_get(
<a href=#519 id=519 data-nosnippet>519</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#520 id=520 data-nosnippet>520</a>        vault_id: <span class="kw-2">&amp;</span>str,
<a href=#521 id=521 data-nosnippet>521</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#522 id=522 data-nosnippet>522</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#523 id=523 data-nosnippet>523</a>            <span class="string">"{}/vault/{}?key={}"</span>,
<a href=#524 id=524 data-nosnippet>524</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#525 id=525 data-nosnippet>525</a>            vault_id,
<a href=#526 id=526 data-nosnippet>526</a>            <span class="self">self</span>.api_key()
<a href=#527 id=527 data-nosnippet>527</a>        );
<a href=#528 id=528 data-nosnippet>528</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#529 id=529 data-nosnippet>529</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#530 id=530 data-nosnippet>530</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#531 id=531 data-nosnippet>531</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#532 id=532 data-nosnippet>532</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#533 id=533 data-nosnippet>533</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#534 id=534 data-nosnippet>534</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#535 id=535 data-nosnippet>535</a>        }
<a href=#536 id=536 data-nosnippet>536</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#537 id=537 data-nosnippet>537</a>    }
<a href=#538 id=538 data-nosnippet>538</a>
<a href=#539 id=539 data-nosnippet>539</a>    <span class="doccomment">/// Rename a vault (and/or update its description). Only the
<a href=#540 id=540 data-nosnippet>540</a>    /// non-`None` fields are sent.
<a href=#541 id=541 data-nosnippet>541</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_update(
<a href=#542 id=542 data-nosnippet>542</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#543 id=543 data-nosnippet>543</a>        vault_id: <span class="kw-2">&amp;</span>str,
<a href=#544 id=544 data-nosnippet>544</a>        name: <span class="prelude-ty">Option</span>&lt;<span class="kw-2">&amp;</span>str&gt;,
<a href=#545 id=545 data-nosnippet>545</a>        description: <span class="prelude-ty">Option</span>&lt;<span class="kw-2">&amp;</span>str&gt;,
<a href=#546 id=546 data-nosnippet>546</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#547 id=547 data-nosnippet>547</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#548 id=548 data-nosnippet>548</a>            <span class="string">"{}/vault/{}?key={}"</span>,
<a href=#549 id=549 data-nosnippet>549</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#550 id=550 data-nosnippet>550</a>            vault_id,
<a href=#551 id=551 data-nosnippet>551</a>            <span class="self">self</span>.api_key()
<a href=#552 id=552 data-nosnippet>552</a>        );
<a href=#553 id=553 data-nosnippet>553</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#554 id=554 data-nosnippet>554</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#555 id=555 data-nosnippet>555</a>        <span class="kw">let </span><span class="kw-2">mut </span>body_map = serde_json::Map::new();
<a href=#556 id=556 data-nosnippet>556</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = name {
<a href=#557 id=557 data-nosnippet>557</a>            body_map.insert(<span class="string">"name"</span>.into(), serde_json::Value::String(v.into()));
<a href=#558 id=558 data-nosnippet>558</a>        }
<a href=#559 id=559 data-nosnippet>559</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(v) = description {
<a href=#560 id=560 data-nosnippet>560</a>            body_map.insert(<span class="string">"description"</span>.into(), serde_json::Value::String(v.into()));
<a href=#561 id=561 data-nosnippet>561</a>        }
<a href=#562 id=562 data-nosnippet>562</a>        <span class="kw">let </span>body_bytes = serde_json::to_vec(<span class="kw-2">&amp;</span>serde_json::Value::Object(body_map))<span class="question-mark">?</span>;
<a href=#563 id=563 data-nosnippet>563</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#564 id=564 data-nosnippet>564</a>        headers.insert(CONTENT_TYPE, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#565 id=565 data-nosnippet>565</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#566 id=566 data-nosnippet>566</a>            </span>.send_with_retry(Method::PATCH, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(body_bytes))
<a href=#567 id=567 data-nosnippet>567</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#568 id=568 data-nosnippet>568</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#569 id=569 data-nosnippet>569</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#570 id=570 data-nosnippet>570</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#571 id=571 data-nosnippet>571</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#572 id=572 data-nosnippet>572</a>        }
<a href=#573 id=573 data-nosnippet>573</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#574 id=574 data-nosnippet>574</a>    }
<a href=#575 id=575 data-nosnippet>575</a>
<a href=#576 id=576 data-nosnippet>576</a>    <span class="doccomment">/// Delete a vault by id (cascades to all items).
<a href=#577 id=577 data-nosnippet>577</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_delete(
<a href=#578 id=578 data-nosnippet>578</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#579 id=579 data-nosnippet>579</a>        vault_id: <span class="kw-2">&amp;</span>str,
<a href=#580 id=580 data-nosnippet>580</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#581 id=581 data-nosnippet>581</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#582 id=582 data-nosnippet>582</a>            <span class="string">"{}/vault/{}?key={}"</span>,
<a href=#583 id=583 data-nosnippet>583</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#584 id=584 data-nosnippet>584</a>            vault_id,
<a href=#585 id=585 data-nosnippet>585</a>            <span class="self">self</span>.api_key()
<a href=#586 id=586 data-nosnippet>586</a>        );
<a href=#587 id=587 data-nosnippet>587</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#588 id=588 data-nosnippet>588</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#589 id=589 data-nosnippet>589</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#590 id=590 data-nosnippet>590</a>            </span>.send_with_retry(Method::DELETE, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>)
<a href=#591 id=591 data-nosnippet>591</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#592 id=592 data-nosnippet>592</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#593 id=593 data-nosnippet>593</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#594 id=594 data-nosnippet>594</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#595 id=595 data-nosnippet>595</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#596 id=596 data-nosnippet>596</a>        }
<a href=#597 id=597 data-nosnippet>597</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#598 id=598 data-nosnippet>598</a>    }
<a href=#599 id=599 data-nosnippet>599</a>
<a href=#600 id=600 data-nosnippet>600</a>    <span class="doccomment">/// Rotate a vault's key. Requires the CURRENT key in the
<a href=#601 id=601 data-nosnippet>601</a>    /// `X-Vault-Key` header; the server returns a fresh key in the
<a href=#602 id=602 data-nosnippet>602</a>    /// response body. After this call the old key is permanently
<a href=#603 id=603 data-nosnippet>603</a>    /// invalid for this vault. The current key is forwarded to the
<a href=#604 id=604 data-nosnippet>604</a>    /// server transiently and never logged by the SDK.
<a href=#605 id=605 data-nosnippet>605</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_rotate(
<a href=#606 id=606 data-nosnippet>606</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#607 id=607 data-nosnippet>607</a>        vault_id: <span class="kw-2">&amp;</span>str,
<a href=#608 id=608 data-nosnippet>608</a>        current_vault_key: <span class="kw-2">&amp;</span>str,
<a href=#609 id=609 data-nosnippet>609</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#610 id=610 data-nosnippet>610</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#611 id=611 data-nosnippet>611</a>            <span class="string">"{}/vault/{}/rotate?key={}"</span>,
<a href=#612 id=612 data-nosnippet>612</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#613 id=613 data-nosnippet>613</a>            vault_id,
<a href=#614 id=614 data-nosnippet>614</a>            <span class="self">self</span>.api_key()
<a href=#615 id=615 data-nosnippet>615</a>        );
<a href=#616 id=616 data-nosnippet>616</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#617 id=617 data-nosnippet>617</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#618 id=618 data-nosnippet>618</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#619 id=619 data-nosnippet>619</a>        headers.insert(
<a href=#620 id=620 data-nosnippet>620</a>            <span class="string">"X-Vault-Key"</span>,
<a href=#621 id=621 data-nosnippet>621</a>            HeaderValue::from_str(current_vault_key)
<a href=#622 id=622 data-nosnippet>622</a>                <span class="comment">// Generic message — never echo the key value back.
<a href=#623 id=623 data-nosnippet>623</a>                </span>.map_err(|<span class="kw">_</span>| ScrapflyError::Config(<span class="string">"X-Vault-Key contained invalid bytes"</span>.into()))<span class="question-mark">?</span>,
<a href=#624 id=624 data-nosnippet>624</a>        );
<a href=#625 id=625 data-nosnippet>625</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#626 id=626 data-nosnippet>626</a>            </span>.send_with_retry(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">None</span>)
<a href=#627 id=627 data-nosnippet>627</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#628 id=628 data-nosnippet>628</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#629 id=629 data-nosnippet>629</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#630 id=630 data-nosnippet>630</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#631 id=631 data-nosnippet>631</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#632 id=632 data-nosnippet>632</a>        }
<a href=#633 id=633 data-nosnippet>633</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#634 id=634 data-nosnippet>634</a>    }
<a href=#635 id=635 data-nosnippet>635</a>
<a href=#636 id=636 data-nosnippet>636</a>    <span class="doccomment">/// List items in a vault. Items include the encrypted `secret_blob`
<a href=#637 id=637 data-nosnippet>637</a>    /// but not plaintext — the blob is meaningless without the
<a href=#638 id=638 data-nosnippet>638</a>    /// customer-held key.
<a href=#639 id=639 data-nosnippet>639</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_item_list(
<a href=#640 id=640 data-nosnippet>640</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#641 id=641 data-nosnippet>641</a>        vault_id: <span class="kw-2">&amp;</span>str,
<a href=#642 id=642 data-nosnippet>642</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#643 id=643 data-nosnippet>643</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#644 id=644 data-nosnippet>644</a>            <span class="string">"{}/vault/{}/item?key={}"</span>,
<a href=#645 id=645 data-nosnippet>645</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#646 id=646 data-nosnippet>646</a>            vault_id,
<a href=#647 id=647 data-nosnippet>647</a>            <span class="self">self</span>.api_key()
<a href=#648 id=648 data-nosnippet>648</a>        );
<a href=#649 id=649 data-nosnippet>649</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#650 id=650 data-nosnippet>650</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#651 id=651 data-nosnippet>651</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#652 id=652 data-nosnippet>652</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#653 id=653 data-nosnippet>653</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#654 id=654 data-nosnippet>654</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#655 id=655 data-nosnippet>655</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#656 id=656 data-nosnippet>656</a>        }
<a href=#657 id=657 data-nosnippet>657</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#658 id=658 data-nosnippet>658</a>    }
<a href=#659 id=659 data-nosnippet>659</a>
<a href=#660 id=660 data-nosnippet>660</a>    <span class="doccomment">/// Create a new item in a vault. The vault key is required to
<a href=#661 id=661 data-nosnippet>661</a>    /// envelope-encrypt the per-item DEK; it is forwarded transiently
<a href=#662 id=662 data-nosnippet>662</a>    /// in the `X-Vault-Key` header. Caller-supplied `item` JSON shape
<a href=#663 id=663 data-nosnippet>663</a>    /// follows the documented contract (see itemCreateRequest in
<a href=#664 id=664 data-nosnippet>664</a>    /// pkg/vault/controller.go) — typically:
<a href=#665 id=665 data-nosnippet>665</a>    ///
<a href=#666 id=666 data-nosnippet>666</a>    /// ```json
<a href=#667 id=667 data-nosnippet>667</a>    /// {
<a href=#668 id=668 data-nosnippet>668</a>    ///   "type": "password",
<a href=#669 id=669 data-nosnippet>669</a>    ///   "label": "...",
<a href=#670 id=670 data-nosnippet>670</a>    ///   "origin": "https://example.com",
<a href=#671 id=671 data-nosnippet>671</a>    ///   "username": "user",
<a href=#672 id=672 data-nosnippet>672</a>    ///   "secret": {"password": "hunter2"}
<a href=#673 id=673 data-nosnippet>673</a>    /// }
<a href=#674 id=674 data-nosnippet>674</a>    /// ```
<a href=#675 id=675 data-nosnippet>675</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_item_create(
<a href=#676 id=676 data-nosnippet>676</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#677 id=677 data-nosnippet>677</a>        vault_id: <span class="kw-2">&amp;</span>str,
<a href=#678 id=678 data-nosnippet>678</a>        vault_key: <span class="kw-2">&amp;</span>str,
<a href=#679 id=679 data-nosnippet>679</a>        item: serde_json::Value,
<a href=#680 id=680 data-nosnippet>680</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#681 id=681 data-nosnippet>681</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#682 id=682 data-nosnippet>682</a>            <span class="string">"{}/vault/{}/item?key={}"</span>,
<a href=#683 id=683 data-nosnippet>683</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#684 id=684 data-nosnippet>684</a>            vault_id,
<a href=#685 id=685 data-nosnippet>685</a>            <span class="self">self</span>.api_key()
<a href=#686 id=686 data-nosnippet>686</a>        );
<a href=#687 id=687 data-nosnippet>687</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#688 id=688 data-nosnippet>688</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#689 id=689 data-nosnippet>689</a>        <span class="kw">let </span>body_bytes = serde_json::to_vec(<span class="kw-2">&amp;</span>item)<span class="question-mark">?</span>;
<a href=#690 id=690 data-nosnippet>690</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#691 id=691 data-nosnippet>691</a>        headers.insert(CONTENT_TYPE, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#692 id=692 data-nosnippet>692</a>        headers.insert(
<a href=#693 id=693 data-nosnippet>693</a>            <span class="string">"X-Vault-Key"</span>,
<a href=#694 id=694 data-nosnippet>694</a>            HeaderValue::from_str(vault_key)
<a href=#695 id=695 data-nosnippet>695</a>                .map_err(|<span class="kw">_</span>| ScrapflyError::Config(<span class="string">"X-Vault-Key contained invalid bytes"</span>.into()))<span class="question-mark">?</span>,
<a href=#696 id=696 data-nosnippet>696</a>        );
<a href=#697 id=697 data-nosnippet>697</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#698 id=698 data-nosnippet>698</a>            </span>.send_with_retry(Method::POST, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(body_bytes))
<a href=#699 id=699 data-nosnippet>699</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#700 id=700 data-nosnippet>700</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#701 id=701 data-nosnippet>701</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#702 id=702 data-nosnippet>702</a>        <span class="kw">if </span>status != <span class="number">200 </span>&amp;&amp; status != <span class="number">201 </span>{
<a href=#703 id=703 data-nosnippet>703</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#704 id=704 data-nosnippet>704</a>        }
<a href=#705 id=705 data-nosnippet>705</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#706 id=706 data-nosnippet>706</a>    }
<a href=#707 id=707 data-nosnippet>707</a>
<a href=#708 id=708 data-nosnippet>708</a>    <span class="doccomment">/// Patch an existing item. `vault_key` is REQUIRED iff the patch
<a href=#709 id=709 data-nosnippet>709</a>    /// rotates the secret payload (i.e. `secret` is present in
<a href=#710 id=710 data-nosnippet>710</a>    /// `patch`); for pure metadata edits (label, origin, username,
<a href=#711 id=711 data-nosnippet>711</a>    /// metadata) the server accepts the request without an
<a href=#712 id=712 data-nosnippet>712</a>    /// `X-Vault-Key` header. The SDK forwards the key only when
<a href=#713 id=713 data-nosnippet>713</a>    /// supplied — it does not auto-detect the patch shape.
<a href=#714 id=714 data-nosnippet>714</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_item_update(
<a href=#715 id=715 data-nosnippet>715</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#716 id=716 data-nosnippet>716</a>        vault_id: <span class="kw-2">&amp;</span>str,
<a href=#717 id=717 data-nosnippet>717</a>        item_id: <span class="kw-2">&amp;</span>str,
<a href=#718 id=718 data-nosnippet>718</a>        vault_key: <span class="prelude-ty">Option</span>&lt;<span class="kw-2">&amp;</span>str&gt;,
<a href=#719 id=719 data-nosnippet>719</a>        patch: serde_json::Value,
<a href=#720 id=720 data-nosnippet>720</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#721 id=721 data-nosnippet>721</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#722 id=722 data-nosnippet>722</a>            <span class="string">"{}/vault/{}/item/{}?key={}"</span>,
<a href=#723 id=723 data-nosnippet>723</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#724 id=724 data-nosnippet>724</a>            vault_id,
<a href=#725 id=725 data-nosnippet>725</a>            item_id,
<a href=#726 id=726 data-nosnippet>726</a>            <span class="self">self</span>.api_key()
<a href=#727 id=727 data-nosnippet>727</a>        );
<a href=#728 id=728 data-nosnippet>728</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#729 id=729 data-nosnippet>729</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#730 id=730 data-nosnippet>730</a>        <span class="kw">let </span>body_bytes = serde_json::to_vec(<span class="kw-2">&amp;</span>patch)<span class="question-mark">?</span>;
<a href=#731 id=731 data-nosnippet>731</a>        <span class="kw">let </span><span class="kw-2">mut </span>headers = HeaderMap::new();
<a href=#732 id=732 data-nosnippet>732</a>        headers.insert(CONTENT_TYPE, HeaderValue::from_static(<span class="string">"application/json"</span>));
<a href=#733 id=733 data-nosnippet>733</a>        <span class="kw">if let </span><span class="prelude-val">Some</span>(k) = vault_key {
<a href=#734 id=734 data-nosnippet>734</a>            headers.insert(
<a href=#735 id=735 data-nosnippet>735</a>                <span class="string">"X-Vault-Key"</span>,
<a href=#736 id=736 data-nosnippet>736</a>                HeaderValue::from_str(k).map_err(|<span class="kw">_</span>| {
<a href=#737 id=737 data-nosnippet>737</a>                    ScrapflyError::Config(<span class="string">"X-Vault-Key contained invalid bytes"</span>.into())
<a href=#738 id=738 data-nosnippet>738</a>                })<span class="question-mark">?</span>,
<a href=#739 id=739 data-nosnippet>739</a>            );
<a href=#740 id=740 data-nosnippet>740</a>        }
<a href=#741 id=741 data-nosnippet>741</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#742 id=742 data-nosnippet>742</a>            </span>.send_with_retry(Method::PATCH, url, <span class="prelude-val">Some</span>(headers), <span class="prelude-val">Some</span>(body_bytes))
<a href=#743 id=743 data-nosnippet>743</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#744 id=744 data-nosnippet>744</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#745 id=745 data-nosnippet>745</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#746 id=746 data-nosnippet>746</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#747 id=747 data-nosnippet>747</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#748 id=748 data-nosnippet>748</a>        }
<a href=#749 id=749 data-nosnippet>749</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#750 id=750 data-nosnippet>750</a>    }
<a href=#751 id=751 data-nosnippet>751</a>
<a href=#752 id=752 data-nosnippet>752</a>    <span class="doccomment">/// Delete a single vault item.
<a href=#753 id=753 data-nosnippet>753</a>    </span><span class="kw">pub async fn </span>cloud_browser_vault_item_delete(
<a href=#754 id=754 data-nosnippet>754</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#755 id=755 data-nosnippet>755</a>        vault_id: <span class="kw-2">&amp;</span>str,
<a href=#756 id=756 data-nosnippet>756</a>        item_id: <span class="kw-2">&amp;</span>str,
<a href=#757 id=757 data-nosnippet>757</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#758 id=758 data-nosnippet>758</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#759 id=759 data-nosnippet>759</a>            <span class="string">"{}/vault/{}/item/{}?key={}"</span>,
<a href=#760 id=760 data-nosnippet>760</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#761 id=761 data-nosnippet>761</a>            vault_id,
<a href=#762 id=762 data-nosnippet>762</a>            item_id,
<a href=#763 id=763 data-nosnippet>763</a>            <span class="self">self</span>.api_key()
<a href=#764 id=764 data-nosnippet>764</a>        );
<a href=#765 id=765 data-nosnippet>765</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#766 id=766 data-nosnippet>766</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid vault url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#767 id=767 data-nosnippet>767</a>        <span class="kw">let </span>resp = <span class="self">self
<a href=#768 id=768 data-nosnippet>768</a>            </span>.send_with_retry(Method::DELETE, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>)
<a href=#769 id=769 data-nosnippet>769</a>            .<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#770 id=770 data-nosnippet>770</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#771 id=771 data-nosnippet>771</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#772 id=772 data-nosnippet>772</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#773 id=773 data-nosnippet>773</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#774 id=774 data-nosnippet>774</a>        }
<a href=#775 id=775 data-nosnippet>775</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#776 id=776 data-nosnippet>776</a>    }
<a href=#777 id=777 data-nosnippet>777</a>
<a href=#778 id=778 data-nosnippet>778</a>    <span class="doccomment">/// Get debug recording playback metadata for a run. The response carries
<a href=#779 id=779 data-nosnippet>779</a>    /// `available`, `status` (one of `ready`, `uploading`, `unavailable`,
<a href=#780 id=780 data-nosnippet>780</a>    /// `disabled`), `metadata`, `video_url`, and `retry_after_ms`.
<a href=#781 id=781 data-nosnippet>781</a>    </span><span class="kw">pub async fn </span>cloud_browser_playback(
<a href=#782 id=782 data-nosnippet>782</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#783 id=783 data-nosnippet>783</a>        run_id: <span class="kw-2">&amp;</span>str,
<a href=#784 id=784 data-nosnippet>784</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#785 id=785 data-nosnippet>785</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#786 id=786 data-nosnippet>786</a>            <span class="string">"{}/run/{}/playback?key={}"</span>,
<a href=#787 id=787 data-nosnippet>787</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#788 id=788 data-nosnippet>788</a>            run_id,
<a href=#789 id=789 data-nosnippet>789</a>            <span class="self">self</span>.api_key()
<a href=#790 id=790 data-nosnippet>790</a>        );
<a href=#791 id=791 data-nosnippet>791</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#792 id=792 data-nosnippet>792</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid playback url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#793 id=793 data-nosnippet>793</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#794 id=794 data-nosnippet>794</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#795 id=795 data-nosnippet>795</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#796 id=796 data-nosnippet>796</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#797 id=797 data-nosnippet>797</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#798 id=798 data-nosnippet>798</a>        }
<a href=#799 id=799 data-nosnippet>799</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#800 id=800 data-nosnippet>800</a>    }
<a href=#801 id=801 data-nosnippet>801</a>
<a href=#802 id=802 data-nosnippet>802</a>    <span class="doccomment">/// Poll the playback endpoint until the recording resolves to a terminal
<a href=#803 id=803 data-nosnippet>803</a>    /// state (`ready` or `unavailable`) or the timeout elapses. Honours the
<a href=#804 id=804 data-nosnippet>804</a>    /// server's `retry_after_ms` hint when present.
<a href=#805 id=805 data-nosnippet>805</a>    </span><span class="kw">pub async fn </span>cloud_browser_wait_for_playback(
<a href=#806 id=806 data-nosnippet>806</a>        <span class="kw-2">&amp;</span><span class="self">self</span>,
<a href=#807 id=807 data-nosnippet>807</a>        run_id: <span class="kw-2">&amp;</span>str,
<a href=#808 id=808 data-nosnippet>808</a>        timeout: std::time::Duration,
<a href=#809 id=809 data-nosnippet>809</a>        fallback_interval: std::time::Duration,
<a href=#810 id=810 data-nosnippet>810</a>    ) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#811 id=811 data-nosnippet>811</a>        <span class="kw">let </span>deadline = std::time::Instant::now() + timeout;
<a href=#812 id=812 data-nosnippet>812</a>        <span class="kw">loop </span>{
<a href=#813 id=813 data-nosnippet>813</a>            <span class="kw">let </span>playback = <span class="self">self</span>.cloud_browser_playback(run_id).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#814 id=814 data-nosnippet>814</a>            <span class="kw">let </span>status = playback
<a href=#815 id=815 data-nosnippet>815</a>                .get(<span class="string">"status"</span>)
<a href=#816 id=816 data-nosnippet>816</a>                .and_then(|v| v.as_str())
<a href=#817 id=817 data-nosnippet>817</a>                .unwrap_or(<span class="string">""</span>);
<a href=#818 id=818 data-nosnippet>818</a>            <span class="kw">if </span>status != <span class="string">"uploading" </span>{
<a href=#819 id=819 data-nosnippet>819</a>                <span class="kw">return </span><span class="prelude-val">Ok</span>(playback);
<a href=#820 id=820 data-nosnippet>820</a>            }
<a href=#821 id=821 data-nosnippet>821</a>            <span class="kw">let </span>remaining = deadline.saturating_duration_since(std::time::Instant::now());
<a href=#822 id=822 data-nosnippet>822</a>            <span class="kw">if </span>remaining.is_zero() {
<a href=#823 id=823 data-nosnippet>823</a>                <span class="kw">return </span><span class="prelude-val">Ok</span>(playback);
<a href=#824 id=824 data-nosnippet>824</a>            }
<a href=#825 id=825 data-nosnippet>825</a>            <span class="kw">let </span>retry_after_ms = playback
<a href=#826 id=826 data-nosnippet>826</a>                .get(<span class="string">"retry_after_ms"</span>)
<a href=#827 id=827 data-nosnippet>827</a>                .and_then(|v| v.as_u64())
<a href=#828 id=828 data-nosnippet>828</a>                .unwrap_or(<span class="number">0</span>);
<a href=#829 id=829 data-nosnippet>829</a>            <span class="kw">let </span>next = <span class="kw">if </span>retry_after_ms &gt; <span class="number">0 </span>{
<a href=#830 id=830 data-nosnippet>830</a>                std::time::Duration::from_millis(retry_after_ms)
<a href=#831 id=831 data-nosnippet>831</a>            } <span class="kw">else </span>{
<a href=#832 id=832 data-nosnippet>832</a>                fallback_interval
<a href=#833 id=833 data-nosnippet>833</a>            };
<a href=#834 id=834 data-nosnippet>834</a>            tokio::time::sleep(next.min(remaining)).<span class="kw">await</span>;
<a href=#835 id=835 data-nosnippet>835</a>        }
<a href=#836 id=836 data-nosnippet>836</a>    }
<a href=#837 id=837 data-nosnippet>837</a>
<a href=#838 id=838 data-nosnippet>838</a>    <span class="doccomment">/// Terminate a Cloud Browser session.
<a href=#839 id=839 data-nosnippet>839</a>    </span><span class="kw">pub async fn </span>cloud_browser_session_stop(<span class="kw-2">&amp;</span><span class="self">self</span>, session_id: <span class="kw-2">&amp;</span>str) -&gt; <span class="prelude-ty">Result</span>&lt;(), ScrapflyError&gt; {
<a href=#840 id=840 data-nosnippet>840</a>        <span class="kw">if </span>session_id.is_empty() {
<a href=#841 id=841 data-nosnippet>841</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(ScrapflyError::Config(<span class="string">"session_id is required"</span>.into()));
<a href=#842 id=842 data-nosnippet>842</a>        }
<a href=#843 id=843 data-nosnippet>843</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#844 id=844 data-nosnippet>844</a>            <span class="string">"{}/session/{}/stop?key={}"</span>,
<a href=#845 id=845 data-nosnippet>845</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#846 id=846 data-nosnippet>846</a>            session_id,
<a href=#847 id=847 data-nosnippet>847</a>            <span class="self">self</span>.api_key()
<a href=#848 id=848 data-nosnippet>848</a>        );
<a href=#849 id=849 data-nosnippet>849</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#850 id=850 data-nosnippet>850</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid session url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#851 id=851 data-nosnippet>851</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::POST, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#852 id=852 data-nosnippet>852</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#853 id=853 data-nosnippet>853</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#854 id=854 data-nosnippet>854</a>            <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#855 id=855 data-nosnippet>855</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#856 id=856 data-nosnippet>856</a>        }
<a href=#857 id=857 data-nosnippet>857</a>        <span class="prelude-val">Ok</span>(())
<a href=#858 id=858 data-nosnippet>858</a>    }
<a href=#859 id=859 data-nosnippet>859</a>
<a href=#860 id=860 data-nosnippet>860</a>    <span class="doccomment">/// List all running Cloud Browser sessions.
<a href=#861 id=861 data-nosnippet>861</a>    </span><span class="kw">pub async fn </span>cloud_browser_sessions(<span class="kw-2">&amp;</span><span class="self">self</span>) -&gt; <span class="prelude-ty">Result</span>&lt;serde_json::Value, ScrapflyError&gt; {
<a href=#862 id=862 data-nosnippet>862</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#863 id=863 data-nosnippet>863</a>            <span class="string">"{}/sessions?key={}"</span>,
<a href=#864 id=864 data-nosnippet>864</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#865 id=865 data-nosnippet>865</a>            <span class="self">self</span>.api_key()
<a href=#866 id=866 data-nosnippet>866</a>        );
<a href=#867 id=867 data-nosnippet>867</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#868 id=868 data-nosnippet>868</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid sessions url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#869 id=869 data-nosnippet>869</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#870 id=870 data-nosnippet>870</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#871 id=871 data-nosnippet>871</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#872 id=872 data-nosnippet>872</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#873 id=873 data-nosnippet>873</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#874 id=874 data-nosnippet>874</a>        }
<a href=#875 id=875 data-nosnippet>875</a>        <span class="prelude-val">Ok</span>(serde_json::from_slice(<span class="kw-2">&amp;</span>body)<span class="question-mark">?</span>)
<a href=#876 id=876 data-nosnippet>876</a>    }
<a href=#877 id=877 data-nosnippet>877</a>
<a href=#878 id=878 data-nosnippet>878</a>    <span class="doccomment">/// Download a debug session recording video (raw bytes).
<a href=#879 id=879 data-nosnippet>879</a>    </span><span class="kw">pub async fn </span>cloud_browser_video(<span class="kw-2">&amp;</span><span class="self">self</span>, run_id: <span class="kw-2">&amp;</span>str) -&gt; <span class="prelude-ty">Result</span>&lt;Vec&lt;u8&gt;, ScrapflyError&gt; {
<a href=#880 id=880 data-nosnippet>880</a>        <span class="kw">let </span>url = <span class="macro">format!</span>(
<a href=#881 id=881 data-nosnippet>881</a>            <span class="string">"{}/run/{}/video?key={}"</span>,
<a href=#882 id=882 data-nosnippet>882</a>            rest_base(<span class="self">self</span>.cloud_browser_host()),
<a href=#883 id=883 data-nosnippet>883</a>            run_id,
<a href=#884 id=884 data-nosnippet>884</a>            <span class="self">self</span>.api_key()
<a href=#885 id=885 data-nosnippet>885</a>        );
<a href=#886 id=886 data-nosnippet>886</a>        <span class="kw">let </span>url = Url::parse(<span class="kw-2">&amp;</span>url)
<a href=#887 id=887 data-nosnippet>887</a>            .map_err(|e| ScrapflyError::Config(<span class="macro">format!</span>(<span class="string">"invalid video url: {}"</span>, e)))<span class="question-mark">?</span>;
<a href=#888 id=888 data-nosnippet>888</a>        <span class="kw">let </span>resp = <span class="self">self</span>.send_with_retry(Method::GET, url, <span class="prelude-val">None</span>, <span class="prelude-val">None</span>).<span class="kw">await</span><span class="question-mark">?</span>;
<a href=#889 id=889 data-nosnippet>889</a>        <span class="kw">let </span>status = resp.status().as_u16();
<a href=#890 id=890 data-nosnippet>890</a>        <span class="kw">let </span>body = resp.bytes().<span class="kw">await</span>.map_err(ScrapflyError::Transport)<span class="question-mark">?</span>;
<a href=#891 id=891 data-nosnippet>891</a>        <span class="kw">if </span>status != <span class="number">200 </span>{
<a href=#892 id=892 data-nosnippet>892</a>            <span class="kw">return </span><span class="prelude-val">Err</span>(from_response(status, <span class="kw-2">&amp;</span>body, <span class="number">0</span>, <span class="bool-val">false</span>));
<a href=#893 id=893 data-nosnippet>893</a>        }
<a href=#894 id=894 data-nosnippet>894</a>        <span class="prelude-val">Ok</span>(body.to_vec())
<a href=#895 id=895 data-nosnippet>895</a>    }
<a href=#896 id=896 data-nosnippet>896</a>}
</code></pre></div></section></main></body></html>