bitwarden/server · error · BadRequestException

Invalid Billing Sync Key

Error message

Invalid Billing Sync Key

What it means

Thrown (HTTP 400) on POST /organization-sponsorships/sync (Installation-authorized) when _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync returns false for the sponsoring organization. The self-hosted installation must present the correct billing sync key to sync sponsorship data.

Source

Thrown at src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs:222

        await _setUpSponsorshipCommand.SetUpSponsorshipAsync(
            sponsorship,
            await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId));

        _logger.LogInformation(
            "Sponsorship redemption succeeded: SponsorshipId={SponsorshipId}, SponsoredOrganizationId={SponsoredOrganizationId}",
            sponsorship.Id,
            model.SponsoredOrganizationId);
    }

    [Authorize("Installation")]
    [HttpPost("sync")]
    public async Task<OrganizationSponsorshipSyncResponseModel> Sync([FromBody] OrganizationSponsorshipSyncRequestModel model)
    {
        var sponsoringOrg = await _organizationRepository.GetByIdAsync(model.SponsoringOrganizationCloudId);
        if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(sponsoringOrg, model.BillingSyncKey))
        {
            throw new BadRequestException("Invalid Billing Sync Key");
        }

        var (syncResponseData, offersToSend) = await _syncSponsorshipsCommand.SyncOrganization(sponsoringOrg, model.ToOrganizationSponsorshipSync().SponsorshipsBatch);
        await _sendSponsorshipOfferCommand.BulkSendSponsorshipOfferAsync(sponsoringOrg.DisplayName(), offersToSend);
        return new OrganizationSponsorshipSyncResponseModel(syncResponseData);
    }

    [Authorize("Application")]
    [HttpDelete("{sponsoringOrganizationId}")]
    [SelfHosted(NotSelfHostedOnly = true)]
    public async Task RevokeSponsorship(Guid sponsoringOrganizationId)
    {

        var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default);
        if (_currentContext.UserId != orgUser?.UserId)
        {
            throw new BadRequestException("Can only revoke a sponsorship you granted.");
        }

View on GitHub (pinned to e93b962371)

Solutions

  1. Copy the current billing sync key from the cloud organization's billing settings.
  2. Update the self-hosted installation configuration with the correct key.
  3. Verify the SponsoringOrganizationCloudId in the request is correct.
Defensive patterns

Strategy: validation

Validate before calling

// Installation client: verify the billing sync key is configured and current before syncing.
if (!model.billingSyncKey) throw new Error('Billing sync key required for sponsorship sync');
if (!model.sponsoringOrganizationCloudId) throw new Error('Sponsoring org cloud id required');

Try / catch

try {
  await post('/organization-sponsorships/sync', syncModel);
} catch (e) {
  if (e.isBadRequest && /invalid billing sync key/i.test(e.message)) {
    await refreshBillingSyncKey(); // re-fetch from cloud, retry once
  } else { throw e; }
}

Prevention

When it happens

Trigger: Self-hosted installation sends a stale or wrong billing sync key; the key was regenerated in the cloud portal; the sponsoring org id in the request does not match the configured org.

Common situations: Configuration drift between cloud billing settings and the self-hosted installation's billing sync configuration.

Related errors


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/ec99e8955fe34ca8. Report an issue: GitHub.