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
- 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
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
The SDK uses Nexus callbacks
createSessionandredeemSelectioncall the authenticated Nexus API client. The browser never calls token-authenticated Collection operations directly. - 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
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.
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>
)
}
function MediaLibraryEntry({ canWriteMedia }) {
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.
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,
}
}
const SDK_URL = "https://collection.vieunite.com/sdk/v1/picker.js"
const PICKER_ORIGIN = "https://collection.vieunite.com"
let sdkLoadPromise = null
let pickerInstance = null
let lastImportSummary = null
function loadPickerSdk() {
if (globalThis.VieunitePicker) return Promise.resolve()
if (sdkLoadPromise) return sdkLoadPromise
sdkLoadPromise = new Promise((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() {
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.
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))
}
}
const result = 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))
}
}
function invalidateMediaQueries(queryClient: QueryClient): void {
for (const queryKey of [
["media"],
["folders", "children"],
["mediaSources", "resolve"],
]) {
queryClient.invalidateQueries({ queryKey })
}
}
function invalidateMediaQueries(queryClient) {
for (const queryKey of [
["media"],
["folders", "children"],
["mediaSources", "resolve"],
]) {
queryClient.invalidateQueries({ queryKey })
}
}
| Backend summary | Nexus behavior |
|---|---|
completed | Refresh media and report how many items were imported or already present. |
partial | Keep successful items, refresh media and include the failed count in the message. |
failed | Show an error and do not claim that the Media Library was updated. |
| Picker cancelled | Close 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 callback | Nexus case-specific route | Collection operations | Returns to SDK |
|---|---|---|---|
createSession | POST /v1/media/vieunite-collection/picker/sessions | POST /v1/picker/sessions | session_id, picker_url, expiry |
redeemSelection | POST /v1/media/vieunite-collection/picker/selections/redeem | Redeem, resolve references and request asset grants | Canonical 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.
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.
session_id · user_id · organisation_id · expires_atpicker_url for this authenticated SDK requestCOLLECTION_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}
const COLLECTION_ORIGIN = "https://collection.vieunite.com"
const TENANT_TOKEN = process.env.VIEUNITE_TENANT_TOKEN!
const CMS_PUBLIC_ORIGIN = new URL(process.env.CMS_PUBLIC_ORIGIN!).origin
async function collectionPost<T>(
path: string,
body: unknown,
headers: Record<string, string> = {},
): Promise<T> {
const response = await fetch(`${COLLECTION_ORIGIN}${path}`, {
method: "POST",
redirect: "error",
headers: {
Authorization: `Bearer ${TENANT_TOKEN}`,
"Content-Type": "application/json",
Accept: "application/json",
...headers,
},
body: JSON.stringify(body),
})
if (!response.ok) throw new UpstreamError(response.status)
return response.json() as Promise<T>
}
app.post(
"/v1/media/vieunite-collection/picker/sessions",
requireMediaWriter,
async (req: MediaWriterRequest, res: Response) => {
if (req.body.callback_origin !== CMS_PUBLIC_ORIGIN) {
return res.status(400).json({ detail: "callback_origin_not_allowed" })
}
const upstream = await collectionPost<{ data: PickerSession }>(
"/v1/picker/sessions",
{
allowed_types: ["artwork"],
selection_mode: "multiple",
max_selection: 0,
default_filters: {
resource_type: ["image"],
orientations: req.body.default_filters.orientations,
},
host_capabilities: { credit_display: false },
expires_in: req.body.expires_in,
callback_origin: CMS_PUBLIC_ORIGIN,
},
)
await pickerSessions.bind({
sessionId: upstream.data.session_id,
userId: req.user.id,
organisationId: req.user.organisationId,
expiresAt: upstream.data.expires_at,
})
res.set("Cache-Control", "no-store").json(upstream)
},
)
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;
$collectionOrigin = "https://collection.vieunite.com";
$tenantToken = (string) env("VIEUNITE_TENANT_TOKEN");
$cmsUrl = parse_url((string) config("app.url"));
$cmsPort = isset($cmsUrl["port"]) ? ":{$cmsUrl["port"]}" : "";
$cmsOrigin = $cmsUrl["scheme"] . "://" . $cmsUrl["host"] . $cmsPort;
$collectionPost = function (string $path, array $body, array $headers = [])
use ($collectionOrigin, $tenantToken): array {
return Http::withToken($tenantToken)
->acceptJson()
->withHeaders($headers)
->withoutRedirecting()
->post("{$collectionOrigin}{$path}", $body)
->throw()
->json();
};
Route::middleware(["web", "auth", "media.write"])->post(
"/v1/media/vieunite-collection/picker/sessions",
function (Request $request) use ($collectionPost, $cmsOrigin) {
abort_unless(
$request->string("callback_origin")->toString() === $cmsOrigin,
400,
"callback_origin_not_allowed",
);
$upstream = $collectionPost("/v1/picker/sessions", [
"allowed_types" => ["artwork"],
"selection_mode" => "multiple",
"max_selection" => 0,
"default_filters" => [
"resource_type" => ["image"],
"orientations" => $request->input("default_filters.orientations", []),
],
"host_capabilities" => ["credit_display" => false],
"expires_in" => (int) $request->input("expires_in", 600),
"callback_origin" => $cmsOrigin,
]);
$session = $upstream["data"];
PickerSession::bindOwner(
$session["session_id"],
$request->user()->id,
$request->user()->organisation_id,
$session["expires_at"],
);
return response()->json($upstream)
->header("Cache-Control", "no-store");
},
);
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.
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
async function redeemAndResolve(
binding: PickerSessionBinding,
selectionCode: string,
): Promise<{
references: CanonicalReference[]
resolved: ResolvedReference[]
}> {
const redeemed = await collectionPost<PickerRedemption>(
"/v1/picker/selections/redeem",
{
session_id: binding.sessionId,
selection_code: selectionCode,
},
{ "Idempotency-Key": `cms-picker:${binding.localId}` },
)
if ((redeemed.data.session_id ?? binding.sessionId) !== binding.sessionId) {
throw new UpstreamResponseError("session_mismatch")
}
const references = validateCanonicalReferences(redeemed.data.selection)
await pickerSessions.saveSelection(binding.localId, references)
const resolved: ResolvedReference[] = []
for (const batch of chunks(references, 100)) {
const response = await collectionPost<{ data: ResolvedReference[] }>(
"/v1/references/resolve",
{
references: batch.map(({ type, id, rendition }) => ({
type,
id,
rendition,
})),
fields: [
"id", "type", "name", "rights",
"available_versions", "version", "updated_at",
],
context: { usage: "cms_display" },
},
)
resolved.push(...response.data)
}
return { references, resolved }
}
<?php
function redeemAndResolve(
PickerSessionBinding $binding,
string $selectionCode,
callable $collectionPost,
): array {
$redeemed = $collectionPost(
"/v1/picker/selections/redeem",
[
"session_id" => $binding->session_id,
"selection_code" => $selectionCode,
],
["Idempotency-Key" => "cms-picker:{$binding->local_id}"],
);
$sessionId = $redeemed["data"]["session_id"] ?? $binding->session_id;
if ($sessionId !== $binding->session_id) {
throw new UpstreamResponseException("session_mismatch");
}
$references = validateCanonicalReferences($redeemed["data"]["selection"]);
PickerSession::saveSelection($binding->local_id, $references);
$resolved = [];
foreach (array_chunk($references, 100) as $batch) {
$response = $collectionPost("/v1/references/resolve", [
"references" => array_map(
fn (array $item) => [
"type" => $item["type"],
"id" => $item["id"],
"rendition" => $item["rendition"],
],
$batch,
),
"fields" => [
"id", "type", "name", "rights",
"available_versions", "version", "updated_at",
],
"context" => ["usage" => "cms_display"],
]);
array_push($resolved, ...$response["data"]);
}
return [$references, $resolved];
}
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.
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}
import { createHash } from "node:crypto"
const MAX_IMPORT_BYTES = 50 * 1024 * 1024
function localImportKey(
organisationId: string,
reference: CanonicalReference,
resourceVersion: number,
): string {
return createHash("sha256").update([
organisationId,
reference.provider,
reference.type,
reference.id,
reference.rendition,
String(resourceVersion),
].join("|")).digest("hex")
}
async function importOne(
organisationId: string,
reference: CanonicalReference,
resolved: ResolvedReference,
): Promise<ImportResult> {
if (resolved.status !== "available") {
throw new ImportRejected("artwork_unavailable")
}
const resource = resolved.resource
const version = findVersion(resource, reference.rendition)
const key = localImportKey(organisationId, reference, resource.version)
const existing = await mediaStore.findByImportKey(key)
if (existing) {
return { ...reference, import_status: "reused", media_id: existing.id }
}
const grants = await collectionPost<AssetGrantResponse>(
"/v1/assets/signed-urls",
{
requests: [{
artwork_id: reference.id,
variant: reference.rendition,
}],
expires_in: 900,
},
)
const grant = requireMatchingGrant(grants, reference)
requireAllowedHttpsOrigin(grant.url, ASSET_ORIGINS)
// Deliberately use a new request with no Collection Authorization header.
const response = await fetch(grant.url, {
redirect: "error",
signal: AbortSignal.timeout(120_000),
})
if (!response.ok || !response.body) {
throw new ImportRejected("asset_download_failed")
}
if (response.headers.get("content-type")?.split(";", 1)[0] !== "image/jpeg") {
throw new ImportRejected("asset_not_jpeg")
}
const chunks: Buffer[] = []
let size = 0
for await (const chunk of response.body) {
const bytes = Buffer.from(chunk)
size += bytes.length
if (size > MAX_IMPORT_BYTES) throw new ImportRejected("asset_too_large")
chunks.push(bytes)
}
const image = Buffer.concat(chunks)
if (!image.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff]))) {
throw new ImportRejected("asset_not_jpeg")
}
const media = await mediaStore.saveImage({
importKey: key,
bytes: image,
width: version.delivery.width,
height: version.delivery.height,
name: resource.name,
})
return { ...reference, import_status: "imported", media_id: media.id }
}
<?php
use Illuminate\Support\Facades\Http;
const MAX_IMPORT_BYTES = 50 * 1024 * 1024;
function localImportKey(
string $organisationId,
array $reference,
int $resourceVersion,
): string {
return hash("sha256", implode("|", [
$organisationId,
$reference["provider"],
$reference["type"],
$reference["id"],
$reference["rendition"],
(string) $resourceVersion,
]));
}
function importOne(
string $organisationId,
array $reference,
array $resolved,
callable $collectionPost,
): array {
if ($resolved["status"] !== "available") {
throw new ImportRejected("artwork_unavailable");
}
$resource = $resolved["resource"];
$version = findVersion($resource, $reference["rendition"]);
$key = localImportKey($organisationId, $reference, $resource["version"]);
if ($existing = MediaStore::findByImportKey($key)) {
return [...$reference, "import_status" => "reused", "media_id" => $existing->id];
}
$grants = $collectionPost("/v1/assets/signed-urls", [
"requests" => [[
"artwork_id" => $reference["id"],
"variant" => $reference["rendition"],
]],
"expires_in" => 900,
]);
$grant = requireMatchingGrant($grants, $reference);
requireAllowedHttpsOrigin(
$grant["url"],
config("services.vieunite.asset_origins"),
);
// Separate client: no Collection Authorization header and no redirects.
$response = Http::withoutRedirecting()
->timeout(120)
->get($grant["url"]);
if (!$response->successful()) {
throw new ImportRejected("asset_download_failed");
}
if (strtok($response->header("Content-Type"), ";") !== "image/jpeg") {
throw new ImportRejected("asset_not_jpeg");
}
$image = $response->body();
if (strlen($image) > MAX_IMPORT_BYTES) {
throw new ImportRejected("asset_too_large");
}
if (substr($image, 0, 3) !== "\xFF\xD8\xFF") {
throw new ImportRejected("asset_not_jpeg");
}
$media = MediaStore::saveImage([
"import_key" => $key,
"bytes" => $image,
"width" => $version["delivery"]["width"],
"height" => $version["delivery"]["height"],
"name" => $resource["name"],
]);
return [...$reference, "import_status" => "imported", "media_id" => $media->id];
}
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.
@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
app.post(
"/v1/media/vieunite-collection/picker/selections/redeem",
requireMediaWriter,
async (req: MediaWriterRequest, res: Response) => {
const binding = await pickerSessions.claimOwned({
sessionId: req.body.session_id,
userId: req.user.id,
organisationId: req.user.organisationId,
})
if (binding.finalized) {
return res.set("Cache-Control", "no-store").json(binding.publicResult)
}
const { references, resolved } = await redeemAndResolve(
binding,
req.body.selection_code,
)
const resolvedByReference = indexResolvedResults(resolved)
const results: ImportResult[] = []
for (const reference of references) {
let result: ImportResult
try {
result = await importOne(
req.user.organisationId,
reference,
resolvedByReference.get(referenceKey(reference))!,
)
} catch (error) {
if (!(error instanceof ImportRejected)) throw error
result = {
...reference,
import_status: "failed",
media_id: null,
error_code: error.publicCode,
}
}
results.push(result)
await pickerSessions.checkpoint(binding.localId, results)
}
const publicResult = {
data: {
session_id: binding.sessionId,
selection: results,
import_summary: summarizeImports(results),
},
}
await pickerSessions.complete(binding.localId, publicResult)
res.set("Cache-Control", "no-store").json(publicResult)
},
)
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::middleware(["web", "auth", "media.write"])->post(
"/v1/media/vieunite-collection/picker/selections/redeem",
function (Request $request) use ($collectionPost) {
$binding = PickerSession::claimOwned(
$request->string("session_id")->toString(),
$request->user()->id,
$request->user()->organisation_id,
);
if ($binding->finalized) {
return response()->json($binding->public_result)
->header("Cache-Control", "no-store");
}
[$references, $resolved] = redeemAndResolve(
$binding,
$request->string("selection_code")->toString(),
$collectionPost,
);
$resolvedByReference = indexResolvedResults($resolved);
$results = [];
foreach ($references as $reference) {
try {
$result = importOne(
$request->user()->organisation_id,
$reference,
$resolvedByReference[referenceKey($reference)],
$collectionPost,
);
} catch (ImportRejected $error) {
$result = [
...$reference,
"import_status" => "failed",
"media_id" => null,
"error_code" => $error->publicCode,
];
}
$results[] = $result;
PickerSession::checkpoint($binding->local_id, $results);
}
$publicResult = ["data" => [
"session_id" => $binding->session_id,
"selection" => $results,
"import_summary" => summarizeImports($results),
]];
PickerSession::complete($binding->local_id, $publicResult);
return response()->json($publicResult)
->header("Cache-Control", "no-store");
},
);
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.
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
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