Skip to content

Upload files directly to storage

This guide describes how to add and replace files in DAM using the direct upload flow, where your system uploads the file bytes straight to Azure Blob Storage instead of sending them through the DAM API.

Overview

Traditionally, files were created by sending the raw bytes to DAM in the request body (the import-from-byte-data endpoint). That approach forces every byte to travel through the DAM API, which does not scale well for large files or replace-heavy integrations.

With direct upload, DAM hands you a short-lived, writable URL pointing directly at Azure Blob Storage. Your system uploads the bytes to that URL, and DAM imports the file asynchronously once the upload completes. This removes the DAM API as a bottleneck and lets you upload large local files efficiently.

The flow has two phases:

  1. Request an upload URL from DAM. DAM reserves a file identifier and returns a writable URL.
  2. Upload the bytes directly to that URL.

Because the import happens asynchronously, you do not get the finished file in the response. Instead, you subscribe to the AfterFileCreated and AfterFileCreateFailed webhooks and correlate them with the fileId returned in phase 1 to learn when the file is ready or whether the import failed.

Note

The import-from-byte-data endpoint is deprecated. New integrations should use the direct upload flow described here. See Migrating from import-from-byte-data below.

Phase 1 — Request an upload URL

Call the upload-url endpoint to reserve a file and obtain a writable URL.

POST /api/{culture}/files/upload-url?api-version=26.0

Request body:

1
2
3
4
5
6
7
8
{
  "fileName": "product-photo.jpg",
  "parentFolderId": "f1d2c3b4-a5e6-7890-1234-567890abcdef",
  "description": "Product photo for the summer campaign.",
  "fileTypeId": null,
  "tryLoadExifData": false,
  "reduceImageToRecommendedSize": true
}
Field Required Description
fileName Yes The name the file will be created with. Up to 128 characters.
parentFolderId Yes The id of the folder the file will be placed in.
description No A description for the file. Up to 4000 characters.
fileTypeId No The file type to assign to the file.
tryLoadExifData No When true, EXIF metadata is read from the uploaded file and stored as attributes. When omitted, no EXIF metadata is read.
reduceImageToRecommendedSize No When true, an oversized transformable image is reduced to the recommended size after import. When omitted, the image is imported unchanged.

Response:

1
2
3
4
5
{
  "uploadUrl": "<upload-url>",
  "validUntilUtc": "2026-06-10T13:05:00Z",
  "fileId": "9a8b7c6d-5e4f-3210-fedc-ba9876543210"
}
Field Description
uploadUrl A writable SAS URL pointing directly at Azure Blob Storage. Upload the file bytes here.
validUntilUtc The UTC timestamp after which uploadUrl expires. Upload the bytes before this time.
fileId The identifier the file will have once it is imported. Use it to correlate the webhook later.

Warning

The uploadUrl is only valid until validUntilUtc. If you do not upload the bytes before it expires, the reservation is cleaned up automatically and you must request a new upload URL.

This endpoint requires the AddFile permission and validates the target folder and file type before returning a URL.

Phase 2 — Upload the bytes

Upload the file content to the returned uploadUrl with an HTTP PUT. Because this is a block blob SAS URL, set the x-ms-blob-type header to BlockBlob. See Azure's Put Blob reference for the request format.

1
2
3
4
5
PUT https://<account>.blob.core.windows.net/<container>/<path>?<sas-token>
x-ms-blob-type: BlockBlob
Content-Type: application/octet-stream

<file bytes>

A successful upload returns 201 Created from Azure Blob Storage. You do not call DAM again — DAM detects the completed upload and imports the file asynchronously.

If the SAS URL has expired, the upload is rejected by Azure Blob Storage. Request a new upload URL and try again.

Code examples

The examples below perform the complete add-file flow — requesting the upload URL and uploading the bytes — with the Bizzkit .NET SDK: first by calling the API directly alongside the Azure Storage SDK, and then with the Bizzkit.Sdk.Dam.Upload helper, which performs both phases in a single call.

Both examples use an authenticated DAM SDK client created with DamClientFactory:

