Skip to content

Browsing Isolation API

Create and manage secure Silo browsing contexts. Requires ADMIN_TOKEN.

See Authentication for token setup and Wire Protocol for how requests are structured.

Bases: BaseAPIClient

API client for Silo browsing isolation operations.

This class provides methods for creating, managing, and deleting secure browsing session contexts using the Authentic8 Silo platform.

The browsing isolation API allows you to: - Create secure browsing contexts with custom policies - Retrieve context information and status - Delete contexts when no longer needed - Bulk create multiple contexts - Generate launch URLs for contexts

Note

Available policy types (not exhaustive — see MkDocs browsing policy reference for full param lists):

  • readonly: Make session read-only ("true" / "false")
  • file_transfer: File upload/download control ("block_all", "allow_all")
  • clipboard: Clipboard direction control ("block_all", "allow_all", "allow_to_local", "block_to_silo")
  • ad_block: Ad blocking ("enable", "disable")
  • browser_chrome: UI mode for the Silo ribbon/chrome. "standard" — default ribbon UI; "seamless" — hides the ribbon (non-catchall users only; catchall users are always forced to "minimal" regardless of this setting); "minimal" — minimal ribbon mode (Ribbon Mode)
  • domain_allow: Whitelist specific domains (added automatically when url is provided)
  • domain_block: Blacklist specific domains
  • ribbon_background_color: HUD banner background (hex string)
  • ribbon_text_color: HUD banner text color (hex string)
  • ribbon_message: HUD banner message (max 100 chars)
Example

from silo_sdk import BrowsingAPI, load_config config = load_config() browsing = BrowsingAPI(config)

Create a context

context_id = browsing.create_context( ... url="https://example.com", ... username="user@company.com", ... policy=[{"type": "readonly", "params": ["true"]}] ... )

Generate launch URL

launch_url = browsing.create_ctx_url(context_id) print(f"Launch URL: {launch_url}")

