diff --git a/app/javascript/mastodon/api_types/media_attachments.ts b/app/javascript/mastodon/api_types/media_attachments.ts
new file mode 100644
index 0000000000..fc027ccd2a
--- /dev/null
+++ b/app/javascript/mastodon/api_types/media_attachments.ts
@@ -0,0 +1,22 @@
+// See app/serializers/rest/media_attachment_serializer.rb
+
+export type MediaAttachmentType =
+  | 'image'
+  | 'gifv'
+  | 'video'
+  | 'unknown'
+  | 'audio';
+
+export interface ApiMediaAttachmentJSON {
+  id: string;
+  type: MediaAttachmentType;
+  url: string;
+  preview_url: string;
+  remoteUrl: string;
+  preview_remote_url: string;
+  text_url: string;
+  // TODO: how to define this?
+  meta: unknown;
+  description?: string;
+  blurhash: string;
+}
diff --git a/app/javascript/mastodon/api_types/polls.ts b/app/javascript/mastodon/api_types/polls.ts
new file mode 100644
index 0000000000..8181f7b813
--- /dev/null
+++ b/app/javascript/mastodon/api_types/polls.ts
@@ -0,0 +1,23 @@
+import type { ApiCustomEmojiJSON } from './custom_emoji';
+
+// See app/serializers/rest/poll_serializer.rb
+
+export interface ApiPollOptionJSON {
+  title: string;
+  votes_count: number;
+}
+
+export interface ApiPollJSON {
+  id: string;
+  expires_at: string;
+  expired: boolean;
+  multiple: boolean;
+  votes_count: number;
+  voters_count: number;
+
+  options: ApiPollOptionJSON[];
+  emojis: ApiCustomEmojiJSON[];
+
+  voted: boolean;
+  own_votes: number[];
+}
diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts
new file mode 100644
index 0000000000..c7dd33b5da
--- /dev/null
+++ b/app/javascript/mastodon/api_types/statuses.ts
@@ -0,0 +1,91 @@
+// See app/serializers/rest/status_serializer.rb
+
+import type { ApiAccountJSON } from './accounts';
+import type { ApiCustomEmojiJSON } from './custom_emoji';
+import type { ApiMediaAttachmentJSON } from './media_attachments';
+import type { ApiPollJSON } from './polls';
+
+// See app/modals/status.rb
+export type StatusVisibility =
+  | 'public'
+  | 'unlisted'
+  | 'private'
+  // | 'limited' // This is never exposed to the API (they become `private`)
+  | 'direct';
+
+export interface ApiStatusApplicationJSON {
+  name: string;
+  website: string;
+}
+
+export interface ApiTagJSON {
+  name: string;
+  url: string;
+}
+
+export interface ApiMentionJSON {
+  id: string;
+  username: string;
+  url: string;
+  acct: string;
+}
+
+export interface ApiPreviewCardJSON {
+  url: string;
+  title: string;
+  description: string;
+  language: string;
+  type: string;
+  author_name: string;
+  author_url: string;
+  provider_name: string;
+  provider_url: string;
+  html: string;
+  width: number;
+  height: number;
+  image: string;
+  image_description: string;
+  embed_url: string;
+  blurhash: string;
+  published_at: string;
+}
+
+export interface ApiStatusJSON {
+  id: string;
+  created_at: string;
+  in_reply_to_id?: string;
+  in_reply_to_account_id?: string;
+  sensitive: boolean;
+  spoiler_text?: string;
+  visibility: StatusVisibility;
+  language: string;
+  uri: string;
+  url: string;
+  replies_count: number;
+  reblogs_count: number;
+  favorites_count: number;
+  edited_at?: string;
+
+  favorited?: boolean;
+  reblogged?: boolean;
+  muted?: boolean;
+  bookmarked?: boolean;
+  pinned?: boolean;
+
+  // filtered: FilterResult[]
+  filtered: unknown; // TODO
+  content?: string;
+  text?: string;
+
+  reblog?: ApiStatusJSON;
+  application?: ApiStatusApplicationJSON;
+  account: ApiAccountJSON;
+  media_attachments: ApiMediaAttachmentJSON[];
+  mentions: ApiMentionJSON[];
+
+  tags: ApiTagJSON[];
+  emojis: ApiCustomEmojiJSON[];
+
+  card?: ApiPreviewCardJSON;
+  poll?: ApiPollJSON;
+}
diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts
index 83e9f6b885..7907fc34f8 100644
--- a/app/javascript/mastodon/models/status.ts
+++ b/app/javascript/mastodon/models/status.ts
@@ -1,4 +1,4 @@
-export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct';
+export type { StatusVisibility } from 'mastodon/api_types/statuses';
 
 // Temporary until we type it correctly
 export type Status = Immutable.Map<string, unknown>;
diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb
index f27dda832a..90a2a97275 100644
--- a/app/serializers/rest/media_attachment_serializer.rb
+++ b/app/serializers/rest/media_attachment_serializer.rb
@@ -3,6 +3,8 @@
 class REST::MediaAttachmentSerializer < ActiveModel::Serializer
   include RoutingHelper
 
+  # Please update `app/javascript/mastodon/api_types/media_attachments.ts` when making changes to the attributes
+
   attributes :id, :type, :url, :preview_url,
              :remote_url, :preview_remote_url, :text_url, :meta,
              :description, :blurhash
diff --git a/app/serializers/rest/poll_serializer.rb b/app/serializers/rest/poll_serializer.rb
index df6ebd0d44..6e00060735 100644
--- a/app/serializers/rest/poll_serializer.rb
+++ b/app/serializers/rest/poll_serializer.rb
@@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class REST::PollSerializer < ActiveModel::Serializer
+  # Please update `app/javascript/mastodon/api_types/polls.ts` when making changes to the attributes
+
   attributes :id, :expires_at, :expired,
              :multiple, :votes_count, :voters_count
 
diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb
index d32621541a..e17e8c823e 100644
--- a/app/serializers/rest/status_serializer.rb
+++ b/app/serializers/rest/status_serializer.rb
@@ -3,6 +3,8 @@
 class REST::StatusSerializer < ActiveModel::Serializer
   include FormattingHelper
 
+  # Please update `app/javascript/mastodon/api_types/statuses.ts` when making changes to the attributes
+
   attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
              :sensitive, :spoiler_text, :visibility, :language,
              :uri, :url, :replies_count, :reblogs_count,