we-promise/sure · error · AuthenticationError

access_forbidden

access_forbidden

Error message

Access forbidden - check your permissions

What it means

The API answered HTTP 403: the credential is valid (it passed auth) but is not entitled to the resource. Mapped to AuthenticationError(:access_forbidden), though it is an authorization problem, not an authentication one - the token works, the target does not.

Source

Thrown at app/models/provider/indexa_capital.rb:206

      jwt
    end

    def handle_response(response)
      case response.code
      when 200, 201
        begin
          JSON.parse(response.body, symbolize_names: true)
        rescue JSON::ParserError => e
          raise Error.new("Invalid JSON in response: #{e.message}", :bad_response)
        end
      when 400
        Rails.logger.error "IndexaCapital API: Bad request - #{response.body}"
        raise Error.new("Bad request: #{response.body}", :bad_request)
      when 401
        raise AuthenticationError.new("Invalid credentials", :unauthorized)
      when 403
        raise AuthenticationError.new("Access forbidden - check your permissions", :access_forbidden)
      when 404
        raise Error.new("Resource not found", :not_found)
      when 429
        raise Error.new("Rate limit exceeded. Please try again later.", :rate_limited)
      when 500..599
        raise Error.new("IndexaCapital server error (#{response.code}). Please try again later.", :server_error)
      else
        Rails.logger.error "IndexaCapital API: Unexpected response - Code: #{response.code}, Body: #{response.body}"
        raise Error.new("Unexpected error: #{response.code} - #{response.body}", :unknown)
      end
    end

    # Extract accounts array from /users/me response
    # API returns: { accounts: [{ account_number: "ABC12345", type: "mutual", status: "active", ... }] }
    def extract_accounts(user_data)
      accounts = user_data[:accounts] || []
      accounts.map do |acct|
        {

View on GitHub (pinned to e69894adb9)

Solutions

  1. Only ever call account endpoints with account_number values freshly returned by list_accounts for the same credential
  2. Drop or disable the offending account from the sync set and re-list to confirm it disappeared
  3. Check the Indexa dashboard for the token's permitted accounts/scopes
  4. If every call 403s with a token that used to work, the token's permissions changed - regenerate it

Example fix

# before
provider.get_portfolio(account_number: params[:account_number]) # user-supplied

# after
owned = provider.list_accounts.map { |a| a[:account_number] }
raise ArgumentError, "account not owned by this credential" unless owned.include?(params[:account_number])
provider.get_portfolio(account_number: params[:account_number])
Defensive patterns

Strategy: validation

Validate before calling

owned = provider.list_accounts.map { |a| a[:account_number] }.to_set
raise ArgumentError, "account not owned by this credential" unless owned.include?(account_number)
provider.get_portfolio(account_number: account_number)

Type guard

def indexa_forbidden?(error)
  error.is_a?(Provider::IndexaCapital::AuthenticationError) && error.error_type == :access_forbidden
end

Try / catch

begin
  provider.get_portfolio(account_number: num)
rescue Provider::IndexaCapital::AuthenticationError => e
  raise unless e.error_type == :access_forbidden
  account.update!(sync_disabled: true, disable_reason: "not_authorized_by_provider")
end

Prevention

When it happens

Trigger: Querying /accounts/{n}/... with an account_number that belongs to a different Indexa user; a token whose plan/permissions exclude the endpoint; regional or account-state restrictions (e.g. closed or transferred pension plan).

Common situations: Typo'd or transposed account numbers that are well-formed but foreign, tokens scoped to a subset of accounts, account ownership changed (divorce/inheritance transfers) while the number stayed in your sync list.

Understand the failure class

Related errors


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/1b8092979528fdfa. Report an issue: GitHub.