Vieunite.Developers
Open quickstart

End-to-end implementation case

How MySignage Nexus Portal made Vieunite artwork part of its native media workflow.

Nexus editors choose Vieunite artwork from the same screens they already use for media. This case study follows that selection through the Picker, backend import and return to the Media Library.

The case

The integration starts and ends inside Nexus

MySignage Nexus Portal is a multi-tenant digital-signage CMS. Its editors already choose images from a Media Library when managing content and building layouts, so Vieunite Collection was added as another way to populate that library—not as a separate content type or publishing path.

Entry points

Existing media surfaces

A featured Browse library action appears on the Media page, with a compact Add from Vieunite action inside the layout media picker.

Selection

Hosted Picker modal

The SDK provides artwork discovery, orientation filtering, multiple selection, completion messaging and cancellation.

Outcome

Normal Nexus media

Each successful selection becomes, or reuses, an ordinary CMS image that existing layouts, playlists and players can consume.

Real editor journey

From the editor’s click to a saved image

Nexus UIBrowse libraryExisting CMS access rules
Hosted PickerDiscover and selectVieunite-owned modal
Nexus backendRedeem and importCalls Collection server-side
Nexus UIRefresh mediaExisting CMS surfaces
  1. 1

    An authorised editor starts from an existing media surfaceNexus renders the featured Media Library action only for users with media-write permission and reuses the compact action inside its existing layout media picker. The backend authorises both callbacks.

  2. 2

    The frontend loads the Picker SDK on demandThe first click loads picker.js, creates a modal client and opens an artwork-only, multiple-selection session. An empty initial orientation list means all orientations.

  3. 3

    The SDK uses Nexus callbackscreateSession and redeemSelection call the authenticated Nexus API client. The browser never calls token-authenticated Collection operations directly.

  4. 4

    The backend completes the import before confirmation returnsNexus redeems the selection, resolves current artwork data, reuses existing media where possible and imports missing image bytes into CMS-owned storage.

  5. 5

    The frontend turns the backend result into product feedbackIt displays imported, already-in-library and failed counts, invalidates media queries, and lets the editor continue with the ordinary Media Library or layout picker.

Frontend implementation

Make Collection feel native to the CMS

The current fe-cms implementation uses one reusable action component across both media surfaces, one SDK adapter, and the generated Nexus OpenAPI client. The UI does not need to understand Collection delivery URLs or storage.

Frontend · Entry points

Put the action where editors already choose media

The main Media Library uses a featured banner that expands on desktop and becomes full-width on mobile; Nexus renders it only for users with media-write permission. The existing layout media picker reuses the compact secondary variant. In either path, the backend checks the editor and organisation again—conditional rendering is useful UX, not the security boundary.

01 Enter from the Media LibraryThe Vieunite action sits beside the folders and media editors already use. Its featured state explains the source before the editor opens it.
Nexus media surfacesTypeScript · React
type MediaEntryProps = { canWriteMedia: boolean }

function MediaLibraryEntry({ canWriteMedia }: MediaEntryProps) {
  return canWriteMedia
    ? <AddFromVieuniteControl variant="featured" />
    : null
}

function LayoutMediaPickerEntry() {
  return (
    <div className="media-source-row">
      <div>
        <strong>Vieunite Collection</strong>
        <span>Import artwork into your media library</span>
      </div>
      <AddFromVieuniteControl variant="secondary" />
    </div>
  )
}

Frontend · SDK adapter

Load the SDK once and connect two CMS callbacks

Nexus injects the SDK script only when needed. The adapter creates the Picker with the exact trusted origin and passes session and redemption requests through the authenticated CMS API client. The callback origin comes from the active browser origin, while the backend still validates it against trusted configuration. Before each opening, Nexus discards any previous Picker client so stale callback configuration cannot survive between sessions.

02 Discover in the hosted PickerCollection supplies search, curated categories, orientation filters and artwork results inside a secure modal while Nexus remains visible behind it.
Picker SDK adapterTypeScript
const SDK_URL = "https://collection.vieunite.com/sdk/v1/picker.js"
const PICKER_ORIGIN = "https://collection.vieunite.com"

