Upload
createAsset uploads a new file into a folder. updateAsset replaces an existing asset's file. CI HUB calls these when the user uploads from the plugin UI.
Enabling Upload in the UI
Upload visibility is controlled by capabilities returned from getFolder and info:
| Capability | Where | Effect |
|---|---|---|
canAddAsset: true | Folder capabilities | Upload button appears in that folder |
canUpdateAsset: true | Per-asset capabilities | "Update" option appears on right-click |
Inputs
createAsset
| Field | Description |
|---|---|
requestData.body.name | Original filename |
requestData.body.dataBase64 | File as base64 data URI (fallback) |
requestData.body.dataBuffer | File as Buffer (preferred) |
requestData.body.parentId | Target folder ID |
requestData.body.createAssetOptions | Filter-format options from info.createAssetOptions |
updateAsset
| Field | Description |
|---|---|
requestData.params.assetId | ID of asset to update |
requestData.body.name | New filename |
requestData.body.dataBase64 | File as base64 data URI (fallback) |
requestData.body.dataBuffer | File as Buffer (preferred) |
Response
return send({
id: string, // new or updated asset ID
name: string, // filename
additionalRenditionsNeeded?: Array<{ // triggers linked-file upload flow
type: string,
url: string
}>
})Patterns
Standard Chunked Upload
ExampleAdapter1 — init upload → PUT chunks to pre-signed URLs → complete:
createAsset: async (locals, requestData) => {
try {
const { name, dataBase64, dataBuffer, createAssetOptions } = requestData.body
const buffer = dataBuffer || Buffer.from(dataBase64.split(',')[1], 'base64')
const assetOptions = parseFilters(createAssetOptions || [])
const { data } = await callApi(locals, 'post', 'v1/assets/asset', {}, {
filename: name,
metadataTemplateId: assetOptions.metadataTemplateId,
securityTemplateIds: [assetOptions.securityTemplateId],
metadata: [],
filesize: buffer.length
})
const { fileId, filename, originalFileId, uploadId } = data
const urls = data.urls || []
const parts = []
if (data.url && urls.length === 0) {
urls.push({ url: data.url, partNumber: 1 })
}
await Promise.all(
urls.map(async url => {
const start = maxChunkSize * (url.partNumber - 1)
const end = Math.min(maxChunkSize * url.partNumber, buffer.length)
const chunk = buffer.subarray(start, end)
const response = await axios.put(url.url, chunk, {
headers: { 'Content-Type': 'application/octet-stream' }
})
parts.push({
etag: response.headers.etag.replace(/"/g, ''),
partNumber: url.partNumber
})
})
)
await callApi(locals, 'post', '/v1/asset/complete', null, {
fileId, filename, filesize: buffer.length,
action: 'complete', originalFileId, uploadId, parts
})
const additionalRenditionsNeeded = createAdditionalRenditions(locals, filename, fileId)
return send({ id: fileId, name: filename, additionalRenditionsNeeded })
} catch (err) {
return sendError(err)
}
},updateAsset — Same Flow with Existing Metadata
Example - fetches existing asset metadata, creates new version with same template/security settings:
updateAsset: async (locals, requestData) => {
try {
const { assetId } = requestData.params
const { name, dataBase64, dataBuffer } = requestData.body
const buffer = dataBuffer || Buffer.from(dataBase64.split(',')[1], 'base64')
const asset = (await callApi(locals, 'GET', `/v1/assets/asset/${assetId}`)).data || {}
const { data } = await callApi(locals, 'post', 'v1/assets/asset', {}, {
filename: name,
metadataTemplateId: asset.metadataTemplateId,
securityTemplateIds: asset.securityTemplateIds,
metadata: (asset.metadata || []).filter(
metadata => !['System Process Source', 'System Layout Links'].includes(metadata.name)
),
filesize: buffer.length,
originalFileId: asset.originalFileId
})
const { fileId, filename, originalFileId, uploadId } = data
const urls = data.urls || []
const parts = []
if (data.url && urls.length === 0) {
urls.push({ url: data.url, partNumber: 1 })
}
await Promise.all(
urls.map(async url => {
const start = maxChunkSize * (url.partNumber - 1)
const end = Math.min(maxChunkSize * url.partNumber, buffer.length)
const chunk = buffer.subarray(start, end)
const response = await axios.put(url.url, chunk, {
headers: { 'Content-Type': 'application/octet-stream' }
})
parts.push({
etag: response.headers.etag.replace(/"/g, ''),
partNumber: url.partNumber
})
})
)
await callApi(locals, 'post', '/v1/asset/complete', null, {
fileId, filename, filesize: buffer.length,
action: 'complete', originalFileId, uploadId, parts
})
const additionalRenditionsNeeded = createAdditionalRenditions(locals, filename, fileId)
return send({ id: fileId, name: filename, additionalRenditionsNeeded })
} catch (err) {
return sendError(err)
}
},Simple FormData Upload
Example - single POST with multipart form data:
const upload = async (locals, requestData) => {
try {
const { name, dataBase64, dataBuffer, parentId } = requestData.body
const assetId = requestData.params?.assetId
const buffer = dataBuffer || Buffer.from(dataBase64.split(',').pop(), 'base64')
const fileSize = buffer.length
const formData = new FormDataNode()
let versionGroup
formData.append('asset', buffer, name || '')
if (assetId) {
versionGroup = (await callApi(locals, 'GET', `/assets/${assetId}`)).asset.versionGroup
}
const id = (await callApi(locals, 'POST', '/assets/upload', {
estimateTime: 1, size: fileSize, totalSize: fileSize,
folderId: assetId ? null : parentId, versionGroup
}, formData, formData.getHeaders()))[0].asset.id
return send({ id, name, additionalRenditionsNeeded: createAdditionalRenditions(locals, name, id) })
} catch (err) {
return sendError(err, 400)
}
}This adapter reuses the same function for both createAsset and updateAsset — when assetId is present, it fetches the version group to create a new version.
Additional Renditions
When uploading InDesign, Illustrator, or Photoshop files, CI HUB can extract linked files and associate them with the uploaded asset. This is driven by additionalRenditionsNeeded in the upload response.
Flow
Creating the Renditions List
Check the file extension — return renditions only for supported creative formats:
const createAdditionalRenditions = (locals, fileName, assetId) => {
const fileExtension = path.extname(fileName).toLowerCase().replace('.', '')
if (['ai', 'indd', 'psd'].includes(fileExtension)) {
return [{
type: 'DocumentSummary',
url: `${locals.endpointUrl}/saveAssetRelations?type=${fileExtension}&assetId=${encodeURIComponent(assetId)}`
}]
}
}The url points to the adapter's own saveAssetRelations handler, which receives the document structure from the plugin and creates asset relationships.
getAdditionalRenditionsNeeded Handler
When using directAccessHelper (browser-side upload), the renditions list isn't computed server-side during upload. Instead, CI HUB calls this handler after upload completes:
getAdditionalRenditionsNeeded: async (locals, requestData) => {
try {
const { assetId, name } = requestData.query
return send(createAdditionalRenditions(locals, name, assetId))
} catch (error) {
return sendError(error, 400)
}
}directAccessHelper
The directAccessHelper is not part of the SDK yet.
For large files, adapters can provide a directAccessHelper — a JavaScript function that runs in the browser and uploads directly to the platform's API (bypassing the CI HUB server). Returned as a string in the adapter's capabilities. The browser-side function receives (axios, payload, sessionId, data, onProgress, adapterData, accessToken, helpers) and returns { id, name, additionalRenditionsNeeded }.
The DAH calls getAdditionalRenditionsNeeded via the CI HUB backend:
const additionalRenditions = async (name, assetId) => await axios.get(
helpers.getBackendServerUrl() + '/api/v1/system/providerInternal/getAdditionalRenditionsNeeded',
{
headers: {
Authorization: 'Bearer '.concat(helpers.getAccessToken()),
'Provider-Authorization': 'Bearer '.concat(helpers.getProviderAccessToken() || helpers.getPayload())
},
params: { name, assetId }
}
)UI Behavior
- Folder with
canAddAsset: true→ upload button visible - User clicks upload → file picker →
createAssetcalled with file buffer andparentId - Asset with
canUpdateAsset: true→ right-click shows "Update" option - User clicks Update → file picker →
updateAssetcalled withassetIdand new file buffer - Upload response with
additionalRenditionsNeeded→ plugin automatically posts document structure to each rendition URL