using Bizzkit.Sdk.Dam;

var factory = new DamClientFactory(new DamConnectionOptions
{
    BaseUrl = "https://myenv-damapi.bizzkit.cloud",
    Authority = "https://myenv-auth.bizzkit.cloud",
    ClientId = "BizzkitClient",
    ClientSecret = "BizzkitSecret",
    Scope = "damapi/"
});

Without the upload helper

Call the upload-url endpoint with the DAM SDK client, then upload the bytes to the returned URL with the Azure Storage Blob client library. It depends on the Bizzkit.Sdk.Dam and Azure.Storage.Blobs packages:

dotnet add package Bizzkit.Sdk.Dam
dotnet add package Azure.Storage.Blobs
using Azure.Storage.Blobs;
using Bizzkit.Sdk.Dam;

var client = await factory.CreateAuthenticatedClientAsync();

// Phase 1 — request a writable upload URL from DAM.
var upload = await client.CreateFileUploadUrlAsync("en-GB", new CreateFileUploadUrlModel
{
    FileName = "product-photo.jpg",
    ParentFolderId = Guid.Parse("f1d2c3b4-a5e6-7890-1234-567890abcdef")
});

// Phase 2 — upload the bytes straight to Azure Blob Storage.
await using var fileStream = File.OpenRead("product-photo.jpg");
await new BlobClient(upload.UploadUrl).UploadAsync(fileStream);

Console.WriteLine($"Upload accepted. The file will be created with id {upload.FileId}.");

DAM imports the file asynchronously once the upload completes. Use upload.FileId to correlate the AfterFileCreated and AfterFileCreateFailed webhooks and learn when the file is ready or whether it failed.

With the upload helper

The Bizzkit.Sdk.Dam.Upload package provides an upload helper that performs both phases for you — it requests the upload URL and uploads the bytes in a single call:

dotnet add package Bizzkit.Sdk.Dam.Upload

Register the factory and the helper, then resolve IDamFileUploader:

1
2
3
4
5
6
7
8
using Bizzkit.Sdk.Dam.Upload;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddSingleton<IDamClientFactory>(factory);
services.AddBizzkitDamFileUploader();

var uploader = services.BuildServiceProvider().GetRequiredService<IDamFileUploader>();

Upload a file. The helper returns the pre-allocated FileId that the file will be created with:

var parameters = new CreateFileUploadUrlModel
{
    FileName = "product-photo.jpg",
    ParentFolderId = Guid.Parse("f1d2c3b4-a5e6-7890-1234-567890abcdef")
};

await using var fileStream = File.OpenRead("product-photo.jpg");
var result = await uploader.UploadFileFromStreamAsync("en-GB", parameters, fileStream);

Console.WriteLine($"Upload accepted. The file will be created with id {result.FileId}.");

The helper also accepts a source URL that Azure Storage copies from directly, without streaming the bytes through your process:

// From a reachable source URL, copied server-side by Azure Storage.
await uploader.UploadFileFromUrlAsync("en-GB", parameters, new Uri("https://example.com/assets/product-photo.jpg"));

As with the direct API calls, DAM imports the file asynchronously. Use result.FileId to correlate the AfterFileCreated and AfterFileCreateFailed webhooks and learn when the file is ready or whether it failed.

Knowing when the file is ready

Because the import runs asynchronously, the file is not immediately available after the upload. To know when it is ready, subscribe to the AfterFileCreated webhook.

When DAM finishes importing the file, it sends an AfterFileCreated event whose payload contains the identifiers of the created files:

1
2
3
4
5
{
  "FileIdsOfCreated": [
    "9a8b7c6d-5e4f-3210-fedc-ba9876543210"
  ]
}

Match the fileId you received in phase 1 against FileIdsOfCreated to correlate the event with your upload.

If the import fails — for example, the uploaded blob cannot be promoted into a file — DAM instead sends an AfterFileCreateFailed event with the same fileId in its FileIdsOfFailed payload. Subscribe to it to handle the unhappy path.

For details on subscribing to webhooks and verifying their signatures, see the Events concept page.