let sdkLoadPromise: Promise<void> | null = null
let pickerInstance: VieunitePicker.PickerClient | null = null
let lastImportSummary: VieuniteCollectionImportSummary | null = null

function loadPickerSdk(): Promise<void> {
  if (globalThis.VieunitePicker) return Promise.resolve()
  if (sdkLoadPromise) return sdkLoadPromise

  sdkLoadPromise = new Promise<void>((resolve, reject) => {
    const script = document.createElement("script")
    script.src = SDK_URL
    script.async = true
    script.addEventListener("load", resolve, { once: true })
    script.addEventListener("error", reject, { once: true })
    document.head.appendChild(script)
  })
  return sdkLoadPromise
}

export async function openVieunitePicker(): Promise<VieunitePickerOpenResult> {
  await loadPickerSdk()
  try {
    pickerInstance?.destroy()
  } catch {
    // Best-effort cleanup before applying fresh callback configuration.
  }
  pickerInstance = null
  lastImportSummary = null

  pickerInstance = VieunitePicker.create({
    pickerOrigin: PICKER_ORIGIN,
    presentation: "modal",
    createSession: (request) =>
      nexusApi.createPickerSession({
        ...request,
        callback_origin: window.location.origin,
      }),
    redeemSelection: async (request) => {
      const response = await nexusApi.redeemPickerSelection({
        session_id: request.session_id,
        selection_code: request.selection_code,
      })
      lastImportSummary = response.data.import_summary
      return response
    },
  })

  const result = await pickerInstance.open({
    allowedTypes: ["artwork"],
    selectionMode: "multiple",
    maxSelection: 0,
    initialFilters: { orientations: [] },
    expiresIn: 600,
  })

  if (result.status === "cancelled") return result
  return {
    status: "imported",
    selectionCount: result.selection.length,
    importSummary: lastImportSummary,
  }
}

Frontend · Result handling

Refresh existing media surfaces after import

The redeem callback does not finish until the Nexus backend has produced an item-level import result. A fully failed summary becomes an error. Completed and partial summaries refresh the existing media queries and show counts for new, reused and failed items.

03 Return to normal Nexus mediaAfter redemption and import complete, Nexus reports the result and the selected artwork appears in the ordinary media grid.
Picker resultTypeScript · React
const result: VieunitePickerOpenResult = await openVieunitePicker()

if (result.status === "imported") {
  const summary = result.importSummary

  if (summary?.status === "failed") {
    showError(
      `Import failed for ${summary.failed} of ${summary.total} artworks.`
    )
  } else {
    invalidateMediaQueries(queryClient)
    showSuccess(formatImportSummary(summary))
  }
}
Queries refreshedTypeScript
function invalidateMediaQueries(queryClient: QueryClient): void {
  for (const queryKey of [
    ["media"],
    ["folders", "children"],
    ["mediaSources", "resolve"],
  ]) {
    queryClient.invalidateQueries({ queryKey })
  }
}
Backend summaryNexus behavior
completedRefresh media and report how many items were imported or already present.
partialKeep successful items, refresh media and include the failed count in the message.
failedShow an error and do not claim that the Media Library was updated.
Picker cancelledClose without an import toast or query refresh.

Frontend ↔ backend contract

The SDK calls Nexus; Nexus calls Collection

The frontend uses the generated Nexus API client, so ordinary CMS authentication and organisation context apply. The Collection tenant credential exists only in the backend service.

SDK callbackNexus case-specific routeCollection operationsReturns to SDK
createSessionPOST /v1/media/vieunite-collection/picker/sessionsPOST /v1/picker/sessionssession_id, picker_url, expiry
redeemSelectionPOST /v1/media/vieunite-collection/picker/selections/redeemRedeem, resolve references and request asset grantsCanonical references plus import_summary

Backend implementation

Turn the completed selection into normal CMS media

Nexus performs the import synchronously inside the redeem callback. The four steps below cover session ownership, current artwork data, image import and a result that can be returned again after a retry.

1

Create and bind the Picker session