Source code in silo_sdk/browsing/isolation_api.py
 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
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
class BrowsingAPI(BaseAPIClient):
    """API client for Silo browsing isolation operations.

    This class provides methods for creating, managing, and deleting secure
    browsing session contexts using the Authentic8 Silo platform.

    The browsing isolation API allows you to:
    - Create secure browsing contexts with custom policies
    - Retrieve context information and status
    - Delete contexts when no longer needed
    - Bulk create multiple contexts
    - Generate launch URLs for contexts

    Note:
        **Available policy types** (not exhaustive — see MkDocs browsing
        policy reference for full param lists):

        - ``readonly``: Make session read-only (``"true"`` / ``"false"``)
        - ``file_transfer``: File upload/download control
          (``"block_all"``, ``"allow_all"``)
        - ``clipboard``: Clipboard direction control
          (``"block_all"``, ``"allow_all"``, ``"allow_to_local"``,
          ``"block_to_silo"``)
        - ``ad_block``: Ad blocking (``"enable"``, ``"disable"``)
        - ``browser_chrome``: UI mode for the Silo ribbon/chrome.
          ``"standard"`` — default ribbon UI;
          ``"seamless"`` — hides the ribbon (**non-catchall users only**;
          catchall users are always forced to ``"minimal"``
          regardless of this setting);
          ``"minimal"`` — minimal ribbon mode (Ribbon Mode)
        - ``domain_allow``: Whitelist specific domains (added automatically
          when ``url`` is provided)
        - ``domain_block``: Blacklist specific domains
        - ``ribbon_background_color``: HUD banner background (hex string)
        - ``ribbon_text_color``: HUD banner text color (hex string)
        - ``ribbon_message``: HUD banner message (max 100 chars)

    Example:
        >>> from silo_sdk import BrowsingAPI, load_config
        >>> config = load_config()
        >>> browsing = BrowsingAPI(config)
        >>>
        >>> # Create a context
        >>> context_id = browsing.create_context(
        ...     url="https://example.com",
        ...     username="user@company.com",
        ...     policy=[{"type": "readonly", "params": ["true"]}]
        ... )
        >>>
        >>> # Generate launch URL
        >>> launch_url = browsing.create_ctx_url(context_id)
        >>> print(f"Launch URL: {launch_url}")
    """

    def __init__(self, config: dict[str, Any]) -> None:
        """Initialize the browsing API client.

        Args:
            config: Configuration dictionary containing API settings

        Raises:
            ConfigurationError: If required configuration is missing
        """
        super().__init__(config)

        # Validate required configuration
        validate_config_for_api(config, ["ADMIN_TOKEN"])

        self.auth_token = config["ADMIN_TOKEN"]
        self.logger.info("Initialized BrowsingAPI client")

    @api_tag(MethodType.API_COMMAND, api_command="create_context")
    def create_context(
        self,
        url: str | None = None,
        username: str | None = None,
        policy: list[dict[str, Any]] | None = None,
        max_uses: int | None = None,
        expires: str | None = None,
        restrict_client_ips: list[str] | None = None,
        org: str | None = None,
        session_type: Literal["silo-ruby", "toolbox-standalone"] | None = None,
        egress_region: str | None = None,
        browser_profile: BrowserProfile | None = None,
        categories: Categories | None = None,
        auto_domain_allow: bool = False,
    ) -> str:
        """Create a new browsing context.

        Creates a secure browsing context that can be used to launch isolated
        browsing sessions with the specified URL and policies.

        Args:
            url: Target URL for the context (optional for toolbox-standalone).
                Sent as ``"urls": [url]`` (array) in the wire format. The domain
                is automatically extracted and added as a ``domain_allow`` policy
                entry to the provided policy list.
            username: Username associated with the context.
                Either username or org must be provided (but not both). Sent as
                ``"user"`` in the wire format (field name differs from this param).
            policy: List of policy dictionaries, each with ``"type"`` and
                ``"params"`` keys. See class-level ``Attributes`` for available
                policy types and valid param values.

                **Seamless UI constraint:** ``{"type": "browser_chrome",
                "params": ["seamless"]}`` is only effective for
                **non-catchall users**. Catchall users — via a catchall
                account, an org configured to treat all users as catchall, or
                partner SSO — are always forced to ``"minimal"`` (Ribbon Mode)
                regardless of this policy setting.
            max_uses: Maximum number of times the context can be used.
                Sent at the **command level** (not inside ``context_data``).
            expires: Context expiration time (epoch time, ISO8601, or offset).
                Sent at the **command level** (not inside ``context_data``),
                same as ``max_uses``.
            restrict_client_ips: List of IP addresses/CIDRs to restrict access.
                Sent as ``"restrict_ips"`` in the wire format (field name differs
                from this param).
            org: Org slug/vanity URL for SAML-enabled organizations.
                Either username or org must be provided (but not both).
            session_type: Session type — ``"silo-ruby"`` (default, Safe Access)
                or ``"toolbox-standalone"`` (Silo for Research).
            egress_region: Egress location name from the managed attribution
                network. Accepts a specific city (e.g., ``"New York, NY"``),
                a country (``"United States"``), a region (``"North America"``),
                or ``"World"`` for random assignment. The backend resolves
                via ``hierarchy_tag`` lookup. See
                :data:`silo_sdk.harvesting.egress_validation.VALID_EGRESS_NAMES`
                for the complete list.

                **Important — mixed licensing:** Even if a location passes SDK
                validation, the user may receive an egress error when the browsing
                context is launched if their license does not include rights to that
                specific location. SDK validation confirms the location name is valid
                for the platform's shared network, but per-user license entitlements
                are enforced at session launch time, not at context creation.
            browser_profile: Browser profile dict for user-agent selection.
                Keys: ``"os"``, ``"browser"``, and optionally ``"timezone"``
                (IANA name) and ``"languages"`` (Accept-Language string).
            categories: Egress category settings with ``"availability"``,
                ``"connectivity"``, and ``"protocol"`` keys. ``"connectivity"``
                and ``"protocol"`` are validated against what is actually
                available at ``egress_region`` (or any location, if
                ``egress_region`` is omitted) — see
                :meth:`get_egress_locations` for per-location details.
            auto_domain_allow: If ``True``, automatically append a
                ``domain_allow`` policy entry for the URL's domain when
                ``url`` is provided. Defaults to ``False``.

                Leave this ``False`` (the default) if you want to construct
                your own ``domain_allow`` policy entry — for example, to
                allow multiple domains in a single entry::

                    policy=[{
                        "type": "domain_allow",
                        "params": ["google.com", "yahoo.com"]
                    }]

                When ``auto_domain_allow=True``, only the domain extracted
                from ``url`` is injected. It cannot be used to allow
                additional domains beyond the target URL's domain.

        Note:
            Wire format parameter mappings:

            - Python ``username`` → wire ``"user"``
              (inside ``context_data``)
            - Python ``url`` (string) → wire ``"urls"`` (list,
              inside ``context_data``)
            - ``max_uses`` and ``expires`` go at the **command level**
              (alongside ``"command"``, not inside ``context_data``)
            - ``domain_allow`` is only added when ``auto_domain_allow=True``

            Wire format structure::

                {
                    "command": "create_context",
                    "max_uses": 5,       # ← command level
                    "expires": "2026-12-31T23:59:59Z",  # ← command level
                    "context_data": {
                        "user": "user@company.com",  # ← username → "user"
                        "urls": ["https://example.com"],  # ← url → "urls" (list)
                        "policy": [...]
                    }
                }

        Returns:
            Browse context ID (``browse_context_id``) that can be used to launch
            sessions. Pass this to :meth:`create_ctx_url` to get a launch URL, or
            use the GET ``/ctx/`` shorthand endpoint directly (see Note).

        Raises:
            BrowsingAPIError: If context creation fails
            ValidationError: If parameters are invalid

        Note:
            **GET /ctx shorthand:** As an alternative to the two-step
            create → launch flow, the ``/ctx/`` endpoint at
            ``https://extapi.authentic8.com/ctx/`` creates and launches a context
            in a single GET request using inline query parameters::

                GET https://extapi.authentic8.com/ctx/
                    ?auth=<admin_token>
                    &user=<username>
                    &url=<target_url>
                    &response=url

            The ``response`` parameter controls the return format:

            - ``redirect`` (default) — HTTP redirect directly into the Silo session
            - ``url`` — returns the launch URL as plain text (useful for automation)
            - ``id`` — returns just the context launch ID as plain text

            This shorthand does **not** go through the ``/api/`` command-array format.

        Example:
            >>> # Basic isolation context
            >>> policy = [
            ...     {"type": "file_transfer", "params": ["block_all"]},
            ...     {"type": "clipboard", "params": ["allow_to_local"]},
            ... ]
            >>> context_id = api.create_context(
            ...     url="https://example.com",
            ...     username="user@company.com",
            ...     policy=policy,
            ...     max_uses=5
            ... )

            >>> # Silo for Research session with egress
            >>> context_id = api.create_context(
            ...     url="https://example.com",
            ...     username="researcher@company.com",
            ...     session_type="toolbox-standalone",
            ...     egress_region="New York, NY",
            ...     browser_profile={"os": "win", "browser": "chrome"},
            ...     categories={"availability": "public", "connectivity": "datacenter"}
            ... )
        """
        # Validate user/org - exactly one must be provided
        if not username and not org:
            raise ValidationError("Either username or org must be provided")
        if username and org:
            raise ValidationError("Only one of username or org should be provided")

        if username and not isinstance(username, str):
            raise ValidationError("Username must be a non-empty string")

        if org and not isinstance(org, str):
            raise ValidationError("Org must be a non-empty string")

        # Initialize policy if not provided
        if policy is None:
            policy = []
        elif not isinstance(policy, list):
            raise ValidationError("Policy must be a list of dictionaries")

        # Build context data
        context_data: dict[str, Any] = {}

        # Add user or org
        if username:
            context_data["user"] = username
        if org:
            context_data["org"] = org

        # Add URL(s) if provided
        if url:
            if not isinstance(url, str):
                raise ValidationError("URL must be a non-empty string")
            try:
                parsed_url = urlparse(url)
                if not parsed_url.netloc:
                    raise ValidationError(f"Invalid URL format: {url}")
                if auto_domain_allow:
                    policy = policy + [
                        {"type": "domain_allow", "params": [parsed_url.netloc]}
                    ]
            except ValidationError:
                # Don't let the generic handler below re-wrap our own clean
                # ValidationError message (e.g. "Invalid URL format: ...")
                # into a confusing "Failed to parse URL: Invalid URL format:
                # ..." double-wrap.
                raise
            except Exception as e:
                raise ValidationError(f"Failed to parse URL: {e}") from e
            context_data["urls"] = [url]

        # Add policy if not empty
        if policy:
            context_data["policy"] = policy

        # Add session type for Silo for Research
        if session_type:
            context_data["session_type"] = session_type

        # Add egress region (validated against full egress hierarchy)
        # The backend resolves egress_region via hierarchy_tag lookup,
        # accepting city, country, region, or "World" for any protocol.
        if egress_region:
            if not is_location_available(egress_region):
                raise ValidationError(
                    f"Invalid egress_region: {egress_region!r}. "
                    f"Use a specific location (e.g. 'New York, NY'), a country "
                    f"(e.g. 'United States'), a region (e.g. 'North America'), "
                    f"or 'World' for random assignment."
                )
            context_data["egress_region"] = egress_region

        # Add browser profile
        if browser_profile:
            context_data["browser_profile"] = browser_profile

        # Add categories for egress settings — validated for structure/type
        # membership, then for availability at the requested egress_region
        # (or "World" if no specific region was requested).
        if categories:
            validate_categories(categories)
            check_categories_against_location(
                egress_region or "World", dict(categories)
            )
            context_data["categories"] = categories

        # Add optional parameters
        if restrict_client_ips is not None:
            if not isinstance(restrict_client_ips, list):
                raise ValidationError("restrict_client_ips must be a list")
            context_data["restrict_ips"] = restrict_client_ips

        # Build create_context command
        create_cmd: dict[str, Any] = {
            "command": "create_context",
            "context_data": context_data,
        }

        # Add max_uses to command (not context_data per API spec)
        if max_uses is not None:
            if not isinstance(max_uses, int) or max_uses <= 0:
                raise ValidationError("max_uses must be a positive integer")
            create_cmd["max_uses"] = max_uses

        # Add expires at command level (not inside context_data, same as max_uses)
        if expires is not None:
            create_cmd["expires"] = expires

        # Prepare API request
        payload = [create_cmd]

        try:
            self.logger.debug("Creating browsing context for URL: %s", url)
            response = self._make_api_request("POST", "api/", self.auth_token, payload)

            # Extract context ID from response
            if len(response) >= 2 and isinstance(response[1], dict):
                result_entry = response[1]
                if "error" in result_entry:
                    raise BrowsingAPIError(f"API error: {result_entry['error']}")
                result = result_entry.get("result", {})
                browse_context_id: str | None = result.get("browse_context_id")

                if browse_context_id:
                    self.logger.info("Created context with ID: %s", browse_context_id)
                    return browse_context_id

            raise BrowsingAPIError(
                f"Failed to retrieve context ID from response: {response}"
            )

        except Exception as e:
            if isinstance(e, BrowsingAPIError):
                raise
            raise BrowsingAPIError(f"Failed to create context: {e}") from e

    @api_tag(MethodType.API_COMMAND, api_command="get_context")
    def get_context(self, browse_context_id: str) -> dict[str, Any]:
        """Retrieve details of a browsing context.

        Gets detailed information about an existing browsing context including
        creation time, usage count, policies, and expiration details.

        Args:
            browse_context_id: ID of the browsing context

        Returns:
            Dictionary containing context details including:
            - browse_context_id: The context ID
            - use_count: Number of times context has been used
            - user_id: Associated user ID
            - created_ts: Creation timestamp
            - expires_ts: Expiration timestamp (if set)
            - max_uses: Maximum allowed uses (if set)
            - policy: Applied policies

        Raises:
            BrowsingAPIError: If context retrieval fails
            ValidationError: If browse_context_id is invalid

        Example:
            >>> context_info = api.get_context("0123456789abcdef0123456789abcdef")
            >>> print(f"Context used {context_info['use_count']} times")
        """
        if not browse_context_id or not isinstance(browse_context_id, str):
            raise ValidationError("browse_context_id must be a non-empty string")

        payload = [{"command": "get_context", "browse_context_id": browse_context_id}]

        try:
            self.logger.debug("Retrieving context: %s", browse_context_id)
            response = self._make_api_request("POST", "api/", self.auth_token, payload)

            # Find the context result in response
            for item in response:
                if isinstance(item, dict) and "result" in item:
                    result = item["result"]
                    if isinstance(result, dict) and "browse_context_id" in result:
                        self.logger.debug("Retrieved context details")
                        return result

            raise BrowsingAPIError(
                f"No context data found in response for ID: {browse_context_id}"
            )

        except Exception as e:
            if isinstance(e, BrowsingAPIError):
                raise
            raise BrowsingAPIError(f"Failed to retrieve context: {e}") from e

    @api_tag(MethodType.API_COMMAND, api_command="delete_context")
    def delete_context(self, browse_context_id: str) -> bool:
        """Delete a browsing context.

        Permanently deletes a browsing context, making it unusable for future
        sessions. This action cannot be undone.

        Args:
            browse_context_id: ID of the browsing context to delete

        Returns:
            True if deletion was successful, False otherwise

        Raises:
            BrowsingAPIError: If context deletion fails
            ValidationError: If browse_context_id is invalid

        Example:
            >>> success = api.delete_context("0123456789abcdef0123456789abcdef")
            >>> if success:
            ...     print("Context deleted successfully")
        """
        if not browse_context_id or not isinstance(browse_context_id, str):
            raise ValidationError("browse_context_id must be a non-empty string")

        payload = [
            {"command": "delete_context", "browse_context_id": browse_context_id}
        ]

        try:
            self.logger.debug("Deleting context: %s", browse_context_id)
            response = self._make_api_request("POST", "api/", self.auth_token, payload)

            result = self._extract_api_result(response)
            if isinstance(result, dict) and result.get("deleted") in (1, True):
                self.logger.info("Successfully deleted context: %s", browse_context_id)
                return True

            self.logger.warning(
                "Unexpected response format when deleting context %s: %s",
                browse_context_id,
                result,
            )
            return False

        except Exception as e:
            if isinstance(e, BrowsingAPIError):
                raise
            raise BrowsingAPIError(f"Failed to delete context: {e}") from e

    @api_tag(MethodType.API_COMMAND, api_command="update_context")
    def update_context(
        self,
        browse_context_id: str,
        context_data: dict[str, Any] | None = None,
        max_uses: int | None = None,
        expires: str | None = None,
        name: str | None = None,
        enabled: bool | None = None,
    ) -> dict[str, Any]:
        """Update an existing browsing context.

        Modifies properties of an existing context. Only the fields provided
        are updated; omitted fields are left unchanged.

        Args:
            browse_context_id: ID of the context to update (required).
            context_data: Updated session configuration dict (same structure as
                :meth:`create_context`). Supports ``policy``, ``egress_region``,
                ``session_type``, ``browser_profile``, ``categories``, etc.
            max_uses: New maximum number of times the context may be used.
            expires: New expiration time for the context.
            name: New display name for the context.
            enabled: Whether the context is enabled.

        Returns:
            Dictionary containing the updated context details from the server.

        Raises:
            BrowsingAPIError: If the update request fails
            ValidationError: If browse_context_id is invalid

        Example:
            >>> api.update_context(
            ...     "0123456789abcdef0123456789abcdef",
            ...     max_uses=10,
            ...     name="updated-context",
            ... )
        """
        if not browse_context_id or not isinstance(browse_context_id, str):
            raise ValidationError("browse_context_id must be a non-empty string")

        command: dict[str, Any] = {
            "command": "update_context",
            "browse_context_id": browse_context_id,
        }
        if context_data is not None:
            command["context_data"] = context_data
        if max_uses is not None:
            command["max_uses"] = max_uses
        if expires is not None:
            command["expires"] = expires
        if name is not None:
            command["name"] = name
        if enabled is not None:
            command["enabled"] = enabled

        payload = [command]

        try:
            self.logger.debug("Updating context: %s", browse_context_id)
            response = self._make_api_request("POST", "api/", self.auth_token, payload)

            result = self._extract_api_result(response)
            self.logger.info("Successfully updated context: %s", browse_context_id)
            if isinstance(result, dict):
                return result
            return {}

        except Exception as e:
            if isinstance(e, BrowsingAPIError):
                raise
            raise BrowsingAPIError(f"Failed to update context: {e}") from e

    @api_tag(MethodType.UTILITY)
    @staticmethod
    def create_ctx_url(ctx: str, base_url: str = "https://a8silo.com") -> str:
        """Create a Silo web client launch URL for a browsing context.

        Generates a ``https://a8silo.com/launch?ctx=<id>`` URL for opening an
        already-created context in the Silo web client. The context must first
        be created via :meth:`create_context`.

        For a single-step alternative that creates and launches in one GET
        request, see the ``/ctx/`` shorthand documented in :meth:`create_context`.

        Args:
            ctx: Browse context ID (from :meth:`create_context`)
            base_url: Base URL for the Silo web client (default: ``https://a8silo.com``)

        Returns:
            Full URL for launching the Silo session in the web client

        Example:
            >>> context_id = api.create_context(url="https://example.com", username="user@co.com")
            >>> launch_url = BrowsingAPI.create_ctx_url(context_id)
            >>> print(launch_url)
            https://a8silo.com/launch?ctx=abc123def456
        """
        if not ctx:
            raise ValidationError("Context ID cannot be empty")

        return f"{base_url.rstrip('/')}/launch?ctx={ctx}"

    @api_tag(MethodType.UTILITY)
    @staticmethod
    def defang_url(url: str) -> str:
        """Defang a URL by replacing certain characters.

        Makes a URL "safe" for sharing by replacing potentially dangerous
        characters that might cause accidental navigation.

        Args:
            url: URL to defang

        Returns:
            Defanged URL with replaced characters

        Example:
            >>> defanged = BrowsingAPI.defang_url("https://malicious.com")
            >>> print(defanged)
            hxxps://malicious[.]com
        """
        if not url:
            return url

        return (
            url.replace("http://", "hxxp://")
            .replace("https://", "hxxps://")
            .replace(":", "[:]")
            .replace(".", "[.]")
        )

    @api_tag(MethodType.UTILITY)
    @staticmethod
    def refang_url(url: str) -> str:
        """Refang a URL by restoring certain characters.

        Restores a defanged URL back to its original form for actual use.

        Args:
            url: Defanged URL to refang

        Returns:
            Refanged URL with restored characters

        Example:
            >>> refanged = BrowsingAPI.refang_url("hxxps://example[.]com")
            >>> print(refanged)
            https://example.com
        """
        if not url:
            return url

        return (
            url.replace("hxxp://", "http://")
            .replace("hxxps://", "https://")
            .replace("[.]", ".")
            .replace("[:]", ":")
        )

    @api_tag(MethodType.CONVENIENCE, wraps=["create_context"])
    def bulk_create_contexts(
        self,
        urls: list[str],
        username: str | None = None,
        policy: list[dict[str, Any]] | None = None,
        max_uses: int | None = None,
        expires: str | None = None,
        org: str | None = None,
        session_type: Literal["silo-ruby", "toolbox-standalone"] | None = None,
        egress_region: str | None = None,
        browser_profile: BrowserProfile | None = None,
        categories: Categories | None = None,
    ) -> dict[str, list[str]]:
        """Create multiple browsing contexts for a list of URLs.

        Efficiently creates contexts for multiple URLs with the same user and
        policy settings. Failed context creations are logged but don't stop
        the process for other URLs.

        Args:
            urls: List of URLs to create contexts for
            username: Username associated with all contexts
            policy: List of policy dictionaries for all contexts
            max_uses: Maximum number of times each context can be used
            expires: Expiration time for all contexts
            org: Org slug for SAML-enabled organizations
            session_type: 'silo-ruby' or 'toolbox-standalone'
            egress_region: Egress location (e.g., 'New York, NY')
            browser_profile: Browser profile settings
            categories: Egress category settings

        Returns:
            Dictionary containing list of successfully created context URLs

        Raises:
            ValidationError: If parameters are invalid

        Example:
            >>> urls = ["https://site1.com", "https://site2.com"]
            >>> policy = [{"type": "readonly", "params": ["true"]}]
            >>> result = api.bulk_create_contexts(urls, "user@company.com", policy)
            >>> print(f"Created {len(result['context_urls'])} contexts")

            >>> # With Silo for Research settings
            >>> result = api.bulk_create_contexts(
            ...     urls=urls,
            ...     username="researcher@company.com",
            ...     session_type="toolbox-standalone",
            ...     egress_region="London"
            ... )
        """
        if not isinstance(urls, list) or not urls:
            raise ValidationError("URLs must be a non-empty list")

        if not username and not org:
            raise ValidationError("Either username or org must be provided")

        if policy is not None and not isinstance(policy, list):
            raise ValidationError("Policy must be a list of dictionaries")

        context_urls: list[str] = []

        self.logger.info("Creating contexts for %d URLs", len(urls))

        for i, url in enumerate(urls):
            try:
                # Refang URL in case it was defanged
                refanged_url = self.refang_url(url)

                # Create context with all parameters
                context_id = self.create_context(
                    url=refanged_url,
                    username=username,
                    policy=policy,
                    max_uses=max_uses,
                    expires=expires,
                    org=org,
                    session_type=session_type,
                    egress_region=egress_region,
                    browser_profile=browser_profile,
                    categories=categories,
                )

                # Generate launch URL
                launch_url = self.create_ctx_url(context_id)
                context_urls.append(launch_url)

                self.logger.debug("Created context %d/%d", i + 1, len(urls))

            except Exception as e:
                self.logger.error("Failed to create context for URL %s: %s", url, e)
                # Continue with other URLs
                continue

        self.logger.info(
            "Successfully created %d/%d contexts", len(context_urls), len(urls)
        )

        return {"context_urls": context_urls}

    @api_tag(MethodType.CONVENIENCE, wraps=["create_context"])
    def create_research_session(
        self,
        username: str,
        egress_region: str,
        url: str | None = None,
        browser_profile: BrowserProfile | None = None,
        categories: Categories | None = None,
        policy: list[dict[str, Any]] | None = None,
        max_uses: int | None = None,
        expires: str | None = None,
        auto_domain_allow: bool = False,
    ) -> str:
        """Create a Silo for Research (toolbox-standalone) session.

        Convenience method for creating research sessions with egress routing
        and browser profile configuration.

        Args:
            username: Username for the session
            egress_region: Egress location (e.g., 'New York, NY', 'London').
                See :meth:`get_egress_locations` for available options.
            url: Optional URL to open in the session
            browser_profile: Browser profile with 'os', 'browser', and optional
                'timezone'/'languages' settings.
                Options for os: 'win', 'mac', 'linux', 'android', 'ios'
                Options for browser: 'chrome', 'firefox', 'edge', 'safari', 'tor'
            categories: Egress category settings. Example:
                {"availability": "public", "connectivity": "datacenter"}
            policy: Optional policy list for the session
            max_uses: Maximum number of times the context can be used
            expires: Context expiration time

        Returns:
            Browse context ID that can be used to launch the session

        Raises:
            BrowsingAPIError: If session creation fails
            ValidationError: If parameters are invalid

        Example:
            >>> context_id = api.create_research_session(
            ...     username="researcher@company.com",
            ...     egress_region="New York, NY",
            ...     url="https://example.com",
            ...     browser_profile={"os": "win", "browser": "chrome"},
            ...     categories={"availability": "public", "connectivity": "isp"}
            ... )
            >>> launch_url = api.create_ctx_url(context_id)
        """
        return self.create_context(
            url=url,
            username=username,
            policy=policy,
            max_uses=max_uses,
            expires=expires,
            session_type="toolbox-standalone",
            egress_region=egress_region,
            browser_profile=browser_profile,
            categories=categories,
            auto_domain_allow=auto_domain_allow,
        )

    @api_tag(MethodType.UTILITY)
    @staticmethod
    def get_egress_locations(
        include_details: bool = False,
    ) -> dict[str, Any]:
        """Get available egress locations by region.

        Args:
            include_details: When ``False`` (default), returns a dict mapping
                region names to lists of location name strings — backward-compatible
                with prior behavior. When ``True``, each location is a dict with
                ``name``, ``connectivity``, ``availability``, and ``protocol`` keys.

        Returns:
            When ``include_details=False``:
                ``{region: [city_name, ...]}``

            When ``include_details=True``:
                ``{region: [{"name": city, "connectivity": [...],
                "availability": [...], "protocol": [...]}]}``

            Cities absent from the canonical location data (see
            :func:`~silo_sdk.harvesting.egress_validation.get_egress_locations_with_details`)
            are omitted from the enriched output entirely, rather than
            appearing with empty detail lists.

        Example:
            >>> locations = BrowsingAPI.get_egress_locations()
            >>> print(locations["North America"])
            ['Toronto', 'Vancouver', 'Mexico City', ...]

            >>> details = BrowsingAPI.get_egress_locations(include_details=True)
            >>> sao_paulo = next(
            ...     e for e in details["Central & South America"]
            ...     if e["name"] == "Sao Paulo"
            ... )
            >>> print("datacenter" in sao_paulo["connectivity"])
            True
            >>> sydney = next(
            ...     e for e in details["Asia-Pacific"] if e["name"] == "Sydney"
            ... )
            >>> print("tor" in sydney["protocol"])
            True
        """
        if not include_details:
            details = get_egress_locations_with_details()
            return {
                region: [e["name"] for e in entries]
                for region, entries in details.items()
            }

        return get_egress_locations_with_details()

