Repository: hikari-py/hikari
Pull request #2337 (2025-04-28 10:28:48)

Implement Guild Onboarding
Implement Guild Onboarding

### Summary
<!-- Small summary of the merge request -->

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
#2227 


review-ai-agent[bot]: <h3>Pull Request Revisions</h3>
<table><tr><th>Revision</th><th>Description</th></tr><tr><td><a href="https://review.ai/pr/hikari-py/hikari/2337?rev=1">r1</a></td><td><i>Generating summary...</i></td></tr></table><hr/>☑️ AI review skipped for <a href="https://review.ai/pr/hikari-py/hikari/2337?rev=1">r1</a><details><summary>Help</summary>
React with emojis to give feedback on AI-generated reviews:
<ul>
<li>👍 means the feedback was helpful and actionable</li>
<li>👎 means the feedback was incorrect or unhelpful</li>
</ul>
💬 Replying to feedback with a comment helps us improve the system. Your input also contributes to shaping future interactions with the AI reviewer.
<br/><br/>
We'd love to hear from you—reach out anytime at <a href="mailto:team@review.ai">team@review.ai</a>.
</details>

Patch:
diff --git a/changes/2337.feature.md b/changes/2337.feature.md
new file mode 100644
index 00000000..15db1f9b
--- /dev/null
+++ b/changes/2337.feature.md
@@ -0,0 +1,1 @@
+Add support for fetching and editing guild onboarding (`rest.fetch_guild_onboarding`, `rest.edit_guild_onboarding`)
diff --git a/hikari/api/entity_factory.py b/hikari/api/entity_factory.py
index fa02a242..e36d76fb 100644
--- a/hikari/api/entity_factory.py
+++ b/hikari/api/entity_factory.py
@@ -1084,6 +1084,21 @@ def serialize_welcome_channel(self, welcome_channel: guild_models.WelcomeChannel
             The serialized representation of the welcome channel.
         """
 
+    @abc.abstractmethod
+    def deserialize_guild_onboarding(self, payload: data_binding.JSONObject) -> guild_models.GuildOnboarding:
+        """Parse a raw payload from Discord into a guild onboarding object.
+
+        Parameters
+        ----------
+        payload
+            The JSON payload to deserialize.
+
+        Returns
+        -------
+        hikari.guilds.GuildOnboarding
+            The deserialized guild onboarding object.
+        """
+
     @abc.abstractmethod
     def deserialize_member(
         self,
diff --git a/hikari/api/rest.py b/hikari/api/rest.py
index c6b755c4..a6ae7b70 100644
--- a/hikari/api/rest.py
+++ b/hikari/api/rest.py
@@ -6947,6 +6947,87 @@ async def edit_welcome_screen(
             If an internal error occurs on Discord while handling the request.
         """
 
+    @abc.abstractmethod
+    async def fetch_guild_onboarding(
+        self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild]
+    ) -> guilds.GuildOnboarding:
+        """Fetch a guild's onboarding object.
+
+        Parameters
+        ----------
+        guild
+            Object or ID of the guild to fetch the onboarding object for.
+
+        Returns
+        -------
+        hikari.guilds.GuildOnboarding
+            The requested onboarding object.
+
+        Raises
+        ------
+        hikari.errors.NotFoundError
+            If the guild is not found.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
+    @abc.abstractmethod
+    async def edit_guild_onboarding(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        *,
+        default_channel_ids: undefined.UndefinedOr[
+            snowflakes.SnowflakeishSequence[channels_.GuildChannel]
+        ] = undefined.UNDEFINED,
+        enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
+        mode: undefined.UndefinedOr[guilds.GuildOnboardingMode] = undefined.UNDEFINED,
+        prompts: undefined.UndefinedOr[
+            typing.Sequence[special_endpoints.GuildOnboardingPromptBuilder]
+        ] = undefined.UNDEFINED,
+        reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+    ) -> guilds.GuildOnboarding:
+        """Edit a guilds onboarding flow.
+
+        Parameters
+        ----------
+        guild
+            Object or ID of the guild to fetch the onboarding object for.
+        default_channel_ids
+            Sequence of channel ids that a user get opted into by default.
+        enabled
+            If the onboarding flow should be enabled in this guild.
+        mode
+            The onboarding mode for the guild. For further information look at [`hikari.guilds.GuildOnboardingMode`][].
+        prompts
+            The prompts of the onboarding flow.
+            For further information look at [`hikari.api.special_endpoints.GuildOnboardingPromptBuilder`][].
+        reason
+            If provided, the reason that will be recorded in the audit logs.
+            Maximum of 512 characters.
+
+        Returns
+        -------
+        hikari.guilds.GuildOnboarding
+            The requested onboarding object.
+
+        Raises
+        ------
+        hikari.errors.NotFoundError
+            If the guild is not found.
+        hikari.errors.UnauthorizedError
+            If you are unauthorized to make the request (invalid/missing token).
+        hikari.errors.RateLimitTooLongError
+            Raised in the event that a rate limit occurs that is
+            longer than `max_rate_limit` when making a request.
+        hikari.errors.InternalServerError
+            If an internal error occurs on Discord while handling the request.
+        """
+
     @abc.abstractmethod
     async def fetch_vanity_url(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild]) -> invites.VanityURL:
         """Fetch a guild's vanity url.