The endpoint requires the ordinary CMS editor session and media-write permission. It accepts discovery preferences from the SDK, but fixes product policy and the trusted callback origin on the server. The returned Collection session is bound to the current user and organisation before its launch URL is returned.

Store locallysession_id · user_id · organisation_id · expires_at
Return temporarilypicker_url for this authenticated SDK request
Create and bind sessionPython · FastAPI
COLLECTION_ORIGIN = "https://collection.vieunite.com"
TENANT_TOKEN = environ["VIEUNITE_TENANT_TOKEN"]
CMS_PUBLIC_ORIGIN = environ["CMS_PUBLIC_ORIGIN"]


async def collection_post(path, payload, *, headers=None):
    async with httpx.AsyncClient(
        base_url=COLLECTION_ORIGIN,
        timeout=30,
        follow_redirects=False,
    ) as client:
        response = await client.post(
            path,
            json=payload,
            headers={
                "Authorization": f"Bearer {TENANT_TOKEN}",
                "Accept": "application/json",
                **(headers or {}),
            },
        )
    response.raise_for_status()
    return response.json()


@router.post("/v1/media/vieunite-collection/picker/sessions")
async def create_picker_session(
    request: PickerSessionInput,
    editor=Depends(require_media_writer),
):
    if request.callback_origin != CMS_PUBLIC_ORIGIN:
        raise BadRequest("callback_origin_not_allowed")

    upstream = await collection_post("/v1/picker/sessions", {
        "allowed_types": ["artwork"],
        "selection_mode": "multiple",
        "max_selection": 0,
        "default_filters": {
            "resource_type": ["image"],
            "orientations": request.default_filters.orientations,
        },
        "host_capabilities": {"credit_display": False},
        "expires_in": request.expires_in,
        "callback_origin": CMS_PUBLIC_ORIGIN,
    })
    session = upstream["data"]

    await picker_sessions.bind(
        session_id=session["session_id"],
        user_id=editor.id,
        organisation_id=editor.organisation_id,
        expires_at=session["expires_at"],
    )
    return {"data": session}
2

Redeem once and resolve current artwork data

The backend claims the locally bound session for the same user and organisation. It redeems the one-time selection with a stable idempotency key, validates the returned canonical references and saves only those references. It then resolves current availability, rendition delivery facts and the Collection resource version in batches of up to 100.

Redeem and resolvePython
async def redeem_and_resolve(binding, selection_code):
    redeemed = await collection_post(
        "/v1/picker/selections/redeem",
        {
            "session_id": binding.session_id,
            "selection_code": selection_code,
        },
        headers={"Idempotency-Key": f"cms-picker:{binding.local_id}"},
    )
    redemption = redeemed["data"]
    if redemption.get("session_id", binding.session_id) != binding.session_id:
        raise UpstreamResponseError("session_mismatch")

    references = validate_canonical_references(redemption["selection"])
    await picker_sessions.save_selection(binding.local_id, references)

    resolved = []
    for batch in chunks(references, size=100):
        response = await collection_post("/v1/references/resolve", {
            "references": [
                {
                    "type": item["type"],
                    "id": item["id"],
                    "rendition": item["rendition"],
                }
                for item in batch
            ],
            "fields": [
                "id", "type", "name", "rights",
                "available_versions", "version", "updated_at",
            ],
            "context": {"usage": "cms_display"},
        })
        resolved.extend(response["data"])

    return references, resolved
3

Reuse existing media or import a validated image

Nexus derives a deterministic import key from organisation, canonical reference, selected rendition and Collection resource version. An existing completed media item is reused before another grant is requested. Missing media receives a short-lived grant and is downloaded with a separate HTTP client.

The asset client sends no Collection Authorization header, follows no redirects, accepts only configured HTTPS origins and bounds the download before the CMS commits its normal image record.

Reuse or import imagePython
def local_import_key(organisation_id, reference, resource_version):
    identity = "|".join([
        organisation_id,
        reference["provider"],
        reference["type"],
        reference["id"],
        reference["rendition"],
        str(resource_version),
    ])
    return sha256(identity.encode()).hexdigest()