__init__

__init__(config: dict[str, Any]) -> None

Initialize the browsing API client.

Parameters:

Name Type Description Default
config dict[str, Any]

Configuration dictionary containing API settings

required

Raises:

Type Description
ConfigurationError

If required configuration is missing

Source code in silo_sdk/browsing/isolation_api.py
def __init__(self, config: dict[str, Any]) -> None:
    """Initialize the browsing API client.

    Args:
        config: Configuration dictionary containing API settings

    Raises:
        ConfigurationError: If required configuration is missing
    """
    super().__init__(config)

    # Validate required configuration
    validate_config_for_api(config, ["ADMIN_TOKEN"])

    self.auth_token = config["ADMIN_TOKEN"]
    self.logger.info("Initialized BrowsingAPI client")

create_context

create_context(url: str | None = None, username: str | None = None, policy: list[dict[str, Any]] | None = None, max_uses: int | None = None, expires: str | None = None, restrict_client_ips: list[str] | None = None, org: str | None = None, session_type: Literal['silo-ruby', 'toolbox-standalone'] | None = None, egress_region: str | None = None, browser_profile: BrowserProfile | None = None, categories: Categories | None = None, auto_domain_allow: bool = False) -> str

Create a new browsing context.

Creates a secure browsing context that can be used to launch isolated browsing sessions with the specified URL and policies.

