diff --git a/ddsc/core/ddsapi.py b/ddsc/core/ddsapi.py
index 0ec7f94..d0dd066 100644
--- a/ddsc/core/ddsapi.py
+++ b/ddsc/core/ddsapi.py
@@ -470,7 +470,7 @@ class DataServiceApi(object):
         return self._get_collection(url_prefix, data)
 
     def create_upload(self, project_id, filename, content_type, size,
-                      hash_value, hash_alg, storage_provider_id=None):
+                      hash_value, hash_alg, storage_provider_id=None, chunked=True):
         """
         Post to /projects/{project_id}/uploads to create a uuid for uploading chunks.
         NOTE: The optional hash_value and hash_alg parameters are being removed from the DukeDS API.
@@ -481,6 +481,8 @@ class DataServiceApi(object):
         :param hash_value: str hash value of the entire file
         :param hash_alg: str algorithm used to create hash_value
         :param storage_provider_id: str optional storage provider id
+        :param chunked: is the uploaded file made up of multiple chunks. When False a single upload url is returned.
+            For more see https://github.com/Duke-Translational-Bioinformatics/duke-data-service/blob/develop/api_design/DDS-1182-nonchucked_upload_api_design.md
         :return: requests.Response containing the successful result
         """
         data = {
@@ -491,6 +493,7 @@ class DataServiceApi(object):
                 "value": hash_value,
                 "algorithm": hash_alg
             },
+            "chunked": chunked,
         }
         if storage_provider_id:
             data['storage_provider'] = {'id': storage_provider_id}
diff --git a/ddsc/core/fileuploader.py b/ddsc/core/fileuploader.py
index 0a64039..fb7e3e7 100644
--- a/ddsc/core/fileuploader.py
+++ b/ddsc/core/fileuploader.py
@@ -97,14 +97,16 @@ class FileUploadOperations(object):
         self.data_service = data_service
         self.waiting_monitor = waiting_monitor
 
-    def create_upload(self, project_id, path_data, hash_data, remote_filename=None, storage_provider_id=None):
+    def _create_upload(self, project_id, path_data, hash_data, remote_filename=None, storage_provider_id=None,
+                       chunked=True):
         """
-        Create upload so we can send call further methods.
+        Create upload for uploading multiple chunks or the non-chunked variety (includes upload url).
         :param project_id: str: uuid of the project
         :param path_data: PathData: holds file system data about the file we are uploading
         :param hash_data: HashData: contains hash alg and value for the file we are uploading
         :param remote_filename: str: name to use for our remote file (defaults to path_data basename otherwise)
         :param storage_provider_id: str: optional storage provider id
+        :param chunked: bool: should we create a chunked upload
         :return: str: uuid for the upload
         """
         if not remote_filename:
@@ -115,10 +117,42 @@ class FileUploadOperations(object):
         def func():
             return self.data_service.create_upload(project_id, remote_filename, mime_type, size,
                                                    hash_data.value, hash_data.alg,
-                                                   storage_provider_id=storage_provider_id)
+                                                   storage_provider_id=storage_provider_id,
+                                                   chunked=chunked)
 
         resp = retry_until_resource_is_consistent(func, self.waiting_monitor)