async def import_one(organisation_id, reference, resolved):
    if resolved["status"] != "available":
        raise ImportRejected("artwork_unavailable")

    resource = resolved["resource"]
    version = find_version(resource, reference["rendition"])
    key = local_import_key(
        organisation_id,
        reference,
        resource["version"],
    )

    if existing := await media_store.find_by_import_key(key):
        return {**reference, "import_status": "reused", "media_id": existing.id}

    grants = await collection_post("/v1/assets/signed-urls", {
        "requests": [{
            "artwork_id": reference["id"],
            "variant": reference["rendition"],
        }],
        "expires_in": 900,
    })
    grant = require_matching_grant(grants, reference)
    require_allowed_https_origin(grant["url"], ASSET_ORIGINS)

    fileobj = SpooledTemporaryFile(max_size=8 * 1024 * 1024)
    size = 0
    async with httpx.AsyncClient(
        timeout=120,
        follow_redirects=False,
    ) as asset_client:
        async with asset_client.stream("GET", grant["url"]) as response:
            if response.status_code != 200:
                raise ImportRejected("asset_download_failed")
            if response.headers.get("content-type", "").split(";", 1)[0] != "image/jpeg":
                raise ImportRejected("asset_not_jpeg")

            async for chunk in response.aiter_bytes():
                size += len(chunk)
                if size > MAX_IMPORT_BYTES:
                    raise ImportRejected("asset_too_large")
                fileobj.write(chunk)

    fileobj.seek(0)
    if fileobj.read(3) != b"\xff\xd8\xff":
        raise ImportRejected("asset_not_jpeg")
    fileobj.seek(0)

    media = await media_store.save_image(
        import_key=key,
        fileobj=fileobj,
        size_bytes=size,
        width=version["delivery"]["width"],
        height=version["delivery"]["height"],
        name=resource["name"],
    )
    return {**reference, "import_status": "imported", "media_id": media.id}
4

Checkpoint each item and return an import summary

The redeem route records an outcome after every selected item, so one failure does not remove successful siblings. A completed session stores a sanitized public result and replays it on retry without another Collection request, download or CMS media insert.

Checkpoint and finalizePython · FastAPI
@router.post("/v1/media/vieunite-collection/picker/selections/redeem")
async def redeem_selection(
    request: PickerRedeemInput,
    response: Response,
    editor=Depends(require_media_writer),
):
    binding = await picker_sessions.claim_owned(
        session_id=request.session_id,
        user_id=editor.id,
        organisation_id=editor.organisation_id,
    )
    if binding.finalized:
        return binding.public_result

    references, resolved = await redeem_and_resolve(
        binding,
        request.selection_code,
    )
    resolved_by_reference = index_resolved_results(resolved)

    results = []
    for reference in references:
        try:
            result = await import_one(
                editor.organisation_id,
                reference,
                resolved_by_reference[reference_key(reference)],
            )
        except ImportRejected as error:
            result = {
                **reference,
                "import_status": "failed",
                "media_id": None,
                "error_code": error.public_code,
            }

        results.append(result)
        await picker_sessions.checkpoint(binding.local_id, results)

    public_result = {
        "data": {
            "session_id": binding.session_id,
            "selection": results,
            "import_summary": summarize_imports(results),
        }
    }
    await picker_sessions.complete(binding.local_id, public_result)
    response.headers["Cache-Control"] = "no-store"
    return public_result

CMS data contract

Use local media in the product; keep temporary access server-side

After import, Nexus pages, APIs and players use the CMS's own media record. The Collection reference remains beside that record for source identity, version-aware reuse and future validation.

Use in CMS records and APIs source identity: provider · type · id · rendition source version: Collection resource_version delivery identity: local media_id delivery location: local CMS media URL media facts: width · height · mime_type result: import_status · sanitized error_code
Keep out of CMS responses and persistence tenant token and Authorization header — backend only selection_code — redeem once, then discard signed delivery URL — download once, then discard signed URL query string — never log object-storage credentials — backend only

For your CMS

Adapt the workflow to your CMS

Search documentation

Start with “Picker”, “rendition”, or an endpoint path.
    Case study screenshot

    Screenshot preview