Parameters:

Name Type Description Default
url str | None

Target URL for the context (optional for toolbox-standalone). Sent as "urls": [url] (array) in the wire format. The domain is automatically extracted and added as a domain_allow policy entry to the provided policy list.

None
username str | None

Username associated with the context. Either username or org must be provided (but not both). Sent as "user" in the wire format (field name differs from this param).

None
policy list[dict[str, Any]] | None

List of policy dictionaries, each with "type" and "params" keys. See class-level Attributes for available policy types and valid param values.

Seamless UI constraint: {"type": "browser_chrome", "params": ["seamless"]} is only effective for non-catchall users. Catchall users — via a catchall account, an org configured to treat all users as catchall, or partner SSO — are always forced to "minimal" (Ribbon Mode) regardless of this policy setting.

None
max_uses int | None

Maximum number of times the context can be used. Sent at the command level (not inside context_data).

None
expires str | None

Context expiration time (epoch time, ISO8601, or offset). Sent at the command level (not inside context_data), same as max_uses.

None
restrict_client_ips list[str] | None

List of IP addresses/CIDRs to restrict access. Sent as "restrict_ips" in the wire format (field name differs from this param).

None
org str | None

Org slug/vanity URL for SAML-enabled organizations. Either username or org must be provided (but not both).

None
session_type Literal['silo-ruby', 'toolbox-standalone'] | None

Session type — "silo-ruby" (default, Safe Access) or "toolbox-standalone" (Silo for Research).

None
egress_region str | None

Egress location name from the managed attribution network. Accepts a specific city (e.g., "New York, NY"), a country ("United States"), a region ("North America"), or "World" for random assignment. The backend resolves via hierarchy_tag lookup. See :data:silo_sdk.harvesting.egress_validation.VALID_EGRESS_NAMES for the complete list.

Important — mixed licensing: Even if a location passes SDK validation, the user may receive an egress error when the browsing context is launched if their license does not include rights to that specific location. SDK validation confirms the location name is valid for the platform's shared network, but per-user license entitlements are enforced at session launch time, not at context creation.

None
browser_profile BrowserProfile | None

Browser profile dict for user-agent selection. Keys: "os", "browser", and optionally "timezone" (IANA name) and "languages" (Accept-Language string).

None
categories Categories | None

Egress category settings with "availability", "connectivity", and "protocol" keys. "connectivity" and "protocol" are validated against what is actually available at egress_region (or any location, if egress_region is omitted) — see :meth:get_egress_locations for per-location details.

None
auto_domain_allow bool

If True, automatically append a domain_allow policy entry for the URL's domain when url is provided. Defaults to False.

Leave this False (the default) if you want to construct your own domain_allow policy entry — for example, to allow multiple domains in a single entry::

policy=[{
    "type": "domain_allow",
    "params": ["google.com", "yahoo.com"]
}]

When auto_domain_allow=True, only the domain extracted from url is injected. It cannot be used to allow additional domains beyond the target URL's domain.

False
Note

Wire format parameter mappings:

  • Python username → wire "user" (inside context_data)
  • Python url (string) → wire "urls" (list, inside context_data)
  • max_uses and expires go at the command level (alongside "command", not inside context_data)
  • domain_allow is only added when auto_domain_allow=True

Wire format structure::

{
    "command": "create_context",
    "max_uses": 5,       # ← command level
    "expires": "2026-12-31T23:59:59Z",  # ← command level
    "context_data": {
        "user": "user@company.com",  # ← username → "user"
        "urls": ["https://example.com"],  # ← url → "urls" (list)
        "policy": [...]
    }
}

Returns:

Type Description
str

Browse context ID (browse_context_id) that can be used to launch

str

sessions. Pass this to :meth:create_ctx_url to get a launch URL, or

str

use the GET /ctx/ shorthand endpoint directly (see Note).

Raises:

Type Description
BrowsingAPIError

If context creation fails

ValidationError

If parameters are invalid

Note

GET /ctx shorthand: As an alternative to the two-step create → launch flow, the /ctx/ endpoint at https://extapi.authentic8.com/ctx/ creates and launches a context in a single GET request using inline query parameters::

GET https://extapi.authentic8.com/ctx/
    ?auth=<admin_token>
    &user=<username>
    &url=<target_url>
    &response=url

The response parameter controls the return format:

  • redirect (default) — HTTP redirect directly into the Silo session
  • url — returns the launch URL as plain text (useful for automation)
  • id — returns just the context launch ID as plain text

This shorthand does not go through the /api/ command-array format.

Example

Basic isolation context

policy = [ ... {"type": "file_transfer", "params": ["block_all"]}, ... {"type": "clipboard", "params": ["allow_to_local"]}, ... ] context_id = api.create_context( ... url="https://example.com", ... username="user@company.com", ... policy=policy, ... max_uses=5 ... )

Silo for Research session with egress

