diff --git a/packages/api-v4/.changeset/pr-9390-added-1689111404354.md b/packages/api-v4/.changeset/pr-9390-added-1689111404354.md
new file mode 100644
index 00000000000..5f0c1659af1
--- /dev/null
+++ b/packages/api-v4/.changeset/pr-9390-added-1689111404354.md
@@ -0,0 +1,5 @@
+---
+"@linode/api-v4": Added
+---
+
+Endpoints for VPC Subnets ([#9390](https://github.com/linode/manager/pull/9390))
diff --git a/packages/api-v4/src/vpcs/types.ts b/packages/api-v4/src/vpcs/types.ts
index ca4bc1dcf5f..789c4b39776 100644
--- a/packages/api-v4/src/vpcs/types.ts
+++ b/packages/api-v4/src/vpcs/types.ts
@@ -12,7 +12,7 @@ export interface CreateVPCPayload {
   label: string;
   description?: string;
   region: string;
-  subnets?: SubnetPostObject[];
+  subnets?: CreateSubnetPayload[];
 }
 
 export interface UpdateVPCPayload {
@@ -20,15 +20,19 @@ export interface UpdateVPCPayload {
   description?: string;
 }
 
-export interface SubnetPostObject {
+export interface CreateSubnetPayload {
   label: string;
-  ipv4: string;
-  ipv6: string;
+  ipv4?: string;
+  ipv6?: string;
 }
 
-export interface Subnet extends SubnetPostObject {
+export interface Subnet extends CreateSubnetPayload {
   id: number;
   linodes: number[];
   created: string;
   updated: string;
 }
+
+export interface ModifySubnetPayload {
+  label: string;
+}
diff --git a/packages/api-v4/src/vpcs/vpcs.ts b/packages/api-v4/src/vpcs/vpcs.ts
index 371bdd1e156..2fefb1471f7 100644
--- a/packages/api-v4/src/vpcs/vpcs.ts
+++ b/packages/api-v4/src/vpcs/vpcs.ts
@@ -1,5 +1,7 @@
 import {
+  createSubnetSchema,
   createVPCSchema,
+  modifySubnetSchema,
   updateVPCSchema,
 } from '@linode/validation/lib/vpcs.schema';
 import { BETA_API_ROOT as API_ROOT } from '../constants';
@@ -11,7 +13,14 @@ import Request, {
   setXFilter,
 } from '../request';
 import { Filter, ResourcePage as Page, Params } from '../types';
-import { CreateVPCPayload, UpdateVPCPayload, VPC } from './types';
+import {
+  CreateSubnetPayload,
+  CreateVPCPayload,
+  ModifySubnetPayload,
+  Subnet,
+  UpdateVPCPayload,
+  VPC,
+} from './types';
 
 // VPC methods
 /**
@@ -79,3 +88,82 @@ export const deleteVPC = (vpcID: number) =>
   );
 
 // Subnet methods
+/**
+ * getSubnets
+ *
+ * Return a paginated list of subnets under a specified VPC.
+ *
+ */
+export const getSubnets = (vpcID: number, params?: Params, filter?: Filter) =>
+  Request<Page<Subnet>>(
+    setURL(`${API_ROOT}/vpcs/${encodeURIComponent(vpcID)}/subnets`),
+    setMethod('GET'),
+    setParams(params),
+    setXFilter(filter)
+  );
+
+/**
+ * getSubnet
+ *
+ * Return details for a single specified subnet under a specified VPC.
+ *
+ */
+export const getSubnet = (vpcID: number, subnetID: number) =>
+  Request<Subnet>(
+    setURL(
+      `${API_ROOT}/vpcs/${encodeURIComponent(
+        vpcID
+      )}/subnets/${encodeURIComponent(subnetID)}`
+    ),
+    setMethod('GET')
+  );
+
+/**
+ * createSubnet
+ *
+ * Create a new subnet under an existing VPC.
+ *
+ */
+export const createSubnet = (vpcID: number, data: CreateSubnetPayload) =>
+  Request<Subnet>(
+    setURL(`${API_ROOT}/vpcs/${encodeURIComponent(vpcID)}/subnets`),
+    setMethod('POST'),
+    setData(data, createSubnetSchema)
+  );
+
+/**
+ * modifySubnet
+ *
+ * Modify an existing subnet.
+ *
+ */
+export const modifySubnet = (
+  vpcID: number,
+  subnetID: number,
+  data: ModifySubnetPayload
+) =>
+  Request<Subnet>(
+    setURL(
+      `${API_ROOT}/vpcs/${encodeURIComponent(
+        vpcID
+      )}/subnets/${encodeURIComponent(subnetID)}`
+    ),
+    setMethod('PUT'),
+    setData(data, modifySubnetSchema)
+  );
+
+/**
+ * deleteSubnet
+ *
+ * Delete a single specified subnet belonging to a specified VPC.
+ *
+ */
+export const deleteSubnet = (vpcID: number, subnetID: number) =>
+  Request<{}>(
+    setURL(
+      `${API_ROOT}/vpcs/${encodeURIComponent(
+        vpcID
+      )}/subnets/${encodeURIComponent(subnetID)}`
+    ),
+    setMethod('DELETE')
+  );
diff --git a/packages/manager/.changeset/pr-9390-tech-stories-1689111377085.md b/packages/manager/.changeset/pr-9390-tech-stories-1689111377085.md
new file mode 100644
index 00000000000..53dc591d4cf
--- /dev/null
+++ b/packages/manager/.changeset/pr-9390-tech-stories-1689111377085.md
@@ -0,0 +1,5 @@
+---
+"@linode/manager": Tech Stories
+---
+
+React Query queries for VPC Subnets ([#9390](https://github.com/linode/manager/pull/9390))
diff --git a/packages/manager/src/queries/vpcs.ts b/packages/manager/src/queries/vpcs.ts
index 7e1c8a51c5d..e61f1718bbd 100644
--- a/packages/manager/src/queries/vpcs.ts
+++ b/packages/manager/src/queries/vpcs.ts
@@ -1,11 +1,19 @@
 import {
+  CreateSubnetPayload,
   CreateVPCPayload,
+  ModifySubnetPayload,
+  Subnet,
   UpdateVPCPayload,
   VPC,
+  createSubnet,
   createVPC,
+  deleteSubnet,
   deleteVPC,
+  getSubnet,
+  getSubnets,
   getVPC,
   getVPCs,
+  modifySubnet,
   updateVPC,
 } from '@linode/api-v4';
 import {
@@ -16,26 +24,28 @@ import {
 } from '@linode/api-v4/lib/types';
 import { useMutation, useQuery, useQueryClient } from 'react-query';
 
-export const queryKey = 'vpcs';
+export const vpcQueryKey = 'vpcs';
+export const subnetQueryKey = 'subnets';
 
+// VPC queries
 export const useVPCsQuery = (params: Params, filter: Filter) => {
   return useQuery<ResourcePage<VPC>, APIError[]>(
-    [queryKey, 'paginated', params, filter],
+    [vpcQueryKey, 'paginated', params, filter],
     () => getVPCs(params, filter),
     { keepPreviousData: true }
   );
 };
 
 export const useVPCQuery = (id: number) => {
-  return useQuery<VPC, APIError[]>([queryKey, 'vpc', id], () => getVPC(id));
+  return useQuery<VPC, APIError[]>([vpcQueryKey, 'vpc', id], () => getVPC(id));
 };
 
 export const useCreateVPCMutation = () => {
   const queryClient = useQueryClient();
   return useMutation<VPC, APIError[], CreateVPCPayload>(createVPC, {
     onSuccess: (VPC) => {
-      queryClient.invalidateQueries([queryKey, 'paginated']);
-      queryClient.setQueryData([queryKey, 'vpc', VPC.id], VPC);
+      queryClient.invalidateQueries([vpcQueryKey, 'paginated']);
+      queryClient.setQueryData([vpcQueryKey, 'vpc', VPC.id], VPC);
     },
   });
 };
@@ -46,8 +56,8 @@ export const useUpdateVPCMutation = (id: number) => {
     (data) => updateVPC(id, data),
     {
       onSuccess: (VPC) => {
-        queryClient.invalidateQueries([queryKey, 'paginated']);
-        queryClient.setQueryData<VPC>([queryKey, 'vpc', VPC.id], VPC);
+        queryClient.invalidateQueries([vpcQueryKey, 'paginated']);
+        queryClient.setQueryData<VPC>([vpcQueryKey, 'vpc', VPC.id], VPC);
       },
     }
   );
@@ -57,8 +67,101 @@ export const useDeleteVPCMutation = (id: number) => {
   const queryClient = useQueryClient();
   return useMutation<{}, APIError[]>(() => deleteVPC(id), {
     onSuccess: () => {
-      queryClient.invalidateQueries([queryKey, 'paginated']);
-      queryClient.removeQueries([queryKey, 'vpc', id]);
+      queryClient.invalidateQueries([vpcQueryKey, 'paginated']);
+      queryClient.removeQueries([vpcQueryKey, 'vpc', id]);
+    },
+  });
+};
+
+// Subnet queries
+export const useSubnetsQuery = (
+  vpcID: number,
+  params: Params,
+  filter: Filter
+) => {
+  return useQuery<ResourcePage<Subnet>, APIError[]>(
+    [vpcQueryKey, 'vpc', vpcID, subnetQueryKey, 'paginated', params, filter],
+    () => getSubnets(vpcID, params, filter),
+    { keepPreviousData: true }
+  );
+};
+
+export const useSubnetQuery = (vpcID: number, subnetID: number) => {
+  return useQuery<Subnet, APIError[]>(
+    [vpcQueryKey, 'vpc', vpcID, subnetQueryKey, 'subnet', subnetID],
+    () => getSubnet(vpcID, subnetID)
+  );
+};
+
+export const useCreateSubnetMutation = (vpcID: number) => {
+  const queryClient = useQueryClient();
+  return useMutation<Subnet, APIError[], CreateSubnetPayload>(
+    (data) => createSubnet(vpcID, data),
+    {
+      onSuccess: () => {
+        // New subnet created --> refresh the paginated and individual VPC queries, plus the /subnets VPC query
+        queryClient.invalidateQueries([vpcQueryKey, 'paginated']);
+        queryClient.invalidateQueries([vpcQueryKey, 'vpc', vpcID]);
+        queryClient.invalidateQueries([
+          vpcQueryKey,
+          'vpc',
+          vpcID,
+          subnetQueryKey,
+        ]);
+      },
+    }
+  );
+};
+
+export const useUpdateSubnetMutation = (vpcID: number, subnetID: number) => {
+  const queryClient = useQueryClient();
+  return useMutation<Subnet, APIError[], ModifySubnetPayload>(
+    (data) => modifySubnet(vpcID, subnetID, data),
+    {
+      onSuccess: () => {
+        // Subnet modified --> refresh the paginated and individual VPC queries, plus the paginated & individual subnet queries
+        queryClient.invalidateQueries([vpcQueryKey, 'paginated']);
+        queryClient.invalidateQueries([vpcQueryKey, 'vpc', vpcID]);
+        queryClient.invalidateQueries([
+          vpcQueryKey,
+          'vpc',
+          vpcID,
+          subnetQueryKey,
+        ]);
+        queryClient.invalidateQueries([
+          vpcQueryKey,
+          'vpc',
+          vpcID,
+          subnetQueryKey,
+          'subnet',
+          subnetID,
+        ]);
+      },
+    }
+  );
+};
+
+export const useDeleteSubnetMutation = (vpcID: number, subnetID: number) => {
+  const queryClient = useQueryClient();
+  return useMutation<{}, APIError[]>(() => deleteSubnet(vpcID, subnetID), {
+    onSuccess: () => {
+      // Subnet deleted --> refresh the paginated and individual VPC queries, plus the paginated subnet query, & clear the individual subnet query
+      queryClient.invalidateQueries([vpcQueryKey, 'paginated']);
+      queryClient.invalidateQueries([vpcQueryKey, 'vpc', vpcID]);
+      queryClient.invalidateQueries([
+        vpcQueryKey,
+        'vpc',
+        vpcID,
+        subnetQueryKey,
+      ]);
+      queryClient.removeQueries([
+        vpcQueryKey,
+        'vpc',
+        vpcID,
+        subnetQueryKey,
+        'subnet',
+        subnetID,
+      ]);
     },
   });
 };
diff --git a/packages/validation/.changeset/pr-9390-added-1689111447323.md b/packages/validation/.changeset/pr-9390-added-1689111447323.md
new file mode 100644
index 00000000000..19d6a550b3a
--- /dev/null
+++ b/packages/validation/.changeset/pr-9390-added-1689111447323.md
@@ -0,0 +1,5 @@
+---
+"@linode/validation": Added
+---
+
+Validation for VPC subnet creation and modifications ([#9390](https://github.com/linode/manager/pull/9390))
diff --git a/packages/validation/src/vpcs.schema.ts b/packages/validation/src/vpcs.schema.ts
index cc0d2180c11..3d9e5b98a4e 100644
--- a/packages/validation/src/vpcs.schema.ts
+++ b/packages/validation/src/vpcs.schema.ts
@@ -1,39 +1,93 @@
+import ipaddr from 'ipaddr.js';
 import { array, object, string } from 'yup';
 
 const LABEL_MESSAGE = 'VPC label must be between 1 and 64 characters.';
+const LABEL_REQUIRED = 'Label is required';
+const LABEL_REQUIREMENTS =
+  'Must include only ASCII letters, numbers, and dashes';
+
+const labelTestDetails = {
+  testName: 'no two dashes in a row',
+  testMessage: 'Must not contain two dashes in a row',
+};
+
+const validateIP = (value?: string): boolean => {
+  if (!value) {
+    return false;
+  }
+
+  try {
+    const addr = ipaddr.parse(value);
+    const type = addr.kind();
+
+    if (type === 'ipv4') {
+      ipaddr.IPv4.isValid(value);
+      ipaddr.IPv4.parseCIDR(value);
+    } else if (type === 'ipv6') {
+      ipaddr.IPv6.parseCIDR(value);
+    } else {
+      return false;
+    }
+
+    return true;
+  } catch (err) {
+    return false;
+  }
+};
+
+const labelValidation = string()
+  .test(
+    labelTestDetails.testName,
+    labelTestDetails.testMessage,
+    (value) => !value?.includes('--')
+  )
+  .min(1, LABEL_MESSAGE)
+  .max(64, LABEL_MESSAGE)
+  .matches(/[a-zA-Z0-9-]+/, LABEL_REQUIREMENTS);
 
 export const createVPCSchema = object({
-  label: string()
-    .test(
-      'no two dashes in a row',
-      'Must not contain two dashes in a row',
-      (value) => !value?.includes('--')
-    )
-    .required('Label is required')
-    .min(1, LABEL_MESSAGE)
-    .max(64, LABEL_MESSAGE)
-    .matches(
-      /[a-zA-Z0-9-]+/,
-      'Must include only ASCII letters, numbers, and dashes'
-    ),
+  label: labelValidation.required(LABEL_REQUIRED),
   description: string(),
   region: string().required('Region is required'),
   subnets: array().of(object()),
 });
 
 export const updateVPCSchema = object({
-  label: string()
-    .notRequired()
-    .test(
-      'no two dashes in a row',
-      'Must not contain two dashes in a row',
-      (value) => !value?.includes('--')
-    )
-    .min(1, LABEL_MESSAGE)
-    .max(64, LABEL_MESSAGE)
-    .matches(
-      /[a-zA-Z0-9-]+/,
-      'Must include only ASCII letters, numbers, and dashes'
-    ),
+  label: labelValidation.notRequired(),
   description: string().notRequired(),
 });
+
+export const createSubnetSchema = object().shape(
+  {
+    label: labelValidation.required(LABEL_REQUIRED),
+    ipv4: string()
+      .test({
+        name: 'cidr',
+        message: 'The IPv4 range must be in CIDR format',
+        test: validateIP,
+      })
+      .notRequired()
+      .ensure()
+      .when('ipv6', {
+        is: '',
+        then: string().required(),
+      }),
+    ipv6: string()
+      .test({
+        name: 'cidr mask',
+        message: 'Must be the subnet mask of the IP, e.g. /24',
+        test: validateIP,
+      })
+      .notRequired()
+      .ensure()
+      .when('ipv4', {
+        is: '',
+        then: string().required(),
+      }),
+  },
+  [['ipv6', 'ipv4']]
+);
+
+export const modifySubnetSchema = object({
+  label: labelValidation.required(LABEL_REQUIRED),
+});