CI HUBCI HUB SDK
Building an Adapter

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:

CapabilityWhereEffect
canAddAsset: trueFolder capabilitiesUpload button appears in that folder
canUpdateAsset: truePer-asset capabilities"Update" option appears on right-click

Inputs

createAsset

FieldDescription
requestData.body.nameOriginal filename
requestData.body.dataBase64File as base64 data URI (fallback)
requestData.body.dataBufferFile as Buffer (preferred)
requestData.body.parentIdTarget folder ID
requestData.body.createAssetOptionsFilter-format options from info.createAssetOptions

updateAsset

FieldDescription
requestData.params.assetIdID of asset to update
requestData.body.nameNew filename
requestData.body.dataBase64File as base64 data URI (fallback)
requestData.body.dataBufferFile 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 → createAsset called with file buffer and parentId
  • Asset with canUpdateAsset: true → right-click shows "Update" option
  • User clicks Update → file picker → updateAsset called with assetId and new file buffer
  • Upload response with additionalRenditionsNeeded → plugin automatically posts document structure to each rendition URL

See Create Asset Schema | Update Asset Schema

On this page