context_id = api.create_context( ... url="https://example.com", ... username="researcher@company.com", ... session_type="toolbox-standalone", ... egress_region="New York, NY", ... browser_profile={"os": "win", "browser": "chrome"}, ... categories={"availability": "public", "connectivity": "datacenter"} ... )

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.API_COMMAND, api_command="create_context")
def create_context(
    self,
    url: str | None = None,
    username: str | None = None,
    policy: list[dict[str, Any]] | None = None,
    max_uses: int | None = None,
    expires: str | None = None,
    restrict_client_ips: list[str] | None = None,
    org: str | None = None,
    session_type: Literal["silo-ruby", "toolbox-standalone"] | None = None,
    egress_region: str | None = None,
    browser_profile: BrowserProfile | None = None,
    categories: Categories | None = None,
    auto_domain_allow: bool = False,
) -> str:
    """Create a new browsing context.

    Creates a secure browsing context that can be used to launch isolated
    browsing sessions with the specified URL and policies.

    Args:
        url: Target URL for the context (optional for toolbox-standalone).
            Sent as ``"urls": [url]`` (array) in the wire format. The domain
            is automatically extracted and added as a ``domain_allow`` policy
            entry to the provided policy list.
        username: Username associated with the context.
            Either username or org must be provided (but not both). Sent as
            ``"user"`` in the wire format (field name differs from this param).
        policy: List of policy dictionaries, each with ``"type"`` and
            ``"params"`` keys. See class-level ``Attributes`` for available
            policy types and valid param values.

            **Seamless UI constraint:** ``{"type": "browser_chrome",
            "params": ["seamless"]}`` is only effective for
            **non-catchall users**. Catchall users — via a catchall
            account, an org configured to treat all users as catchall, or
            partner SSO — are always forced to ``"minimal"`` (Ribbon Mode)
            regardless of this policy setting.
        max_uses: Maximum number of times the context can be used.
            Sent at the **command level** (not inside ``context_data``).
        expires: Context expiration time (epoch time, ISO8601, or offset).
            Sent at the **command level** (not inside ``context_data``),
            same as ``max_uses``.
        restrict_client_ips: List of IP addresses/CIDRs to restrict access.
            Sent as ``"restrict_ips"`` in the wire format (field name differs
            from this param).
        org: Org slug/vanity URL for SAML-enabled organizations.
            Either username or org must be provided (but not both).
        session_type: Session type — ``"silo-ruby"`` (default, Safe Access)
            or ``"toolbox-standalone"`` (Silo for Research).
        egress_region: Egress location name from the managed attribution
            network. Accepts a specific city (e.g., ``"New York, NY"``),
            a country (``"United States"``), a region (``"North America"``),
            or ``"World"`` for random assignment. The backend resolves
            via ``hierarchy_tag`` lookup. See
            :data:`silo_sdk.harvesting.egress_validation.VALID_EGRESS_NAMES`
            for the complete list.

            **Important — mixed licensing:** Even if a location passes SDK
            validation, the user may receive an egress error when the browsing
            context is launched if their license does not include rights to that
            specific location. SDK validation confirms the location name is valid
            for the platform's shared network, but per-user license entitlements
            are enforced at session launch time, not at context creation.
        browser_profile: Browser profile dict for user-agent selection.
            Keys: ``"os"``, ``"browser"``, and optionally ``"timezone"``
            (IANA name) and ``"languages"`` (Accept-Language string).
        categories: Egress category settings with ``"availability"``,
            ``"connectivity"``, and ``"protocol"`` keys. ``"connectivity"``
            and ``"protocol"`` are validated against what is actually
            available at ``egress_region`` (or any location, if
            ``egress_region`` is omitted) — see
            :meth:`get_egress_locations` for per-location details.
        auto_domain_allow: If ``True``, automatically append a
            ``domain_allow`` policy entry for the URL's domain when
            ``url`` is provided. Defaults to ``False``.

            Leave this ``False`` (the default) if you want to construct
            your own ``domain_allow`` policy entry — for example, to
            allow multiple domains in a single entry::

                policy=[{
                    "type": "domain_allow",
                    "params": ["google.com", "yahoo.com"]
                }]

            When ``auto_domain_allow=True``, only the domain extracted
            from ``url`` is injected. It cannot be used to allow
            additional domains beyond the target URL's domain.

    Note:
        Wire format parameter mappings:

        - Python ``username`` → wire ``"user"``
          (inside ``context_data``)
        - Python ``url`` (string) → wire ``"urls"`` (list,
          inside ``context_data``)
        - ``max_uses`` and ``expires`` go at the **command level**
          (alongside ``"command"``, not inside ``context_data``)
        - ``domain_allow`` is only added when ``auto_domain_allow=True``

        Wire format structure::

            {
                "command": "create_context",
                "max_uses": 5,       # ← command level
                "expires": "2026-12-31T23:59:59Z",  # ← command level
                "context_data": {
                    "user": "user@company.com",  # ← username → "user"
                    "urls": ["https://example.com"],  # ← url → "urls" (list)
                    "policy": [...]
                }
            }

    Returns:
        Browse context ID (``browse_context_id``) that can be used to launch
        sessions. Pass this to :meth:`create_ctx_url` to get a launch URL, or
        use the GET ``/ctx/`` shorthand endpoint directly (see Note).

    Raises:
        BrowsingAPIError: If context creation fails
        ValidationError: If parameters are invalid

    Note:
        **GET /ctx shorthand:** As an alternative to the two-step
        create → launch flow, the ``/ctx/`` endpoint at
        ``https://extapi.authentic8.com/ctx/`` creates and launches a context
        in a single GET request using inline query parameters::

            GET https://extapi.authentic8.com/ctx/
                ?auth=<admin_token>
                &user=<username>
                &url=<target_url>
                &response=url

        The ``response`` parameter controls the return format:

        - ``redirect`` (default) — HTTP redirect directly into the Silo session
        - ``url`` — returns the launch URL as plain text (useful for automation)
        - ``id`` — returns just the context launch ID as plain text

        This shorthand does **not** go through the ``/api/`` command-array format.

    Example:
        >>> # Basic isolation context
        >>> policy = [
        ...     {"type": "file_transfer", "params": ["block_all"]},
        ...     {"type": "clipboard", "params": ["allow_to_local"]},
        ... ]
        >>> context_id = api.create_context(
        ...     url="https://example.com",
        ...     username="user@company.com",
        ...     policy=policy,
        ...     max_uses=5
        ... )

        >>> # Silo for Research session with egress
        >>> context_id = api.create_context(
        ...     url="https://example.com",
        ...     username="researcher@company.com",
        ...     session_type="toolbox-standalone",
        ...     egress_region="New York, NY",
        ...     browser_profile={"os": "win", "browser": "chrome"},
        ...     categories={"availability": "public", "connectivity": "datacenter"}
        ... )
    """
    # Validate user/org - exactly one must be provided
    if not username and not org:
        raise ValidationError("Either username or org must be provided")
    if username and org:
        raise ValidationError("Only one of username or org should be provided")

    if username and not isinstance(username, str):
        raise ValidationError("Username must be a non-empty string")

    if org and not isinstance(org, str):
        raise ValidationError("Org must be a non-empty string")

    # Initialize policy if not provided
    if policy is None:
        policy = []
    elif not isinstance(policy, list):
        raise ValidationError("Policy must be a list of dictionaries")

    # Build context data
    context_data: dict[str, Any] = {}

    # Add user or org
    if username:
        context_data["user"] = username
    if org:
        context_data["org"] = org

    # Add URL(s) if provided
    if url:
        if not isinstance(url, str):
            raise ValidationError("URL must be a non-empty string")
        try:
            parsed_url = urlparse(url)
            if not parsed_url.netloc:
                raise ValidationError(f"Invalid URL format: {url}")
            if auto_domain_allow:
                policy = policy + [
                    {"type": "domain_allow", "params": [parsed_url.netloc]}
                ]
        except ValidationError:
            # Don't let the generic handler below re-wrap our own clean
            # ValidationError message (e.g. "Invalid URL format: ...")
            # into a confusing "Failed to parse URL: Invalid URL format:
            # ..." double-wrap.
            raise
        except Exception as e:
            raise ValidationError(f"Failed to parse URL: {e}") from e
        context_data["urls"] = [url]

    # Add policy if not empty
    if policy:
        context_data["policy"] = policy

    # Add session type for Silo for Research
    if session_type:
        context_data["session_type"] = session_type

    # Add egress region (validated against full egress hierarchy)
    # The backend resolves egress_region via hierarchy_tag lookup,
    # accepting city, country, region, or "World" for any protocol.
    if egress_region:
        if not is_location_available(egress_region):
            raise ValidationError(
                f"Invalid egress_region: {egress_region!r}. "
                f"Use a specific location (e.g. 'New York, NY'), a country "
                f"(e.g. 'United States'), a region (e.g. 'North America'), "
                f"or 'World' for random assignment."
            )
        context_data["egress_region"] = egress_region

    # Add browser profile
    if browser_profile:
        context_data["browser_profile"] = browser_profile

    # Add categories for egress settings — validated for structure/type
    # membership, then for availability at the requested egress_region
    # (or "World" if no specific region was requested).
    if categories:
        validate_categories(categories)
        check_categories_against_location(
            egress_region or "World", dict(categories)
        )
        context_data["categories"] = categories

    # Add optional parameters
    if restrict_client_ips is not None:
        if not isinstance(restrict_client_ips, list):
            raise ValidationError("restrict_client_ips must be a list")
        context_data["restrict_ips"] = restrict_client_ips

    # Build create_context command
    create_cmd: dict[str, Any] = {
        "command": "create_context",
        "context_data": context_data,
    }

    # Add max_uses to command (not context_data per API spec)
    if max_uses is not None:
        if not isinstance(max_uses, int) or max_uses <= 0:
            raise ValidationError("max_uses must be a positive integer")
        create_cmd["max_uses"] = max_uses

    # Add expires at command level (not inside context_data, same as max_uses)
    if expires is not None:
        create_cmd["expires"] = expires

    # Prepare API request
    payload = [create_cmd]

    try:
        self.logger.debug("Creating browsing context for URL: %s", url)
        response = self._make_api_request("POST", "api/", self.auth_token, payload)

        # Extract context ID from response
        if len(response) >= 2 and isinstance(response[1], dict):
            result_entry = response[1]
            if "error" in result_entry:
                raise BrowsingAPIError(f"API error: {result_entry['error']}")
            result = result_entry.get("result", {})
            browse_context_id: str | None = result.get("browse_context_id")

            if browse_context_id:
                self.logger.info("Created context with ID: %s", browse_context_id)
                return browse_context_id

        raise BrowsingAPIError(
            f"Failed to retrieve context ID from response: {response}"
        )

    except Exception as e:
        if isinstance(e, BrowsingAPIError):
            raise
        raise BrowsingAPIError(f"Failed to create context: {e}") from e

get_context

get_context(browse_context_id: str) -> dict[str, Any]

Retrieve details of a browsing context.

Gets detailed information about an existing browsing context including creation time, usage count, policies, and expiration details.

Parameters:

Name Type Description Default
browse_context_id str

ID of the browsing context

required

Returns:

Type Description
dict[str, Any]

Dictionary containing context details including:

dict[str, Any]
  • browse_context_id: The context ID
dict[str, Any]
  • use_count: Number of times context has been used
dict[str, Any]
  • user_id: Associated user ID
dict[str, Any]
  • created_ts: Creation timestamp
dict[str, Any]
  • expires_ts: Expiration timestamp (if set)
dict[str, Any]
  • max_uses: Maximum allowed uses (if set)
dict[str, Any]
  • policy: Applied policies

Raises:

Type Description
BrowsingAPIError

If context retrieval fails

ValidationError

If browse_context_id is invalid

Example

