diff --git a/src/Api/Billing/Public/Controllers/OrganizationController.cs b/src/Api/Billing/Public/Controllers/OrganizationController.cs
new file mode 100644
index 0000000000..294165a7e8
--- /dev/null
+++ b/src/Api/Billing/Public/Controllers/OrganizationController.cs
@@ -0,0 +1,81 @@
+using System.Net;
+using Bit.Api.Models.Public.Response;
+using Bit.Core.Context;
+using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Bit.Core.Utilities;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using OrganizationSubscriptionUpdateRequestModel = Bit.Api.Billing.Public.Models.OrganizationSubscriptionUpdateRequestModel;
+
+namespace Bit.Api.Billing.Public.Controllers;
+
+[Route("public/organization")]
+[Authorize("Organization")]
+public class OrganizationController : Controller
+{
+ private readonly IOrganizationService _organizationService;
+ private readonly ICurrentContext _currentContext;
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
+
+ public OrganizationController(
+ IOrganizationService organizationService,
+ ICurrentContext currentContext,
+ IOrganizationRepository organizationRepository,
+ IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
+ {
+ _organizationService = organizationService;
+ _currentContext = currentContext;
+ _organizationRepository = organizationRepository;
+ _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
+ }
+
+ ///
+ /// Update the organization's current subscription for Password Manager and/or Secrets Manager.
+ ///
+ /// The request model containing the updated subscription information.
+ [HttpPut("subscription")]
+ [SelfHosted(NotSelfHostedOnly = true)]
+ [ProducesResponseType((int)HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
+ [ProducesResponseType((int)HttpStatusCode.NotFound)]
+ public async Task PostSubscriptionAsync([FromBody] OrganizationSubscriptionUpdateRequestModel model)
+ {
+
+ await UpdatePasswordManagerAsync(model, _currentContext.OrganizationId.Value);
+
+ await UpdateSecretsManagerAsync(model, _currentContext.OrganizationId.Value);
+
+ return new OkResult();
+ }
+
+ private async Task UpdatePasswordManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId)
+ {
+ if (model.PasswordManager != null)
+ {
+ var organization = await _organizationRepository.GetByIdAsync(organizationId);
+
+ model.PasswordManager.ToPasswordManagerSubscriptionUpdate(organization);
+ await _organizationService.UpdateSubscription(organization.Id, (int)model.PasswordManager.Seats,
+ model.PasswordManager.MaxAutoScaleSeats);
+ if (model.PasswordManager.Storage.HasValue)
+ {
+ await _organizationService.AdjustStorageAsync(organization.Id, (short)model.PasswordManager.Storage);
+ }
+ }
+ }
+
+ private async Task UpdateSecretsManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId)
+ {
+ if (model.SecretsManager != null)
+ {
+ var organization =
+ await _organizationRepository.GetByIdAsync(organizationId);
+
+ var organizationUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization);
+ await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate);
+ }
+ }
+}
diff --git a/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs b/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs
new file mode 100644
index 0000000000..781ad3ca53
--- /dev/null
+++ b/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs
@@ -0,0 +1,138 @@
+using System.ComponentModel.DataAnnotations;
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.Models.Business;
+
+namespace Bit.Api.Billing.Public.Models;
+
+public class OrganizationSubscriptionUpdateRequestModel : IValidatableObject
+{
+ public PasswordManagerSubscriptionUpdateModel PasswordManager { get; set; }
+ public SecretsManagerSubscriptionUpdateModel SecretsManager { get; set; }
+
+ public IEnumerable Validate(ValidationContext validationContext)
+ {
+ if (PasswordManager == null && SecretsManager == null)
+ {
+ yield return new ValidationResult("At least one of PasswordManager or SecretsManager must be provided.");
+ }
+
+ yield return ValidationResult.Success;
+ }
+}
+
+public class PasswordManagerSubscriptionUpdateModel
+{
+ public int? Seats { get; set; }
+ public int? Storage { get; set; }
+ private int? _maxAutoScaleSeats;
+ public int? MaxAutoScaleSeats
+ {
+ get { return _maxAutoScaleSeats; }
+ set { _maxAutoScaleSeats = value < 0 ? null : value; }
+ }
+
+ public virtual void ToPasswordManagerSubscriptionUpdate(Organization organization)
+ {
+ UpdateMaxAutoScaleSeats(organization);
+
+ UpdateSeats(organization);
+
+ UpdateStorage(organization);
+ }
+
+ private void UpdateMaxAutoScaleSeats(Organization organization)
+ {
+ MaxAutoScaleSeats ??= organization.MaxAutoscaleSeats;
+ }
+
+ private void UpdateSeats(Organization organization)
+ {
+ if (Seats is > 0)
+ {
+ if (organization.Seats.HasValue)
+ {
+ Seats = Seats.Value - organization.Seats.Value;
+ }
+ }
+ else
+ {
+ Seats = 0;
+ }
+ }
+
+ private void UpdateStorage(Organization organization)
+ {
+ if (Storage is > 0)
+ {
+ if (organization.MaxStorageGb.HasValue)
+ {
+ Storage = (short?)(Storage - organization.MaxStorageGb.Value);
+ }
+ }
+ else
+ {
+ Storage = null;
+ }
+ }
+}
+
+public class SecretsManagerSubscriptionUpdateModel
+{
+ public int? Seats { get; set; }
+ private int? _maxAutoScaleSeats;
+ public int? MaxAutoScaleSeats
+ {
+ get { return _maxAutoScaleSeats; }
+ set { _maxAutoScaleSeats = value < 0 ? null : value; }
+ }
+ public int? ServiceAccounts { get; set; }
+ private int? _maxAutoScaleServiceAccounts;
+ public int? MaxAutoScaleServiceAccounts
+ {
+ get { return _maxAutoScaleServiceAccounts; }
+ set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; }
+ }
+
+ public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization)
+ {
+ var update = UpdateUpdateMaxAutoScale(organization);
+ UpdateSeats(organization, update);
+ UpdateServiceAccounts(organization, update);
+ return update;
+ }
+
+ private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization)
+ {
+ var update = new SecretsManagerSubscriptionUpdate(organization, false)
+ {
+ MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats,
+ MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts
+ };
+ return update;
+ }
+
+ private void UpdateSeats(Organization organization, SecretsManagerSubscriptionUpdate update)
+ {
+ if (Seats is > 0)
+ {
+ if (organization.SmSeats.HasValue)
+ {
+ Seats = Seats.Value - organization.SmSeats.Value;
+
+ }
+ update.AdjustSeats(Seats.Value);
+ }
+ }
+
+ private void UpdateServiceAccounts(Organization organization, SecretsManagerSubscriptionUpdate update)
+ {
+ if (ServiceAccounts is > 0)
+ {
+ if (organization.SmServiceAccounts.HasValue)
+ {
+ ServiceAccounts = ServiceAccounts.Value - organization.SmServiceAccounts.Value;
+ }
+ update.AdjustServiceAccounts(ServiceAccounts.Value);
+ }
+ }
+}