diff --git a/hikari/api/special_endpoints.py b/hikari/api/special_endpoints.py
index a0e61412..8686ded9 100644
--- a/hikari/api/special_endpoints.py
+++ b/hikari/api/special_endpoints.py
@@ -203,6 +203,233 @@ def set_flags(self, flags: undefined.UndefinedType | int | messages.MessageFlag,
         """
 
 
+class GuildOnboardingPromptOptionBuilder(abc.ABC):
+    """Interface of a guild onboarding prompt option used to edit guild onboardings."""
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    def title(self) -> str:
+        """The prompt's title."""
+
+    @abc.abstractmethod
+    def set_title(self, title: str, /) -> Self:
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @property
+    @abc.abstractmethod
+    def role_ids(self) -> snowflakes.SnowflakeishSequence[guilds.Role]:
+        """The prompt's title."""
+
+    @abc.abstractmethod
+    def set_role_ids(self, role_ids: snowflakes.SnowflakeishSequence[guilds.Role], /) -> Self:
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @property
+    @abc.abstractmethod
+    def channel_ids(self) -> snowflakes.SnowflakeishSequence[channels.GuildChannel]:
+        """The prompt's title."""
+
+    @abc.abstractmethod
+    def set_channel_ids(self, channel_ids: snowflakes.SnowflakeishSequence[channels.GuildChannel], /) -> Self:
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @property
+    @abc.abstractmethod
+    def description(self) -> undefined.UndefinedOr[str]:
+        """The prompt's title."""
+
+    @abc.abstractmethod
+    def set_description(self, title: undefined.UndefinedOr[str], /) -> Self:
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @property
+    @abc.abstractmethod
+    def emoji(self) -> undefined.UndefinedOr[snowflakes.Snowflakeish | emojis.Emoji | str]:
+        """Emoji which should appear on this option."""
+
+    @abc.abstractmethod
+    def set_emoji(self, emoji: undefined.UndefinedOr[snowflakes.Snowflakeish | emojis.Emoji | str], /) -> Self:
+        """Set the emoji to display on this option.
+
+        Parameters
+        ----------
+        emoji
+            Object, ID or raw string of the emoji which should be displayed on
+            this option.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @abc.abstractmethod
+    def build(self) -> typing.MutableMapping[str, typing.Any]:
+        """Build a JSON object from this builder.
+
+        Returns
+        -------
+        typing.MutableMapping[str, typing.Any]
+            The built json object representation of this builder.
+        """
+
+
+class GuildOnboardingPromptBuilder(abc.ABC):
+    """Interface of a guild onboarding prompt used to respond to edit guild onboardings.
+
+    !!! Note
+        You cannot set the type of the prompt. Discord will automatically do that
+        based on how many options the prompt has.
+    """
+
+    __slots__: typing.Sequence[str] = ()
+
+    @property
+    @abc.abstractmethod
+    def id(self) -> undefined.UndefinedOr[snowflakes.SnowflakeishOr[guilds.GuildOnboardingPrompt]]:
+        """The prompt's id."""
+
+    @abc.abstractmethod
+    def set_id(self, id: undefined.UndefinedOr[snowflakes.SnowflakeishOr[guilds.GuildOnboardingPrompt]], /) -> Self:
+        """Set this prompt's id.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @property
+    @abc.abstractmethod
+    def title(self) -> str:
+        """The prompt's title."""
+
+    @abc.abstractmethod
+    def set_title(self, title: str, /) -> Self:
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @property
+    @abc.abstractmethod
+    def single_select(self) -> bool:
+        """The prompt's title."""
+
+    @abc.abstractmethod
+    def set_single_select(self, single_select: bool, /) -> Self:  # noqa: FBT001 - Boolean-typed positional argument
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @property
+    @abc.abstractmethod
+    def required(self) -> bool:
+        """The prompt's title."""
+
+    @abc.abstractmethod
+    def set_required(self, required: bool, /) -> Self:  # noqa: FBT001 - Boolean-typed positional argument
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @property
+    @abc.abstractmethod
+    def in_onboarding(self) -> bool:
+        """The prompt's title."""
+
+    @abc.abstractmethod
+    def set_in_onboarding(self, in_onboarding: bool, /) -> Self:  # noqa: FBT001 - Boolean-typed positional argument
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @property
+    @abc.abstractmethod
+    def options(self) -> typing.Sequence[GuildOnboardingPromptOptionBuilder]:
+        """The prompt's title."""
+
+    @abc.abstractmethod
+    def set_options(self, options: typing.Sequence[GuildOnboardingPromptOptionBuilder], /) -> Self:
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @abc.abstractmethod
+    def add_option(
+        self,
+        title: str,
+        role_ids: undefined.UndefinedNoneOr[snowflakes.SnowflakeishSequence[guilds.Role]] = undefined.UNDEFINED,
+        channel_ids: undefined.UndefinedNoneOr[
+            snowflakes.SnowflakeishSequence[channels.GuildChannel]
+        ] = undefined.UNDEFINED,
+        description: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+        emoji: undefined.UndefinedOr[snowflakes.Snowflakeish | emojis.Emoji | str] = undefined.UNDEFINED,
+    ) -> Self:
+        """Set this prompt's title.
+
+        Returns
+        -------
+        GuildOnboardingPromptBuilder
+            The guild onboarding builder.
+        """
+
+    @abc.abstractmethod
+    def build(self) -> typing.MutableMapping[str, typing.Any]:
+        """Build a JSON object from this builder.
+
+        Returns
+        -------
+        typing.MutableMapping[str, typing.Any]
+            The built json object representation of this builder.
+        """
+
+
 class AutocompleteChoiceBuilder(abc.ABC):
     """Interface of an autocomplete choice used to respond to interactions."""
 
diff --git a/hikari/guilds.py b/hikari/guilds.py
index 6dabad3c..efb1766d 100644
--- a/hikari/guilds.py
+++ b/hikari/guilds.py
@@ -99,6 +99,28 @@ class GuildExplicitContentFilterLevel(int, enums.Enum):
     """Filter all posts."""
 
 
+@typing.final
+class GuildOnboardingMode(int, enums.Enum):
+    """Defines the criteria used to satisfy Onboarding constraints that are required for enabling."""
+
+    ONBOARDING_DEFAULT = 0
+    """Counts only Default Channels towards constraints."""
+
+    ONBOARDING_ADVANCED = 1
+    """Counts Default Channels and Questions towards constraints."""
+
+
+@typing.final
+class GuildOnboardingPromptType(int, enums.Enum):
+    """Guild Onboarding Prompt Type. This is automatically decided by discord."""
+
+    MULTIPLE_CHOICE = 0
+    """Multiple fields you can click at."""
+
+    DROPDOWN = 1
+    """Similar to a select menu component."""
+
+
 @typing.final
 class GuildFeature(str, enums.Enum):
     """Features that a guild can provide."""
@@ -1621,6 +1643,81 @@ class WelcomeScreen:
     """An array of up to 5 of the channels shown in the welcome screen."""
 
 
+@attrs_extensions.with_copy
+@attrs.define(kw_only=True, weakref_slot=False)
+class GuildOnboarding:
+    """Used to represent the Guild Onboarding Object."""
+
+    guild_id: snowflakes.Snowflake = attrs.field(hash=True, repr=True)
+    """The ID of the Guild this Onboarding belongs to."""
+
+    default_channel_ids: typing.Sequence[snowflakes.Snowflake] = attrs.field(hash=False, repr=True)
+    """Channel IDs that members get opted into automatically."""
+
+    enabled: bool = attrs.field(hash=False, repr=True)
+    """Whether onboarding is enabled in the guild."""
+
+    mode: GuildOnboardingMode = attrs.field(hash=False, repr=True)
+    """Current mode of onboarding."""
+
+    prompts: typing.Sequence[GuildOnboardingPrompt] = attrs.field(hash=False, repr=True)
+    """Prompts shown during onboarding and in customize community."""
+
+
+@attrs_extensions.with_copy
+@attrs.define(kw_only=True, weakref_slot=False)
+class GuildOnboardingPromptOption(snowflakes.Unique):
+    """Used to represent a Guild Onboarding Prompt Option."""
+
+    id: snowflakes.Snowflake = attrs.field(hash=True, repr=True)
+    """The ID of the option"""
+
+    channel_ids: typing.Sequence[snowflakes.Snowflake] = attrs.field(hash=False, repr=True)
+    """The IDs of the channels a user is opted into when selecting this option."""
+
+    role_ids: typing.Sequence[snowflakes.Snowflake] = attrs.field(hash=False, repr=True)
+    """The IDs of the roles a user will get assigned when selecting this option."""
+
+    title: str = attrs.field(hash=False, repr=True)
+    """The title of the option."""
+
+    description: str | None = attrs.field(hash=False, repr=True)
+    """The description of the option."""
+
+    emoji: emojis_.Emoji = attrs.field(hash=False, repr=False)
+    """Emoji of the option."""
+
+
+@attrs_extensions.with_copy
+@attrs.define(kw_only=True, weakref_slot=False)
+class GuildOnboardingPrompt(snowflakes.Unique):
+    """Used to represent a Guild Onboarding Prompt."""
+
+    id: snowflakes.Snowflake = attrs.field(hash=True, repr=True)
+    """The ID of the prompt."""
+
+    type: GuildOnboardingPromptType = attrs.field(hash=False, repr=True)
+    """The type of the prompt."""
+
+    options: typing.Sequence[GuildOnboardingPromptOption] = attrs.field(hash=False, repr=True)
+    """The options of the prompt."""
+
+    title: str = attrs.field(hash=False, repr=True)
+    """The title of the prompt."""
+
+    single_select: bool = attrs.field(hash=False, repr=True)
+    """Indicates whether users are limited to selecting one option for the prompt."""
+
+    required: bool = attrs.field(hash=False, repr=True)
+    """Indicates whether the prompt is required before a user completes the onboarding flow."""
+
+    in_onboarding: bool = attrs.field(hash=False, repr=True)
+    """Indicates whether the prompt is present in the onboarding flow.
+
+    If [`False`][], the prompt will only appear in the Channels & Roles tab
+    """
+
+
 @attrs_extensions.with_copy
 @attrs.define(kw_only=True, weakref_slot=False)
 class GuildBan:
diff --git a/hikari/impl/entity_factory.py b/hikari/impl/entity_factory.py
index f6b9b471..88a06c3c 100644
--- a/hikari/impl/entity_factory.py
+++ b/hikari/impl/entity_factory.py
@@ -2023,6 +2023,53 @@ def serialize_welcome_channel(self, welcome_channel: guild_models.WelcomeChannel
 
         return payload
 
+    def _deserialize_guild_onboarding_prompt(
+        self, payload: data_binding.JSONObject
+    ) -> guild_models.GuildOnboardingPrompt:
+        options: list[guild_models.GuildOnboardingPromptOption] = []
+        for option_payload in payload["options"]:
+            emoji = self.deserialize_emoji(option_payload["emoji"])
+            channel_ids: list[snowflakes.Snowflake] = [
+                snowflakes.Snowflake(channel_id) for channel_id in option_payload["channel_ids"]
+            ]
+            role_ids: list[snowflakes.Snowflake] = [
+                snowflakes.Snowflake(role_id) for role_id in option_payload["role_ids"]
+            ]
+            options.append(
+                guild_models.GuildOnboardingPromptOption(
+                    id=snowflakes.Snowflake(option_payload["id"]),
+                    channel_ids=channel_ids,
+                    role_ids=role_ids,
+                    title=option_payload["title"],
+                    description=option_payload.get("description"),
+                    emoji=emoji,
+                )
+            )
+
+        return guild_models.GuildOnboardingPrompt(
+            id=snowflakes.Snowflake(payload["id"]),
+            type=guild_models.GuildOnboardingPromptType(payload["type"]),
+            in_onboarding=payload["in_onboarding"],
+            required=payload["required"],
+            single_select=payload["single_select"],
+            title=payload["title"],
+            options=options,
+        )
+
+    @typing_extensions.override
+    def deserialize_guild_onboarding(self, payload: data_binding.JSONObject) -> guild_models.GuildOnboarding:
+        default_channel_ids = [
+            snowflakes.Snowflake(default_channel_id) for default_channel_id in payload["default_channel_ids"]
+        ]
+        prompts = [self._deserialize_guild_onboarding_prompt(prompt_payload) for prompt_payload in payload["prompts"]]
+        return guild_models.GuildOnboarding(
+            guild_id=snowflakes.Snowflake(payload["guild_id"]),
+            enabled=payload["enabled"],
+            mode=guild_models.GuildOnboardingMode(payload["mode"]),
+            default_channel_ids=default_channel_ids,
+            prompts=prompts,
+        )
+
     @typing_extensions.override
     def deserialize_member(
         self,
diff --git a/hikari/impl/rest.py b/hikari/impl/rest.py
index 1975eee4..4e42196f 100644
--- a/hikari/impl/rest.py
+++ b/hikari/impl/rest.py
@@ -503,6 +503,17 @@ def _transform_emoji_to_url_format(
     return emoji
 
 
+def _build_prompts(
+    prompts: typing.Sequence[special_endpoints.GuildOnboardingPromptBuilder],
+) -> list[typing.MutableMapping[str, typing.Any]]:
+    prompt_bodys: list[typing.MutableMapping[str, typing.Any]] = []
+    for index, prompt in enumerate(prompts):
+        if prompt.id is undefined.UNDEFINED:
+            prompt.set_id(index)
+        prompt_bodys.append(prompt.build())
+    return prompt_bodys
+
+
 class RESTClientImpl(rest_api.RESTClient):
     """Implementation of the V10-compatible Discord HTTP API.
 
@@ -4046,6 +4057,42 @@ async def edit_welcome_screen(
         assert isinstance(response, dict)
         return self._entity_factory.deserialize_welcome_screen(response)
 
+    @typing_extensions.override
+    async def fetch_guild_onboarding(
+        self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild]
+    ) -> guilds.GuildOnboarding:
+        route = routes.GET_GUILD_ONBOARDING.compile(guild=guild)
+        response = await self._request(route)
+        assert isinstance(response, dict)
+        return self._entity_factory.deserialize_guild_onboarding(response)
+
+    @typing_extensions.override
+    async def edit_guild_onboarding(
+        self,
+        guild: snowflakes.SnowflakeishOr[guilds.PartialGuild],
+        *,
+        default_channel_ids: undefined.UndefinedOr[
+            snowflakes.SnowflakeishSequence[channels_.GuildChannel]
+        ] = undefined.UNDEFINED,
+        enabled: undefined.UndefinedOr[bool] = undefined.UNDEFINED,
+        mode: undefined.UndefinedOr[guilds.GuildOnboardingMode] = undefined.UNDEFINED,
+        prompts: undefined.UndefinedOr[
+            typing.Sequence[special_endpoints.GuildOnboardingPromptBuilder]
+        ] = undefined.UNDEFINED,
+        reason: undefined.UndefinedOr[str] = undefined.UNDEFINED,
+    ) -> guilds.GuildOnboarding:
+        route = routes.PUT_GUILD_ONBOARDING.compile(guild=guild)
+        body = data_binding.JSONObjectBuilder()
+        body.put_snowflake_array("default_channel_ids", default_channel_ids)
+        body.put("enabled", enabled)
+        body.put("mode", mode, conversion=int)
+        if prompts is not undefined.UNDEFINED:
+            body.put("prompts", _build_prompts(prompts))
+
+        response = await self._request(route, json=body, reason=reason)
+        assert isinstance(response, dict)
+        return self._entity_factory.deserialize_guild_onboarding(response)
+
     @typing_extensions.override
     async def fetch_vanity_url(self, guild: snowflakes.SnowflakeishOr[guilds.PartialGuild]) -> invites.VanityURL:
         route = routes.GET_GUILD_VANITY_URL.compile(guild=guild)
diff --git a/hikari/impl/special_endpoints.py b/hikari/impl/special_endpoints.py
index e5939b58..4f6633c7 100644
--- a/hikari/impl/special_endpoints.py
+++ b/hikari/impl/special_endpoints.py
@@ -662,6 +662,203 @@ def build(self) -> typing.MutableMapping[str, typing.Any]:
         return {"name": self._name, "value": self._value}
 
 
+@attrs_extensions.with_copy
+@attrs.define(kw_only=False, weakref_slot=False)
+class GuildOnboardingPromptOptionBuilder(special_endpoints.GuildOnboardingPromptOptionBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.GuildOnboardingPromptOptionBuilder`][]."""
+
+    _title: str = attrs.field(alias="title")
+    _role_ids: snowflakes.SnowflakeishSequence[guilds.Role] = attrs.field(alias="role_ids", kw_only=True, factory=tuple)
+    _channel_ids: snowflakes.SnowflakeishSequence[channels.GuildChannel] = attrs.field(
+        alias="channel_ids", kw_only=True, factory=tuple
+    )
+    _description: undefined.UndefinedOr[str] = attrs.field(
+        alias="description", kw_only=True, default=undefined.UNDEFINED
+    )
+    _emoji: undefined.UndefinedOr[snowflakes.Snowflakeish | emojis.Emoji | str] = attrs.field(
+        alias="emoji", default=undefined.UNDEFINED, kw_only=True
+    )
+    _emoji_id: undefined.UndefinedOr[str] = attrs.field(init=False, default=undefined.UNDEFINED)
+    _emoji_name: undefined.UndefinedOr[str] = attrs.field(init=False, default=undefined.UNDEFINED)
+
+    def __attrs_post_init__(self) -> None:
+        self._emoji_id, self._emoji_name = _build_emoji(self._emoji)
+
+    @property
+    @typing_extensions.override
+    def title(self) -> str:
+        return self._title
+
+    @typing_extensions.override
+    def set_title(self, title: str, /) -> Self:
+        self._title = title
+        return self
+
+    @property
+    @typing_extensions.override
+    def role_ids(self) -> snowflakes.SnowflakeishSequence[guilds.Role]:
+        return self._role_ids
+
+    @typing_extensions.override
+    def set_role_ids(self, role_ids: snowflakes.SnowflakeishSequence[guilds.Role], /) -> Self:
+        self._role_ids = role_ids
+        return self
+
+    @typing_extensions.override
+    def set_channel_ids(self, channel_ids: snowflakes.SnowflakeishSequence[channels.GuildChannel], /) -> Self:
+        self._channel_ids = channel_ids
+        return self
+
+    @property
+    @typing_extensions.override
+    def channel_ids(self) -> snowflakes.SnowflakeishSequence[channels.GuildChannel]:
+        return self._channel_ids
+
+    @property
+    @typing_extensions.override
+    def description(self) -> undefined.UndefinedOr[str]:
+        return self._description
+
+    @typing_extensions.override
+    def set_description(self, description: undefined.UndefinedOr[str], /) -> Self:
+        self._description = description
+        return self
+
+    @property
+    @typing_extensions.override
+    def emoji(self) -> undefined.UndefinedOr[snowflakes.Snowflakeish | emojis.Emoji | str]:
+        return self._emoji
+
+    @typing_extensions.override
+    def set_emoji(self, emoji: undefined.UndefinedOr[snowflakes.Snowflakeish | emojis.Emoji | str], /) -> Self:
+        self._emoji_id, self._emoji_name = _build_emoji(emoji)
+        self._emoji = emoji
+        return self
+
+    @typing_extensions.override
+    def build(self) -> typing.MutableMapping[str, typing.Any]:
+        body = data_binding.JSONObjectBuilder()
+        body.put("title", self._title)
+        body.put("description", self._description)
+        body.put_snowflake_array("channel_ids", self._channel_ids)
+        body.put_snowflake_array("role_ids", self._role_ids)
+
+        body.put("emoji_name", self._emoji_name)
+        body.put("emoji_id", self._emoji_id)
+
+        return body
+
+
+@attrs_extensions.with_copy
+@attrs.define(kw_only=False, weakref_slot=False)
+class GuildOnboardingPromptBuilder(special_endpoints.GuildOnboardingPromptBuilder):
+    """Standard implementation of [`hikari.api.special_endpoints.GuildOnboardingPromptBuilder`][]."""
+
+    _title: str = attrs.field(alias="title")
+    _single_select: bool = attrs.field(alias="single_select")
+    _required: bool = attrs.field(alias="required")
+    _in_onboarding: bool = attrs.field(alias="in_onboarding")
+    _options: list[special_endpoints.GuildOnboardingPromptOptionBuilder] = attrs.field(alias="options", factory=list)
+    _id: undefined.UndefinedOr[snowflakes.SnowflakeishOr[guilds.GuildOnboardingPrompt]] = attrs.field(
+        alias="id",
+        default=undefined.UNDEFINED,
+        init=False,  # We do not need to allow the user to specify an id here. Discord ignores the id anyway.
+    )
+
+    @property
+    @typing_extensions.override
+    def id(self) -> undefined.UndefinedOr[snowflakes.SnowflakeishOr[guilds.GuildOnboardingPrompt]]:
+        return self._id
+
+    @typing_extensions.override
+    def set_id(self, id: undefined.UndefinedOr[snowflakes.SnowflakeishOr[guilds.GuildOnboardingPrompt]], /) -> Self:
+        self._id = id
+        return self
+
+    @property
+    @typing_extensions.override
+    def title(self) -> str:
+        return self._title
+
+    @typing_extensions.override
+    def set_title(self, title: str, /) -> Self:
+        self._title = title
+        return self
+
+    @property
+    @typing_extensions.override
+    def single_select(self) -> bool:
+        return self._single_select
+
+    @typing_extensions.override
+    def set_single_select(self, single_select: bool, /) -> Self:
+        self._single_select = single_select
+        return self
+
+    @property
+    @typing_extensions.override
+    def required(self) -> bool:
+        return self._required
+
+    @typing_extensions.override
+    def set_required(self, required: bool, /) -> Self:
+        self._required = required
+        return self
+
+    @property
+    @typing_extensions.override
+    def in_onboarding(self) -> bool:
+        return self._in_onboarding
+
+    @typing_extensions.override
+    def set_in_onboarding(self, in_onboarding: bool, /) -> Self:
+        self._in_onboarding = in_onboarding
+        return self
+
+    @property
+    @typing_extensions.override
+    def options(self) -> typing.Sequence[special_endpoints.GuildOnboardingPromptOptionBuilder]:
+        return self._options
+
+    @typing_extensions.override
+    def set_options(self, options: typing.Sequence[special_endpoints.GuildOnboardingPromptOptionBuilder], /) -> Self:
+        self._options = list(options)
+        return self
+
+    @typing_extensions.override
+    def add_option(
+        self,
+        title: str,
+        role_ids: undefined.UndefinedNoneOr[snowflakes.SnowflakeishSequence[guilds.Role]] = undefined.UNDEFINED,
+        channel_ids: undefined.UndefinedNoneOr[
+            snowflakes.SnowflakeishSequence[channels.GuildChannel]
+        ] = undefined.UNDEFINED,
+        description: undefined.UndefinedNoneOr[str] = undefined.UNDEFINED,
+        emoji: undefined.UndefinedNoneOr[snowflakes.Snowflakeish | emojis.Emoji | str] = undefined.UNDEFINED,
+    ) -> Self:
+        self._options.append(
+            GuildOnboardingPromptOptionBuilder(
+                title=title,
+                role_ids=role_ids or [],
+                channel_ids=channel_ids or [],
+                description=description or undefined.UNDEFINED,
+                emoji=emoji or undefined.UNDEFINED,
+            )
+        )
+        return self
+
+    @typing_extensions.override
+    def build(self) -> typing.MutableMapping[str, typing.Any]:
+        body = data_binding.JSONObjectBuilder()
+        body.put_array("options", self._options, conversion=lambda option: option.build())
+        body.put("title", self._title)
+        body.put("single_select", self._single_select)
+        body.put("required", self._required)
+        body.put("in_onboarding", self._in_onboarding)
+        body.put_snowflake("id", self._id)
+        return body
+
+
 @attrs_extensions.with_copy
 @attrs.define(weakref_slot=False)
 class InteractionAutocompleteBuilder(special_endpoints.InteractionAutocompleteBuilder):
diff --git a/hikari/internal/routes.py b/hikari/internal/routes.py
index 127990da..e2845588 100644
--- a/hikari/internal/routes.py
+++ b/hikari/internal/routes.py
@@ -420,6 +420,9 @@ def compile_to_file(
 GET_GUILD_WELCOME_SCREEN: typing.Final[Route] = Route(GET, "/guilds/{guild}/welcome-screen")
 PATCH_GUILD_WELCOME_SCREEN: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/welcome-screen")
 
+GET_GUILD_ONBOARDING: typing.Final[Route] = Route(GET, "/guilds/{guild}/onboarding")
+PUT_GUILD_ONBOARDING: typing.Final[Route] = Route(PUT, "/guilds/{guild}/onboarding")
+
 GET_GUILD_MEMBER_VERIFICATION: typing.Final[Route] = Route(GET, "/guilds/{guild}/member-verification")
 PATCH_GUILD_MEMBER_VERIFICATION: typing.Final[Route] = Route(PATCH, "/guilds/{guild}/member-verification")
 


Test patch:
diff --git a/tests/hikari/impl/test_entity_factory.py b/tests/hikari/impl/test_entity_factory.py
index ca634e11..b40c0bc0 100644
--- a/tests/hikari/impl/test_entity_factory.py
+++ b/tests/hikari/impl/test_entity_factory.py
@@ -3338,6 +3338,67 @@ def test_serialize_welcome_channel_with_no_emoji(self, entity_factory_impl, mock
 
         assert result == {"channel_id": "4312312", "description": "meow2"}
 
+    @pytest.fixture
+    def guild_onboarding_prompt_payload(self) -> typing.MutableMapping[str, typing.Any]:
+        return {
+            "id": "123",
+            "type": 0,
+            "title": "test",
+            "single_select": True,
+            "required": True,
+            "in_onboarding": True,
+            "options": [
+                {
+                    "id": "456",
+                    "channel_ids": ["123"],
+                    "role_ids": ["123"],
+                    "emoji": {"name": "❤️"},
+                    "title": "title",
+                    "description": "description",
+                }
+            ],
+        }
+
+    def test_deserialize_guild_onboarding_prompt(
+        self, entity_factory_impl, guild_onboarding_prompt_payload: typing.MutableMapping[str, typing.Any]
+    ):
+        prompt = entity_factory_impl._deserialize_guild_onboarding_prompt(guild_onboarding_prompt_payload)
+        assert isinstance(prompt, guild_models.GuildOnboardingPrompt)
+        assert prompt.title == "test"
+        assert prompt.id == 123
+        assert prompt.type == guild_models.GuildOnboardingPromptType.MULTIPLE_CHOICE
+        assert prompt.in_onboarding == True
+        assert prompt.required == True
+        assert prompt.single_select == True
+        assert prompt.options == [
+            guild_models.GuildOnboardingPromptOption(
+                id=snowflakes.Snowflake("456"),
+                channel_ids=[snowflakes.Snowflake("123")],
+                role_ids=[snowflakes.Snowflake("123")],
+                emoji=emoji_models.UnicodeEmoji("❤️"),
+                title="title",
+                description="description",
+            )
+        ]
+
+    def test_deserialize_guild_onboarding(
+        self, entity_factory_impl, guild_onboarding_prompt_payload: typing.MutableMapping[str, typing.Any]
+    ):
+        payload = {
+            "default_channel_ids": ["123"],
+            "prompts": [guild_onboarding_prompt_payload],
+            "guild_id": "187",
+            "enabled": True,
+            "mode": guild_models.GuildOnboardingMode.ONBOARDING_DEFAULT.value,
+        }
+        onboarding = entity_factory_impl.deserialize_guild_onboarding(payload)
+        assert onboarding.guild_id == snowflakes.Snowflake("187")
+        assert onboarding.enabled == True
+        assert onboarding.default_channel_ids == [snowflakes.Snowflake("123")]
+        assert onboarding.mode == guild_models.GuildOnboardingMode.ONBOARDING_DEFAULT
+        assert len(onboarding.prompts) == 1
+        assert isinstance(onboarding.prompts[0], guild_models.GuildOnboardingPrompt)
+
     def test_deserialize_member(self, entity_factory_impl, mock_app, member_payload, user_payload):
         member_payload = {**member_payload, "guild_id": "76543325"}
         member = entity_factory_impl.deserialize_member(member_payload)
diff --git a/tests/hikari/impl/test_rest.py b/tests/hikari/impl/test_rest.py
index 7b55fa45..729556a9 100644
--- a/tests/hikari/impl/test_rest.py
+++ b/tests/hikari/impl/test_rest.py
@@ -5967,6 +5967,68 @@ async def test_edit_welcome_screen_without_optional_kwargs(self, rest_client):
             rest_client._request.return_value
         )
 
+    async def test_fetch_guild_onboarding(self, rest_client):
+        GUILD = 123
+        rest_client._request = mock.AsyncMock(return_value={"haha": "funny"})
+        expected_route = routes.GET_GUILD_ONBOARDING.compile(guild=GUILD)
+        result = await rest_client.fetch_guild_onboarding(GUILD)
+        assert result is rest_client._entity_factory.deserialize_guild_onboarding.return_value
+        rest_client._request.assert_awaited_once_with(expected_route)
+        rest_client._entity_factory.deserialize_guild_onboarding.assert_called_once_with(
+            rest_client._request.return_value
+        )
+
+    async def test_edit_guild_onboarding(self, rest_client):
+        GUILD = 123
+        expected_route = routes.PUT_GUILD_ONBOARDING.compile(guild=GUILD)
+        rest_client._request = mock.AsyncMock(return_value={"haha": "funny"})
+        result = await rest_client.edit_guild_onboarding(
+            guild=GUILD,
+            default_channel_ids=[456],
+            enabled=True,
+            mode=guilds.GuildOnboardingMode.ONBOARDING_DEFAULT,
+            prompts=[
+                special_endpoints.GuildOnboardingPromptBuilder(
+                    title="Test Title", single_select=True, in_onboarding=True, required=True
+                ),
+                special_endpoints.GuildOnboardingPromptBuilder(
+                    title="Test Title", single_select=True, in_onboarding=True, required=True
+                ).set_id(187),
+            ],
+            reason="test reason",
+        )
+        assert result is rest_client._entity_factory.deserialize_guild_onboarding.return_value
+        rest_client._request.assert_awaited_once_with(
+            expected_route,
+            json={
+                "enabled": True,
+                "default_channel_ids": ["456"],
+                "mode": guilds.GuildOnboardingMode.ONBOARDING_DEFAULT.value,
+                "prompts": [
+                    {
+                        "id": "0",
+                        "title": "Test Title",
+                        "single_select": True,
+                        "in_onboarding": True,
+                        "required": True,
+                        "options": [],
+                    },
+                    {
+                        "id": "187",
+                        "title": "Test Title",
+                        "single_select": True,
+                        "in_onboarding": True,
+                        "required": True,
+                        "options": [],
+                    },
+                ],
+            },
+            reason="test reason",
+        )
+        rest_client._entity_factory.deserialize_guild_onboarding.assert_called_once_with(
+            rest_client._request.return_value
+        )
+
     async def test_fetch_vanity_url(self, rest_client):
         vanity_url = StubModel(789)
         expected_route = routes.GET_GUILD_VANITY_URL.compile(guild=123)
diff --git a/tests/hikari/impl/test_special_endpoints.py b/tests/hikari/impl/test_special_endpoints.py
index 654fbb95..68cece7e 100644
--- a/tests/hikari/impl/test_special_endpoints.py
+++ b/tests/hikari/impl/test_special_endpoints.py
@@ -761,6 +761,163 @@ def test_build(self):
         assert choice.build() == {"name": "atlantic", "value": "slow"}
 
 
+class TestGuildOnboardingPromptOptionBuilder:
+    DEFAULT_ROLE_ID = 123
+    DEFAULT_CHANNEL_ID = 456
+    DEFAULT_EMOJI_ID = 789
+
+    @pytest.fixture
+    def prompt_option(self) -> special_endpoints_api.GuildOnboardingPromptOptionBuilder:
+        return special_endpoints.GuildOnboardingPromptOptionBuilder(
+            title="Title",
+            description="Description",
+            role_ids=[self.DEFAULT_ROLE_ID],
+            channel_ids=[self.DEFAULT_CHANNEL_ID],
+            emoji=self.DEFAULT_EMOJI_ID,
+        )
+
+    def test_default_prompt_option(
+        self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder
+    ) -> None:
+        assert prompt_option.title == "Title"
+        assert prompt_option.description == "Description"
+        assert prompt_option.channel_ids == [self.DEFAULT_CHANNEL_ID]
+        assert prompt_option.role_ids == [self.DEFAULT_ROLE_ID]
+        assert prompt_option.emoji == self.DEFAULT_EMOJI_ID
+
+    def test_set_title(self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder) -> None:
+        prompt_option.set_title("1")
+        assert prompt_option.title == "1"
+
+    def test_set_description(self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder) -> None:
+        prompt_option.set_description("1")
+        assert prompt_option.description == "1"
+
+    def test_set_channel_ids(self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder) -> None:
+        prompt_option.set_channel_ids([187])
+        assert prompt_option.channel_ids == [187]
+
+    def test_set_role_ids(self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder) -> None:
+        prompt_option.set_role_ids([187])
+        assert prompt_option.role_ids == [187]
+
+    def test_set_emoji(self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder) -> None:
+        prompt_option.set_emoji(187)
+        assert prompt_option.emoji == 187
+
+    def test_build(self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder) -> None:
+        assert prompt_option.build() == {
+            "title": "Title",
+            "description": "Description",
+            "channel_ids": ["456"],
+            "role_ids": ["123"],
+            "emoji_id": "789",
+        }
+
+    def test_build_no_emoji(self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder) -> None:
+        body = prompt_option.set_emoji(undefined.UNDEFINED).build()
+
+        assert body == {"title": "Title", "description": "Description", "channel_ids": ["456"], "role_ids": ["123"]}
+
+    def test_build_unicode_emoji(self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder) -> None:
+        body = prompt_option.set_emoji("❤️").build()
+
+        assert body == {
+            "title": "Title",
+            "description": "Description",
+            "channel_ids": ["456"],
+            "role_ids": ["123"],
+            "emoji_name": "❤️",
+        }
+
+
+class TestGuildOnboardingPromptBuilder:
+    @pytest.fixture
+    def prompt_option(self) -> special_endpoints_api.GuildOnboardingPromptOptionBuilder:
+        return special_endpoints.GuildOnboardingPromptOptionBuilder(title="Title")
+
+    @pytest.fixture
+    def prompt(
+        self, prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder
+    ) -> special_endpoints_api.GuildOnboardingPromptBuilder:
+        return special_endpoints.GuildOnboardingPromptBuilder(
+            title="Title", in_onboarding=True, single_select=True, required=True, options=[prompt_option]
+        )
+
+    def test_prompt(
+        self,
+        prompt: special_endpoints_api.GuildOnboardingPromptBuilder,
+        prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder,
+    ) -> None:
+        assert prompt.id == undefined.UNDEFINED
+        assert prompt.title == "Title"
+        assert prompt.in_onboarding == True
+        assert prompt.single_select == True
+        assert prompt.required == True
+        assert prompt.options == [prompt_option]
+
+    def test_set_id(self, prompt: special_endpoints_api.GuildOnboardingPromptBuilder) -> None:
+        prompt.set_id(1)
+        assert prompt.id == 1
+
+    def test_set_title(self, prompt: special_endpoints_api.GuildOnboardingPromptBuilder) -> None:
+        prompt.set_title("1")
+        assert prompt.title == "1"
+
+    def test_set_in_onboarding(self, prompt: special_endpoints_api.GuildOnboardingPromptBuilder) -> None:
+        prompt.set_in_onboarding(False)
+        assert prompt.in_onboarding == False
+
+    def test_set_single_select(self, prompt: special_endpoints_api.GuildOnboardingPromptBuilder) -> None:
+        prompt.set_single_select(False)
+        assert prompt.single_select == False
+
+    def test_set_required(self, prompt: special_endpoints_api.GuildOnboardingPromptBuilder) -> None:
+        prompt.set_required(False)
+        assert prompt.required == False
+
+    def test_set_options(self, prompt: special_endpoints_api.GuildOnboardingPromptBuilder) -> None:
+        prompt.set_options([])
+        assert prompt.options == []
+
+    def test_set_options_tuple(
+        self,
+        prompt: special_endpoints_api.GuildOnboardingPromptBuilder,
+        prompt_option: special_endpoints_api.GuildOnboardingPromptOptionBuilder,
+    ) -> None:
+        prompt.set_options((prompt_option,))
+        assert prompt.options == [prompt_option]
+
+    def test_add_option(self, prompt: special_endpoints_api.GuildOnboardingPromptBuilder) -> None:
+        prompt.set_options([]).add_option(title="Test Title", description="Test Description", role_ids=[123], emoji=789)
+        assert prompt.options == [
+            special_endpoints.GuildOnboardingPromptOptionBuilder(
+                title="Test Title", description="Test Description", role_ids=[123], channel_ids=[], emoji=789
+            )
+        ]
+
+    def test_build(self, prompt: special_endpoints_api.GuildOnboardingPromptBuilder) -> None:
+        assert prompt.build() == {
+            "in_onboarding": True,
+            "options": [{"channel_ids": [], "role_ids": [], "title": "Title"}],
+            "required": True,
+            "single_select": True,
+            "title": "Title",
+        }
+
+    def test_build_id(self, prompt: special_endpoints_api.GuildOnboardingPromptBuilder) -> None:
+        body = prompt.set_id(1).build()
+
+        assert body == {
+            "in_onboarding": True,
+            "options": [{"channel_ids": [], "role_ids": [], "title": "Title"}],
+            "required": True,
+            "single_select": True,
+            "title": "Title",
+            "id": "1",
+        }
+
+
 class TestInteractionDeferredBuilder:
     def test_type_property(self):
         builder = special_endpoints.InteractionDeferredBuilder(5)
@@ -1240,16 +1397,21 @@ async def test_create_with_guild(self):
 
 
 @pytest.mark.parametrize("emoji", ["UNICORN", emojis.UnicodeEmoji("UNICORN")])
-def test__build_emoji_with_unicode_emoji(emoji):
+def test__build_emoji_with_unicode_emoji(emoji: str | emojis.UnicodeEmoji):
     result = special_endpoints._build_emoji(emoji)
 
     assert result == (undefined.UNDEFINED, "UNICORN")
 
 
 @pytest.mark.parametrize(
-    "emoji", [snowflakes.Snowflake(54123123), 54123123, emojis.CustomEmoji(id=54123123, name=None, is_animated=None)]
+    "emoji",
+    [
+        snowflakes.Snowflake(54123123),
+        54123123,
+        emojis.CustomEmoji(id=snowflakes.Snowflake(54123123), name="test", is_animated=True),
+    ],
 )
-def test__build_emoji_with_custom_emoji(emoji):
+def test__build_emoji_with_custom_emoji(emoji: int | str | emojis.CustomEmoji):
     result = special_endpoints._build_emoji(emoji)
 
     assert result == ("54123123", undefined.UNDEFINED)