context_info = api.get_context("0123456789abcdef0123456789abcdef") print(f"Context used {context_info['use_count']} times")

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.API_COMMAND, api_command="get_context")
def get_context(self, browse_context_id: str) -> dict[str, Any]:
    """Retrieve details of a browsing context.

    Gets detailed information about an existing browsing context including
    creation time, usage count, policies, and expiration details.

    Args:
        browse_context_id: ID of the browsing context

    Returns:
        Dictionary containing context details including:
        - browse_context_id: The context ID
        - use_count: Number of times context has been used
        - user_id: Associated user ID
        - created_ts: Creation timestamp
        - expires_ts: Expiration timestamp (if set)
        - max_uses: Maximum allowed uses (if set)
        - policy: Applied policies

    Raises:
        BrowsingAPIError: If context retrieval fails
        ValidationError: If browse_context_id is invalid

    Example:
        >>> context_info = api.get_context("0123456789abcdef0123456789abcdef")
        >>> print(f"Context used {context_info['use_count']} times")
    """
    if not browse_context_id or not isinstance(browse_context_id, str):
        raise ValidationError("browse_context_id must be a non-empty string")

    payload = [{"command": "get_context", "browse_context_id": browse_context_id}]

    try:
        self.logger.debug("Retrieving context: %s", browse_context_id)
        response = self._make_api_request("POST", "api/", self.auth_token, payload)

        # Find the context result in response
        for item in response:
            if isinstance(item, dict) and "result" in item:
                result = item["result"]
                if isinstance(result, dict) and "browse_context_id" in result:
                    self.logger.debug("Retrieved context details")
                    return result

        raise BrowsingAPIError(
            f"No context data found in response for ID: {browse_context_id}"
        )

    except Exception as e:
        if isinstance(e, BrowsingAPIError):
            raise
        raise BrowsingAPIError(f"Failed to retrieve context: {e}") from e

delete_context

delete_context(browse_context_id: str) -> bool

Delete a browsing context.

Permanently deletes a browsing context, making it unusable for future sessions. This action cannot be undone.

Parameters:

Name Type Description Default
browse_context_id str

ID of the browsing context to delete

required

Returns:

Type Description
bool

True if deletion was successful, False otherwise

Raises:

Type Description
BrowsingAPIError

If context deletion fails

ValidationError

If browse_context_id is invalid

Example

success = api.delete_context("0123456789abcdef0123456789abcdef") if success: ... print("Context deleted successfully")

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.API_COMMAND, api_command="delete_context")
def delete_context(self, browse_context_id: str) -> bool:
    """Delete a browsing context.

    Permanently deletes a browsing context, making it unusable for future
    sessions. This action cannot be undone.

    Args:
        browse_context_id: ID of the browsing context to delete

    Returns:
        True if deletion was successful, False otherwise

    Raises:
        BrowsingAPIError: If context deletion fails
        ValidationError: If browse_context_id is invalid

    Example:
        >>> success = api.delete_context("0123456789abcdef0123456789abcdef")
        >>> if success:
        ...     print("Context deleted successfully")
    """
    if not browse_context_id or not isinstance(browse_context_id, str):
        raise ValidationError("browse_context_id must be a non-empty string")

    payload = [
        {"command": "delete_context", "browse_context_id": browse_context_id}
    ]

    try:
        self.logger.debug("Deleting context: %s", browse_context_id)
        response = self._make_api_request("POST", "api/", self.auth_token, payload)

        result = self._extract_api_result(response)
        if isinstance(result, dict) and result.get("deleted") in (1, True):
            self.logger.info("Successfully deleted context: %s", browse_context_id)
            return True

        self.logger.warning(
            "Unexpected response format when deleting context %s: %s",
            browse_context_id,
            result,
        )
        return False

    except Exception as e:
        if isinstance(e, BrowsingAPIError):
            raise
        raise BrowsingAPIError(f"Failed to delete context: {e}") from e

update_context

update_context(browse_context_id: str, context_data: dict[str, Any] | None = None, max_uses: int | None = None, expires: str | None = None, name: str | None = None, enabled: bool | None = None) -> dict[str, Any]

Update an existing browsing context.

Modifies properties of an existing context. Only the fields provided are updated; omitted fields are left unchanged.

Parameters:

Name Type Description Default
browse_context_id str

ID of the context to update (required).

required
context_data dict[str, Any] | None

Updated session configuration dict (same structure as :meth:create_context). Supports policy, egress_region, session_type, browser_profile, categories, etc.

None
max_uses int | None

New maximum number of times the context may be used.

None
expires str | None

New expiration time for the context.

None
name str | None

New display name for the context.

None
enabled bool | None

Whether the context is enabled.

None

Returns:

Type Description
dict[str, Any]

Dictionary containing the updated context details from the server.

Raises:

Type Description
BrowsingAPIError

If the update request fails

ValidationError

If browse_context_id is invalid

Example

api.update_context( ... "0123456789abcdef0123456789abcdef", ... max_uses=10, ... name="updated-context", ... )

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.API_COMMAND, api_command="update_context")
def update_context(
    self,
    browse_context_id: str,
    context_data: dict[str, Any] | None = None,
    max_uses: int | None = None,
    expires: str | None = None,
    name: str | None = None,
    enabled: bool | None = None,
) -> dict[str, Any]:
    """Update an existing browsing context.

    Modifies properties of an existing context. Only the fields provided
    are updated; omitted fields are left unchanged.

    Args:
        browse_context_id: ID of the context to update (required).
        context_data: Updated session configuration dict (same structure as
            :meth:`create_context`). Supports ``policy``, ``egress_region``,
            ``session_type``, ``browser_profile``, ``categories``, etc.
        max_uses: New maximum number of times the context may be used.
        expires: New expiration time for the context.
        name: New display name for the context.
        enabled: Whether the context is enabled.

    Returns:
        Dictionary containing the updated context details from the server.

    Raises:
        BrowsingAPIError: If the update request fails
        ValidationError: If browse_context_id is invalid

    Example:
        >>> api.update_context(
        ...     "0123456789abcdef0123456789abcdef",
        ...     max_uses=10,
        ...     name="updated-context",
        ... )
    """
    if not browse_context_id or not isinstance(browse_context_id, str):
        raise ValidationError("browse_context_id must be a non-empty string")

    command: dict[str, Any] = {
        "command": "update_context",
        "browse_context_id": browse_context_id,
    }
    if context_data is not None:
        command["context_data"] = context_data
    if max_uses is not None:
        command["max_uses"] = max_uses
    if expires is not None:
        command["expires"] = expires
    if name is not None:
        command["name"] = name
    if enabled is not None:
        command["enabled"] = enabled

    payload = [command]

    try:
        self.logger.debug("Updating context: %s", browse_context_id)
        response = self._make_api_request("POST", "api/", self.auth_token, payload)

        result = self._extract_api_result(response)
        self.logger.info("Successfully updated context: %s", browse_context_id)
        if isinstance(result, dict):
            return result
        return {}

    except Exception as e:
        if isinstance(e, BrowsingAPIError):
            raise
        raise BrowsingAPIError(f"Failed to update context: {e}") from e

create_ctx_url staticmethod

create_ctx_url(ctx: str, base_url: str = 'https://a8silo.com') -> str

Create a Silo web client launch URL for a browsing context.

Generates a https://a8silo.com/launch?ctx=<id> URL for opening an already-created context in the Silo web client. The context must first be created via :meth:create_context.

For a single-step alternative that creates and launches in one GET request, see the /ctx/ shorthand documented in :meth:create_context.

Parameters:

Name Type Description Default
ctx str

Browse context ID (from :meth:create_context)

required
base_url str

Base URL for the Silo web client (default: https://a8silo.com)

'https://a8silo.com'

Returns:

Type Description
str

Full URL for launching the Silo session in the web client

Example

context_id = api.create_context(url="https://example.com", username="user@co.com") launch_url = BrowsingAPI.create_ctx_url(context_id) print(launch_url) https://a8silo.com/launch?ctx=abc123def456

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.UTILITY)
@staticmethod
def create_ctx_url(ctx: str, base_url: str = "https://a8silo.com") -> str:
    """Create a Silo web client launch URL for a browsing context.

    Generates a ``https://a8silo.com/launch?ctx=<id>`` URL for opening an
    already-created context in the Silo web client. The context must first
    be created via :meth:`create_context`.

    For a single-step alternative that creates and launches in one GET
    request, see the ``/ctx/`` shorthand documented in :meth:`create_context`.

    Args:
        ctx: Browse context ID (from :meth:`create_context`)
        base_url: Base URL for the Silo web client (default: ``https://a8silo.com``)

    Returns:
        Full URL for launching the Silo session in the web client

    Example:
        >>> context_id = api.create_context(url="https://example.com", username="user@co.com")
        >>> launch_url = BrowsingAPI.create_ctx_url(context_id)
        >>> print(launch_url)
        https://a8silo.com/launch?ctx=abc123def456
    """
    if not ctx:
        raise ValidationError("Context ID cannot be empty")

    return f"{base_url.rstrip('/')}/launch?ctx={ctx}"

defang_url staticmethod

defang_url(url: str) -> str

Defang a URL by replacing certain characters.

Makes a URL "safe" for sharing by replacing potentially dangerous characters that might cause accidental navigation.

Parameters:

Name Type Description Default
url str

URL to defang

required

Returns:

Type Description
str

Defanged URL with replaced characters

Example

defanged = BrowsingAPI.defang_url("https://malicious.com") print(defanged) hxxps://malicious[.]com

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.UTILITY)
@staticmethod
def defang_url(url: str) -> str:
    """Defang a URL by replacing certain characters.

    Makes a URL "safe" for sharing by replacing potentially dangerous
    characters that might cause accidental navigation.

    Args:
        url: URL to defang

    Returns:
        Defanged URL with replaced characters

    Example:
        >>> defanged = BrowsingAPI.defang_url("https://malicious.com")
        >>> print(defanged)
        hxxps://malicious[.]com
    """
    if not url:
        return url

    return (
        url.replace("http://", "hxxp://")
        .replace("https://", "hxxps://")
        .replace(":", "[:]")
        .replace(".", "[.]")
    )