Replacing a file

Replacing an existing file uses the same two-phase upload, but you start from the replace-upload-url endpoint, which lets you carry metadata over from the old file. See the DAM API reference for the complete replace-upload-url specification.

POST /api/{culture}/files/replace-upload-url?api-version=26.0

Request body:

1
2
3
4
5
6
7
8
9
{
  "targetFileId": "11111111-2222-3333-4444-555555555555",
  "newFileName": "product-photo-v2.jpg",
  "copyAttributes": true,
  "copyTags": true,
  "copyDescription": true,
  "tryLoadExifData": false,
  "reduceImageToRecommendedSize": false
}
Field Description
targetFileId The identifier of the existing file to replace.
newFileName The file name for the replacement file.
copyAttributes Copy the attributes from the replaced file to the new file.
copyTags Copy the tags from the replaced file to the new file.
copyDescription Copy the description from the replaced file to the new file.
tryLoadExifData Read EXIF metadata from the uploaded file, when available.
reduceImageToRecommendedSize Reduce the image to the recommended size during import.

The response has the same shape as the upload-url response (uploadUrl, validUntilUtc, fileId). Upload the bytes exactly as in phase 2, and rely on the webhook to learn when the replacement is complete. This endpoint requires the ReplaceFile permission.

Replacing with the upload helper

The same IDamFileUploader used for adding files also has a method to replace the existing files. ReplaceFileFromStreamAsync requests the replace-upload URL and uploads the replacement bytes in a single call:

var parameters = new CreateFileReplaceUploadUrlModel
{
    TargetFileId = Guid.Parse("11111111-2222-3333-4444-555555555555"),
    NewFileName = "product-photo-v2.jpg",
    CopyAttributes = true,
    CopyTags = true,
    CopyDescription = true
};

await using var fileStream = File.OpenRead("product-photo-v2.jpg");
var result = await uploader.ReplaceFileFromStreamAsync("en-GB", parameters, fileStream);

Console.WriteLine($"Replacement accepted for file {result.TargetFileId}.");

As with adding a file, the helper also accepts a source URL that Azure Storage copies from directly, without streaming the bytes through your process:

// From a reachable source URL, copied server-side by Azure Storage.
await uploader.ReplaceFileFromUrlAsync("en-GB", parameters, new Uri("https://example.com/assets/product-photo-v2.jpg"));

Both methods return a DamFileReplaceResult carrying the information known at upload time — the TargetFileId being replaced, the NewFileName, and the FileId the replaced file will be created with once the upload is promoted. The replacement is imported asynchronously, so use result.FileId to correlate the AfterFileCreated and AfterFileCreateFailed webhooks and learn when the replacement is ready or whether it failed.

Migrating from import-from-byte-data

The import-from-byte-data endpoint is deprecated. It remains functional for backward compatibility, but new integrations should use the direct upload flow.

Previously, you sent the bytes in the request body:

POST /api/{culture}/files/import-from-byte-data

Now, request an upload URL and upload the bytes directly to storage:

  1. POST /api/{culture}/files/upload-url?api-version=26.0 to reserve the file and get a writable URL.
  2. PUT the bytes to the returned uploadUrl.
  3. Wait for the AfterFileCreated webhook, correlating by fileId.

This keeps the file bytes off the DAM API, scales better for large files, and uses the same readiness signal as the rest of the direct upload flow.

The full flow

The sequence below shows the complete add-file flow, from requesting an upload URL to receiving the readiness webhook.

Hold "Ctrl" to enable pan & zoom
sequenceDiagram
    participant Client
    participant DAM
    participant Blob as Azure Blob Storage

    Client->>DAM: POST /api/{culture}/files/upload-url
    DAM-->>Client: uploadUrl, validUntilUtc, fileId
    Client->>Blob: PUT bytes to uploadUrl
    Blob-->>Client: 201 Created
    Blob-->>DAM: Blob-created event
    DAM->>DAM: Import file (async)
    DAM-->>Client: AfterFileCreated webhook (FileIdsOfCreated)