-        return resp.json()['id']
+        return resp.json()
+
+    def create_upload(self, project_id, path_data, hash_data, remote_filename=None, storage_provider_id=None):
+        """
+        Create a chunked upload id to pass to create_file_chunk_url to create upload urls.
+        :param project_id: str: uuid of the project
+        :param path_data: PathData: holds file system data about the file we are uploading
+        :param hash_data: HashData: contains hash alg and value for the file we are uploading
+        :param remote_filename: str: name to use for our remote file (defaults to path_data basename otherwise)
+        :param storage_provider_id: str: optional storage provider id
+        :return: str: uuid for the upload
+        """
+        upload_response = self._create_upload(project_id, path_data, hash_data, remote_filename=remote_filename,
+                                              storage_provider_id=storage_provider_id, chunked=True)
+        return upload_response['id']
+
+    def create_upload_and_chunk_url(self, project_id, path_data, hash_data, remote_filename=None,
+                                    storage_provider_id=None):
+        """
+        Create an non-chunked upload that returns upload id and upload url. This type of upload doesn't allow
+        additional upload urls. For single chunk files this method is more efficient than
+        create_upload/create_file_chunk_url.
+        :param project_id: str: uuid of the project
+        :param path_data: PathData: holds file system data about the file we are uploading
+        :param hash_data: HashData: contains hash alg and value for the file we are uploading
+        :param remote_filename: str: name to use for our remote file (defaults to path_data basename otherwise)
+        :param storage_provider_id:str: optional storage provider id
+        :return: str, dict: uuid for the upload, upload chunk url dict
+        """
+        upload_response = self._create_upload(project_id, path_data, hash_data, remote_filename=remote_filename,
+                                              storage_provider_id=storage_provider_id, chunked=False)
+        return upload_response['id'], upload_response['signed_url']
 
     def create_file_chunk_url(self, upload_id, chunk_num, chunk):
         """
diff --git a/ddsc/core/projectuploader.py b/ddsc/core/projectuploader.py
index 0e577a1..8873288 100644
--- a/ddsc/core/projectuploader.py
+++ b/ddsc/core/projectuploader.py
@@ -402,15 +402,13 @@ def create_small_file(upload_context):
     parent_data, path_data, remote_file_id = upload_context.params
 
     # The small file will fit into one chunk so read into memory and hash it.
-    chunk_num = 0
     chunk = path_data.read_whole_file()
     hash_data = path_data.get_hash()
 
     # Talk to data service uploading chunk and creating the file.
     upload_operations = FileUploadOperations(data_service, upload_context)
-    upload_id = upload_operations.create_upload(upload_context.project_id, path_data, hash_data,
-                                                storage_provider_id=upload_context.config.storage_provider_id)
-    url_info = upload_operations.create_file_chunk_url(upload_id, chunk_num, chunk)
+    upload_id, url_info = upload_operations.create_upload_and_chunk_url(
+        upload_context.project_id, path_data, hash_data, storage_provider_id=upload_context.config.storage_provider_id)
     upload_operations.send_file_external(url_info, chunk)
     return upload_operations.finish_upload(upload_id, hash_data, parent_data, remote_file_id)
 

Empty files fail to upload to DukeDS S3 backend
This results in an error like `Failed to send file to external store. Error:400 https...`.
This error occurs when uploading the file to the S3 backend.
The text response from the exception:
```
<Error><Code>InvalidPartSizeZero</Code><Message>The upload part size cannot be zero.</Message><Resource...
```
diff --git a/ddsc/core/tests/test_ddsapi.py b/ddsc/core/tests/test_ddsapi.py
index eb51af9..1f093d5 100644
--- a/ddsc/core/tests/test_ddsapi.py
+++ b/ddsc/core/tests/test_ddsapi.py
@@ -720,6 +720,31 @@ class TestDataServiceApi(TestCase):
                 "value": "somehash",
                 "algorithm": "md5"
             },
+            "chunked": True,
+            "storage_provider": {"id": "abc456"}
+        })
+        mock_requests.post.assert_called_with('something.com/v1/projects/123/uploads',
+                                              expected_data,
+                                              headers=ANY)
+
+    def test_create_non_chunked_upload(self):
+        mock_requests = MagicMock()
+        api = DataServiceApi(auth=self.create_mock_auth(config_page_size=100),
+                             url="something.com/v1",
+                             http=mock_requests)
+        mock_requests.post.return_value = fake_response(status_code=201, json_return_value={})
+        api.create_upload(project_id='123', filename='data.txt', content_type='sometype', size=10,
+                          hash_value='somehash', hash_alg='md5', storage_provider_id='abc456',
+                          chunked=False)
+        expected_data = json.dumps({
+            "name": "data.txt",
+            "content_type": "sometype",
+            "size": 10,
+            "hash": {
+                "value": "somehash",
+                "algorithm": "md5"
+            },
+            "chunked": False,
             "storage_provider": {"id": "abc456"}
         })
         mock_requests.post.assert_called_with('something.com/v1/projects/123/uploads',
diff --git a/ddsc/core/tests/test_fileuploader.py b/ddsc/core/tests/test_fileuploader.py
index 02f33a9..51043ba 100644
--- a/ddsc/core/tests/test_fileuploader.py
+++ b/ddsc/core/tests/test_fileuploader.py
@@ -228,11 +228,13 @@ class TestFileUploadOperations(TestCase):
         fop = FileUploadOperations(data_service, MagicMock())
         path_data = MagicMock()
         path_data.name.return_value = 'data.dat'
-        upload_id = fop.create_upload(project_id='12', path_data=path_data, hash_data=MagicMock(), remote_filename='other.dat')
+        upload_id = fop.create_upload(project_id='12', path_data=path_data, hash_data=MagicMock(),
+                                      remote_filename='other.dat')
         self.assertEqual(upload_id, '123')
         args, kwargs = data_service.create_upload.call_args
         self.assertEqual(args[0], '12')
         self.assertEqual(args[1], 'other.dat')
+        self.assertEqual(kwargs['chunked'], True)
 
     @patch('ddsc.core.fileuploader.HashData')
     def test_create_file_chunk_url_uses_one_based_indexing(self, hash_data):
@@ -256,3 +258,28 @@ class TestFileUploadOperations(TestCase):
                                                           4,       # chunk_len
                                                           'h@sh',  # hash_value
                                                           'md5')   # hash_alg
+
+    def test_create_upload_and_chunk_url(self):
+        data_service = MagicMock()
+        response = Mock()
+        response.json.return_value = {'id': '123', 'signed_url': {
+            "http_verb": "PUT",
+            "host": "duke_data_service_prod.s3.amazonaws.com",
+        }}
+        data_service.create_upload.side_effect = [
+            response
+        ]
+        fop = FileUploadOperations(data_service, MagicMock())
+        path_data = MagicMock()
+        path_data.name.return_value = 'data.dat'
+        upload_id, upload_chunk_url = fop.create_upload_and_chunk_url(
+            project_id='12', path_data=path_data, hash_data=MagicMock(), remote_filename='other.dat')
+        self.assertEqual(upload_id, '123')
+        args, kwargs = data_service.create_upload.call_args
+        self.assertEqual(args[0], '12')
+        self.assertEqual(args[1], 'other.dat')
+        self.assertEqual(kwargs['chunked'], False)
+        self.assertEqual(upload_chunk_url, {
+            "http_verb": "PUT",
+            "host": "duke_data_service_prod.s3.amazonaws.com",
+        })
diff --git a/ddsc/core/tests/test_projectuploader.py b/ddsc/core/tests/test_projectuploader.py
index 0b9294b..a543847 100644
--- a/ddsc/core/tests/test_projectuploader.py
+++ b/ddsc/core/tests/test_projectuploader.py
@@ -147,18 +147,16 @@ class TestCreateProjectCommand(TestCase):
 
 
 class TestCreateSmallFile(TestCase):
-    @patch('ddsc.core.projectuploader.FileUploadOperations')
+    @patch('ddsc.core.projectuploader.FileUploadOperations', autospec=True)
     def test_create_small_file_passes_zero_index(self, mock_file_operations):
         mock_path_data = Mock()
         mock_path_data.read_whole_file.return_value = 'data'
-        mock_file_operations.return_value.create_upload.return_value = 'someId'
+        mock_file_operations.return_value.create_upload_and_chunk_url.return_value = (
+            'someId', {'host': 'somehost', 'url': 'someurl'}
+        )
 
         upload_context = Mock(params=(Mock(), mock_path_data, Mock()))
         resp = create_small_file(upload_context)
 
         self.assertEqual(resp, mock_file_operations.return_value.finish_upload.return_value)
-        mock_file_operations.return_value.create_file_chunk_url.assert_called_with(
-            'someId',
-            0,  # zero based index passed to create_file_chunk_url
-            'data'
-        )
+        mock_file_operations.return_value.create_file_chunk_url.assert_not_called()

pip install -e .[dev]
parse_log_pytest
requirements.txt
mock nose pytest
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
charset-normalizer==3.4.1
-e git+https://github.com/Duke-GCB/DukeDSClient.git@a14dc08e5c37446b489d9aed5def6bf5fd0fd445#egg=DukeDSClient
exceptiongroup==1.2.2
future==0.16.0
idna==3.10
iniconfig==2.1.0
mock==5.2.0
nose==1.3.7
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
pytz==2025.2
PyYAML==5.1
requests==2.32.3
six==1.10.0
tomli==2.2.1
urllib3==2.3.0

name: DukeDSClient
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - exceptiongroup==1.2.2
      - future==0.16.0
      - idna==3.10
      - iniconfig==2.1.0
      - mock==5.2.0
      - nose==1.3.7
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - pytz==2025.2
      - pyyaml==5.1
      - requests==2.32.3
      - six==1.10.0
      - tomli==2.2.1
      - urllib3==2.3.0
prefix: /opt/conda/envs/DukeDSClient

ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_create_non_chunked_upload ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_create_upload ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_create_upload_and_chunk_url ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_create_upload_remote_filename ddsc/core/tests/test_projectuploader.py::TestCreateSmallFile::test_create_small_file_passes_zero_index
ddsc/core/tests/test_ddsapi.py::TestMultiJSONResponse::test_pass_through_works_with_one_response ddsc/core/tests/test_ddsapi.py::TestMultiJSONResponse::test_pass_through_works_with_three_responses ddsc/core/tests/test_ddsapi.py::TestMultiJSONResponse::test_pass_through_works_with_two_responses ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_auth_provider_add_user ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_auth_provider_affiliate_single ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_check_err_with_400 ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_check_err_with_404 ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_check_err_with_404_with_flag ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_check_err_with_500 ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_check_err_with_good_response ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_constructor_creates_session_when_passed_none ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_delete_file ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_delete_folder ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_delete_raises_error_on_paging_response ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_all_project_transfers ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_auth_provider ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_auth_provider_affiliates_email ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_auth_provider_affiliates_full_name_contains ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_auth_provider_affiliates_username ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_auth_providers ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_collection_one_page ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_collection_three_pages ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_collection_two_pages ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_folder ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_project_children ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_project_files ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_project_permissions ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_project_transfers ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_projects ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_single_item_raises_error_on_paging_response ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_single_page_works_on_paging_response ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_users_no_filtering ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_get_users_with_filtering ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_list_auth_roles ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_post_raises_error_on_paging_response ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_put_create_upload_url ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_put_create_upload_url_invalid_number ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_put_raises_error_on_paging_response ddsc/core/tests/test_ddsapi.py::TestDataServiceApi::test_relations_methods ddsc/core/tests/test_ddsapi.py::TestDataServiceAuth::test_claim_new_token ddsc/core/tests/test_ddsapi.py::TestDataServiceAuth::test_claim_new_token_error ddsc/core/tests/test_ddsapi.py::TestDataServiceAuth::test_claim_new_token_missing_agent ddsc/core/tests/test_ddsapi.py::TestDataServiceAuth::test_claim_new_token_missing_setup ddsc/core/tests/test_ddsapi.py::TestMissingInitialSetupError::test_constructor ddsc/core/tests/test_ddsapi.py::TestSoftwareAgentNotFoundError::test_constructor ddsc/core/tests/test_ddsapi.py::TestUnexpectedPagingReceivedError::test_constructor ddsc/core/tests/test_ddsapi.py::TestAuthTokenCreationError::test_constructor ddsc/core/tests/test_ddsapi.py::TestInconsistentResourceMonitoring::test_retry_until_resource_is_consistent ddsc/core/tests/test_ddsapi.py::TestInconsistentResourceMonitoring::test_retry_until_resource_is_consistent_with_one_retry ddsc/core/tests/test_ddsapi.py::TestInconsistentResourceMonitoring::test_retry_until_resource_is_consistent_with_two_retries ddsc/core/tests/test_ddsapi.py::TestRetryWhenServiceDown::test_returns_value_when_ok ddsc/core/tests/test_ddsapi.py::TestRetryWhenServiceDown::test_will_just_raise_when_other_error ddsc/core/tests/test_ddsapi.py::TestRetryWhenServiceDown::test_will_retry_after_waiting ddsc/core/tests/test_fileuploader.py::TestParallelChunkProcessor::test_determine_num_chunks ddsc/core/tests/test_fileuploader.py::TestParallelChunkProcessor::test_make_work_parcels ddsc/core/tests/test_fileuploader.py::TestUploadAsync::test_upload_async_sends_exception_to_progress_queue ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_create_file_chunk_url_uses_one_based_indexing ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_create_upload_default_remote_filename ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_create_upload_with_one_pause ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_create_upload_with_one_pause_then_failure ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_create_upload_with_two_pauses ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_finish_upload ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_send_file_external_no_retry_post ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_send_file_external_retry_put ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_send_file_external_retry_put_fail_after_5_times ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_send_file_external_succeeds_3rd_time ddsc/core/tests/test_fileuploader.py::TestFileUploadOperations::test_send_file_external_works_first_time ddsc/core/tests/test_projectuploader.py::TestUploadContext::test_can_pickle ddsc/core/tests/test_projectuploader.py::TestUploadContext::test_done_waiting ddsc/core/tests/test_projectuploader.py::TestUploadContext::test_start_waiting ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_nested_directories ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_nested_directories_skip_parents ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_single_empty_existing_directory ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_single_empty_non_existant_directory ddsc/core/tests/test_projectuploader.py::TestProjectUploadDryRun::test_some_files ddsc/core/tests/test_projectuploader.py::TestCreateProjectCommand::test_constructor_fails_for_id ddsc/core/tests/test_projectuploader.py::TestCreateProjectCommand::test_constructor_ok_for_name ddsc/core/tests/test_projectuploader.py::TestCreateProjectCommand::test_upload_project_run
ddsc/core/projectuploader.py ddsc/core/ddsapi.py ddsc/core/fileuploader.py
ddsc/core/projectuploader.py ddsc/core/ddsapi.py ddsc/core/fileuploader.py