refang_url staticmethod

refang_url(url: str) -> str

Refang a URL by restoring certain characters.

Restores a defanged URL back to its original form for actual use.

Parameters:

Name Type Description Default
url str

Defanged URL to refang

required

Returns:

Type Description
str

Refanged URL with restored characters

Example

refanged = BrowsingAPI.refang_url("hxxps://example[.]com") print(refanged) https://example.com

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.UTILITY)
@staticmethod
def refang_url(url: str) -> str:
    """Refang a URL by restoring certain characters.

    Restores a defanged URL back to its original form for actual use.

    Args:
        url: Defanged URL to refang

    Returns:
        Refanged URL with restored characters

    Example:
        >>> refanged = BrowsingAPI.refang_url("hxxps://example[.]com")
        >>> print(refanged)
        https://example.com
    """
    if not url:
        return url

    return (
        url.replace("hxxp://", "http://")
        .replace("hxxps://", "https://")
        .replace("[.]", ".")
        .replace("[:]", ":")
    )

bulk_create_contexts

bulk_create_contexts(urls: list[str], username: str | None = None, policy: list[dict[str, Any]] | None = None, max_uses: int | None = None, expires: str | None = None, org: str | None = None, session_type: Literal['silo-ruby', 'toolbox-standalone'] | None = None, egress_region: str | None = None, browser_profile: BrowserProfile | None = None, categories: Categories | None = None) -> dict[str, list[str]]

Create multiple browsing contexts for a list of URLs.

Efficiently creates contexts for multiple URLs with the same user and policy settings. Failed context creations are logged but don't stop the process for other URLs.

Parameters:

Name Type Description Default
urls list[str]

List of URLs to create contexts for

required
username str | None

Username associated with all contexts

None
policy list[dict[str, Any]] | None

List of policy dictionaries for all contexts

None
max_uses int | None

Maximum number of times each context can be used

None
expires str | None

Expiration time for all contexts

None
org str | None

Org slug for SAML-enabled organizations

None
session_type Literal['silo-ruby', 'toolbox-standalone'] | None

'silo-ruby' or 'toolbox-standalone'

None
egress_region str | None

Egress location (e.g., 'New York, NY')

None
browser_profile BrowserProfile | None

Browser profile settings

None
categories Categories | None

Egress category settings

None

Returns:

Type Description
dict[str, list[str]]

Dictionary containing list of successfully created context URLs

Raises:

Type Description
ValidationError

If parameters are invalid

Example

urls = ["https://site1.com", "https://site2.com"] policy = [{"type": "readonly", "params": ["true"]}] result = api.bulk_create_contexts(urls, "user@company.com", policy) print(f"Created {len(result['context_urls'])} contexts")

With Silo for Research settings

result = api.bulk_create_contexts( ... urls=urls, ... username="researcher@company.com", ... session_type="toolbox-standalone", ... egress_region="London" ... )

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.CONVENIENCE, wraps=["create_context"])
def bulk_create_contexts(
    self,
    urls: list[str],
    username: str | None = None,
    policy: list[dict[str, Any]] | None = None,
    max_uses: int | None = None,
    expires: str | None = None,
    org: str | None = None,
    session_type: Literal["silo-ruby", "toolbox-standalone"] | None = None,
    egress_region: str | None = None,
    browser_profile: BrowserProfile | None = None,
    categories: Categories | None = None,
) -> dict[str, list[str]]:
    """Create multiple browsing contexts for a list of URLs.

    Efficiently creates contexts for multiple URLs with the same user and
    policy settings. Failed context creations are logged but don't stop
    the process for other URLs.

    Args:
        urls: List of URLs to create contexts for
        username: Username associated with all contexts
        policy: List of policy dictionaries for all contexts
        max_uses: Maximum number of times each context can be used
        expires: Expiration time for all contexts
        org: Org slug for SAML-enabled organizations
        session_type: 'silo-ruby' or 'toolbox-standalone'
        egress_region: Egress location (e.g., 'New York, NY')
        browser_profile: Browser profile settings
        categories: Egress category settings

    Returns:
        Dictionary containing list of successfully created context URLs

    Raises:
        ValidationError: If parameters are invalid

    Example:
        >>> urls = ["https://site1.com", "https://site2.com"]
        >>> policy = [{"type": "readonly", "params": ["true"]}]
        >>> result = api.bulk_create_contexts(urls, "user@company.com", policy)
        >>> print(f"Created {len(result['context_urls'])} contexts")

        >>> # With Silo for Research settings
        >>> result = api.bulk_create_contexts(
        ...     urls=urls,
        ...     username="researcher@company.com",
        ...     session_type="toolbox-standalone",
        ...     egress_region="London"
        ... )
    """
    if not isinstance(urls, list) or not urls:
        raise ValidationError("URLs must be a non-empty list")

    if not username and not org:
        raise ValidationError("Either username or org must be provided")

    if policy is not None and not isinstance(policy, list):
        raise ValidationError("Policy must be a list of dictionaries")

    context_urls: list[str] = []

    self.logger.info("Creating contexts for %d URLs", len(urls))

    for i, url in enumerate(urls):
        try:
            # Refang URL in case it was defanged
            refanged_url = self.refang_url(url)

            # Create context with all parameters
            context_id = self.create_context(
                url=refanged_url,
                username=username,
                policy=policy,
                max_uses=max_uses,
                expires=expires,
                org=org,
                session_type=session_type,
                egress_region=egress_region,
                browser_profile=browser_profile,
                categories=categories,
            )

            # Generate launch URL
            launch_url = self.create_ctx_url(context_id)
            context_urls.append(launch_url)

            self.logger.debug("Created context %d/%d", i + 1, len(urls))

        except Exception as e:
            self.logger.error("Failed to create context for URL %s: %s", url, e)
            # Continue with other URLs
            continue

    self.logger.info(
        "Successfully created %d/%d contexts", len(context_urls), len(urls)
    )

    return {"context_urls": context_urls}

create_research_session

create_research_session(username: str, egress_region: str, url: str | None = None, browser_profile: BrowserProfile | None = None, categories: Categories | None = None, policy: list[dict[str, Any]] | None = None, max_uses: int | None = None, expires: str | None = None, auto_domain_allow: bool = False) -> str

Create a Silo for Research (toolbox-standalone) session.

Convenience method for creating research sessions with egress routing and browser profile configuration.

Parameters:

Name Type Description Default
username str

Username for the session

required
egress_region str

Egress location (e.g., 'New York, NY', 'London'). See :meth:get_egress_locations for available options.

required
url str | None

Optional URL to open in the session

None
browser_profile BrowserProfile | None

Browser profile with 'os', 'browser', and optional 'timezone'/'languages' settings. Options for os: 'win', 'mac', 'linux', 'android', 'ios' Options for browser: 'chrome', 'firefox', 'edge', 'safari', 'tor'

None
categories Categories | None

Egress category settings. Example:

None
policy list[dict[str, Any]] | None

Optional policy list for the session

None
max_uses int | None

Maximum number of times the context can be used

None
expires str | None

Context expiration time

None

Returns:

Type Description
str

Browse context ID that can be used to launch the session

Raises:

Type Description
BrowsingAPIError

If session creation fails

ValidationError

If parameters are invalid

Example

context_id = api.create_research_session( ... username="researcher@company.com", ... egress_region="New York, NY", ... url="https://example.com", ... browser_profile={"os": "win", "browser": "chrome"}, ... categories={"availability": "public", "connectivity": "isp"} ... ) launch_url = api.create_ctx_url(context_id)

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.CONVENIENCE, wraps=["create_context"])
def create_research_session(
    self,
    username: str,
    egress_region: str,
    url: str | None = None,
    browser_profile: BrowserProfile | None = None,
    categories: Categories | None = None,
    policy: list[dict[str, Any]] | None = None,
    max_uses: int | None = None,
    expires: str | None = None,
    auto_domain_allow: bool = False,
) -> str:
    """Create a Silo for Research (toolbox-standalone) session.

    Convenience method for creating research sessions with egress routing
    and browser profile configuration.

    Args:
        username: Username for the session
        egress_region: Egress location (e.g., 'New York, NY', 'London').
            See :meth:`get_egress_locations` for available options.
        url: Optional URL to open in the session
        browser_profile: Browser profile with 'os', 'browser', and optional
            'timezone'/'languages' settings.
            Options for os: 'win', 'mac', 'linux', 'android', 'ios'
            Options for browser: 'chrome', 'firefox', 'edge', 'safari', 'tor'
        categories: Egress category settings. Example:
            {"availability": "public", "connectivity": "datacenter"}
        policy: Optional policy list for the session
        max_uses: Maximum number of times the context can be used
        expires: Context expiration time

    Returns:
        Browse context ID that can be used to launch the session

    Raises:
        BrowsingAPIError: If session creation fails
        ValidationError: If parameters are invalid

    Example:
        >>> context_id = api.create_research_session(
        ...     username="researcher@company.com",
        ...     egress_region="New York, NY",
        ...     url="https://example.com",
        ...     browser_profile={"os": "win", "browser": "chrome"},
        ...     categories={"availability": "public", "connectivity": "isp"}
        ... )
        >>> launch_url = api.create_ctx_url(context_id)
    """
    return self.create_context(
        url=url,
        username=username,
        policy=policy,
        max_uses=max_uses,
        expires=expires,
        session_type="toolbox-standalone",
        egress_region=egress_region,
        browser_profile=browser_profile,
        categories=categories,
        auto_domain_allow=auto_domain_allow,
    )

get_egress_locations staticmethod

get_egress_locations(include_details: bool = False) -> dict[str, Any]

Get available egress locations by region.

Parameters:

Name Type Description Default
include_details bool

When False (default), returns a dict mapping region names to lists of location name strings — backward-compatible with prior behavior. When True, each location is a dict with name, connectivity, availability, and protocol keys.

False

Returns:

Type Description
dict[str, Any]

When include_details=False: {region: [city_name, ...]}

dict[str, Any]

When include_details=True: {region: [{"name": city, "connectivity": [...], "availability": [...], "protocol": [...]}]}

dict[str, Any]

Cities absent from the canonical location data (see

dict[str, Any]

func:~silo_sdk.harvesting.egress_validation.get_egress_locations_with_details)

dict[str, Any]

are omitted from the enriched output entirely, rather than

dict[str, Any]

appearing with empty detail lists.

Example

locations = BrowsingAPI.get_egress_locations() print(locations["North America"]) ['Toronto', 'Vancouver', 'Mexico City', ...]

details = BrowsingAPI.get_egress_locations(include_details=True) sao_paulo = next( ... e for e in details["Central & South America"] ... if e["name"] == "Sao Paulo" ... ) print("datacenter" in sao_paulo["connectivity"]) True sydney = next( ... e for e in details["Asia-Pacific"] if e["name"] == "Sydney" ... ) print("tor" in sydney["protocol"]) True

Source code in silo_sdk/browsing/isolation_api.py
@api_tag(MethodType.UTILITY)
@staticmethod
def get_egress_locations(
    include_details: bool = False,
) -> dict[str, Any]:
    """Get available egress locations by region.

    Args:
        include_details: When ``False`` (default), returns a dict mapping
            region names to lists of location name strings — backward-compatible
            with prior behavior. When ``True``, each location is a dict with
            ``name``, ``connectivity``, ``availability``, and ``protocol`` keys.

    Returns:
        When ``include_details=False``:
            ``{region: [city_name, ...]}``

        When ``include_details=True``:
            ``{region: [{"name": city, "connectivity": [...],
            "availability": [...], "protocol": [...]}]}``

        Cities absent from the canonical location data (see
        :func:`~silo_sdk.harvesting.egress_validation.get_egress_locations_with_details`)
        are omitted from the enriched output entirely, rather than
        appearing with empty detail lists.

    Example:
        >>> locations = BrowsingAPI.get_egress_locations()
        >>> print(locations["North America"])
        ['Toronto', 'Vancouver', 'Mexico City', ...]

        >>> details = BrowsingAPI.get_egress_locations(include_details=True)
        >>> sao_paulo = next(
        ...     e for e in details["Central & South America"]
        ...     if e["name"] == "Sao Paulo"
        ... )
        >>> print("datacenter" in sao_paulo["connectivity"])
        True
        >>> sydney = next(
        ...     e for e in details["Asia-Pacific"] if e["name"] == "Sydney"
        ... )
        >>> print("tor" in sydney["protocol"])
        True
    """
    if not include_details:
        details = get_egress_locations_with_details()
        return {
            region: [e["name"] for e in entries]
            for region, entries in details.items()
        }

    return get_egress_locations_with_details()

Policy Reference

Policy coverage

This page documents the confirmed policy types. Additional policy types may be available depending on your Silo license.

Browsing contexts created via BrowsingAPI.create_context() accept an optional policy list that controls session behavior. Each entry is a dict with a "type" string and a "params" list.

policy = [
    {"type": "file_transfer", "params": ["block_all"]},
    {"type": "clipboard",     "params": ["allow_to_local"]},
    {"type": "readonly",      "params": ["false"]},
    {"type": "browser_chrome","params": ["standard"]},
]

context_id = api.create_context(
    url="https://example.com",
    username="analyst@company.com",
    policy=policy,
)

domain_allow injection is opt-in via the auto_domain_allow parameter — it is not added automatically when url is provided. See the domain_allow section below for details.


Policy Types

readonly

Makes the session read-only — blocks downloads, clipboard interaction, and other write operations.

Param Effect
"true" Session is read-only
"false" Session allows interaction (default)
{"type": "readonly", "params": ["true"]}

file_transfer

Controls whether files can be uploaded or downloaded during the session.

Param Effect
"block_all" Block all file transfers (upload and download)
"allow_all" Allow all file transfers
{"type": "file_transfer", "params": ["block_all"]}

clipboard

Controls the direction of clipboard operations between the local machine and the isolated session.

Param Effect
"block_all" Block clipboard in both directions
"allow_all" Allow clipboard in both directions
"allow_to_local" Allow copying from the session to the local machine
"block_to_silo" Block pasting into the session from the local machine

Multiple params can be combined in the same list:

{"type": "clipboard", "params": ["allow_to_local", "block_to_silo"]}

ad_block

Enables or disables ad blocking for the session.

Param Effect
"enable" Ad blocking on
"disable" Ad blocking off
{"type": "ad_block", "params": ["enable"]}

browser_chrome

Controls the visual UI mode of the Silo ribbon (the banner displayed during an isolation session).

Param UI mode Notes
"standard" Full ribbon UI (default) All controls visible
"seamless" Hidden ribbon — transparent browsing Non-catchall users only — see warning below
"minimal" Minimal ribbon (Ribbon Mode) Reduced controls visible
{"type": "browser_chrome", "params": ["seamless"]}

Seamless UI does not apply to catchall users

The "seamless" mode is only effective for non-catchall users.

A user is treated as catchall when accessed via a catchall account, when the org is configured to treat all users as catchall, or via partner SSO. Catchall users are always forced into "minimal" (Ribbon Mode) regardless of the browser_chrome policy value. This is a platform-level enforcement and cannot be overridden via policy.

If you need seamless UI, ensure users are not provisioned as catchall.


domain_allow

Whitelists specific domains that the user is permitted to navigate to during the session.

{"type": "domain_allow", "params": ["example.com"]}

Opt-in auto-injection with auto_domain_allow

When you pass a url to create_context(), a domain_allow entry for the URL's domain is not added automatically. To enable automatic injection of the URL's domain, pass auto_domain_allow=True:

context_id = api.create_context(
    url="https://example.com",
    username="analyst@company.com",
    policy=policy,
    auto_domain_allow=True,  # injects {"type": "domain_allow", "params": ["example.com"]}
)

Leave auto_domain_allow=False (the default) when you want to construct your own domain_allow entry — for example, to allow multiple domains in a single entry:

policy = [
    {
        "type": "domain_allow",
        "params": ["google.com", "yahoo.com"],
    }
]
context_id = api.create_context(
    url="https://google.com",
    username="analyst@company.com",
    policy=policy,
    # auto_domain_allow=False (default) — the policy above takes effect as-is
)

Auto-injection only adds the single domain extracted from url; it cannot be used to allow additional domains beyond the target URL's domain.

Multiple domains can be included in a single domain_allow entry or by adding separate entries:

# Single entry — multiple domains in one params list
policy = [
    {"type": "domain_allow", "params": ["example.com", "trusted-partner.com"]},
]

# Multiple entries — one domain each
policy = [
    {"type": "domain_allow", "params": ["example.com"]},
    {"type": "domain_allow", "params": ["trusted-partner.com"]},
]

domain_block

Blacklists specific domains, preventing the user from navigating to them during the session.

{"type": "domain_block", "params": ["blocked-site.com"]}

ribbon_background_color

Sets a custom background color for the Silo ribbon/HUD banner. Useful for branding or indicating session type (e.g., red for sensitive environments).

{"type": "ribbon_background_color", "params": ["#cc0000"]}

Accepts a CSS hex color string (e.g., "#cc0000", "#ffffff").


ribbon_text_color

Sets a custom text color for the Silo ribbon/HUD banner.

{"type": "ribbon_text_color", "params": ["#ffffff"]}

ribbon_message

Displays a custom message in the Silo ribbon/HUD banner. Maximum 100 characters.

{"type": "ribbon_message", "params": ["Sensitive session — do not share screen"]}

Complete Custom Policy Example

The Postman collection Create Context — Custom Policy request demonstrates a context with multiple policies configured together:

policy = [
    {"type": "file_transfer",  "params": ["block_all"]},
    {"type": "clipboard",      "params": ["allow_to_local", "block_to_silo"]},
    {"type": "readonly",       "params": ["false"]},
    {"type": "browser_chrome", "params": ["standard"]},
]

context_id = api.create_context(
    url="https://secure.example.com",
    username="analyst@company.com",
    policy=policy,
    max_uses=5,
    expires="3600",          # 1 hour
    auto_domain_allow=True,  # automatically allow the URL's domain
)

Seamless UI Example

# Only use this with non-catchall users (catchall users are forced to minimal)
policy = [
    {"type": "browser_chrome", "params": ["seamless"]},
    {"type": "file_transfer",  "params": ["block_all"]},
]

context_id = api.create_context(
    url="https://app.example.com",
    username="authenticated.user@company.com",
    policy=policy,
    max_uses=1,
    auto_domain_allow=True,  # allow the app domain automatically
)

Browser Profile

create_context() accepts an optional browser_profile dict that controls the emulated user agent for the session.

Key Description Example
os Operating system to emulate "win", "mac", "linux", "android", "ios"
browser Browser to emulate "chrome", "firefox", "edge", "safari", "tor"
timezone IANA timezone name (optional) "America/New_York"
languages Accept-Language header value (optional) "en-US,en"
context_id = api.create_context(
    url="https://example.com",
    username="user@company.com",
    browser_profile={
        "os": "win",
        "browser": "chrome",
        "timezone": "America/New_York",
        "languages": "en-US,en",
    },
)