0+ MUSIC VIDEOS
    Partner iframe / embed API · v2

    Partner Embed (SSO) integration guide

    Drop the Music Video Marketplace Workspace into your own product as an iframe, with your users already signed in. Your server signs a small JSON payload describing the logged-in user with your client_secret (HMAC-SHA256); we verify it, link or provision the matching MVM account, and boot the embed under your branding. No user-facing login step, no password sharing.

    At a glance

    1. Your backend builds a JSON payload for the current user.
    2. Your backend signs the raw JSON string with HMAC-SHA256 + client_secret.
    3. Your frontend loads the embed URL with the base64 payload and lowercase hex signature.
    4. We verify, create a 12-hour session, and render the Workspace with your logo + accent colour.

    Sign server-side only. Your client_secret must never reach a browser.

    1. Credentials & provisioning

    CredentialDescriptionWhere it is used
    client_idYour unique partner identifier (32-char hex). Not secret.Embed URL path + request bodies
    client_secretHMAC signing key.Server-side signing only — never expose to browsers

    Both values are issued by Music Video Marketplace during onboarding — there is no public signup and no self-serve credential rotation. Email info@musicvideomarketplace.com to request credentials, to have a secret rotated immediately if it leaks, or to change your embed_mode or stored branding. Your partner record also carries your account status (only active partners can open sessions), partner_type, a monthly per-user download limit, and any partner-wide auto-discount.

    2. Signing the payload (HMAC-SHA256)

    1. Serialise the payload once into a JSON string. Keep that exact string.
    2. Sign the raw JSON string (not the base64 form) with HMAC-SHA256 and your client_secret.
    3. Hex-encode the digest, lowercase.
    4. Base64-encode the same raw JSON string for the URL, then URL-encode it.

    Worked example (Node)

    const crypto = require('crypto');
    
    const CLIENT_ID = 'pk_partner_demo_0000000000000000';
    const CLIENT_SECRET = process.env.MVM_CLIENT_SECRET;   // server-side only
    
    // 1. one canonical JSON string — sign and send exactly this
    const payloadString = JSON.stringify({
      partner_external_user_id: 'your-user-123',
      email: 'jane@example.com',
      display_name: 'Jane Doe',
      timestamp: new Date().toISOString(),
    });
    
    // 2. lowercase hex HMAC of the RAW string
    const sig = crypto.createHmac('sha256', CLIENT_SECRET)
      .update(payloadString)
      .digest('hex');
    
    // 3. base64 of the SAME raw string, URL-encoded
    const b64 = Buffer.from(payloadString, 'utf8').toString('base64');
    
    const embedUrl =
      `https://musicvideomarketplace.com/partner/embed/${CLIENT_ID}` +
      `?payload=${encodeURIComponent(b64)}&sig=${sig}`;

    Same thing in Python

    import base64, hmac, hashlib, json, urllib.parse
    from datetime import datetime, timezone
    
    payload_string = json.dumps({
        "partner_external_user_id": "your-user-123",
        "email": "jane@example.com",
        "display_name": "Jane Doe",
        "timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
    }, separators=(",", ":"))
    
    sig = hmac.new(CLIENT_SECRET.encode(), payload_string.encode(), hashlib.sha256).hexdigest()
    b64 = base64.b64encode(payload_string.encode()).decode()
    embed_url = f"https://musicvideomarketplace.com/partner/embed/{CLIENT_ID}?payload={urllib.parse.quote(b64)}&sig={sig}"

    Byte-for-byte matters. Re-serialising the object on the way out (different key order, different spacing, a pretty-printer, a trailing newline) changes the bytes and the signature no longer matches. Compare case-insensitively on our side is handled — we lowercase your signature before comparing — but everything else must match exactly.

    3. Payload fields

    {
      "partner_external_user_id": "your-user-123",
      "email": "user@example.com",
      "display_name": "Jane Doe",
      "timestamp": "2026-09-02T13:45:00.000Z",
      "logo_url": "https://cdn.yourbrand.example/logo.svg",
      "accent_color": "#1D4ED8"
    }
    FieldRequiredDescription
    partner_external_user_idRequiredYour platform's stable, unique ID for the currently signed-in user. Missing → 400. This is the identity key for the whole session: it is upserted against (partner_id, partner_external_user_id), so re-using one value across users merges them into a single MVM account and they will see each other's purchases and projects.
    emailStrongly recommendedUsed to link an existing MVM account (matched case-insensitively) or auto-provision a new passwordless, email-confirmed one. Without it we cannot resolve an internal user, internal_user_id comes back null, and ownership / purchase history cannot be attributed.
    display_nameOptionalDisplay name stored on the link record and shown in the embed.
    timestampOptional, strongly recommendedISO 8601 (e.g. 2026-09-02T13:45:00.000Z). Must be within ±5 minutes of our server clock, otherwise 403 Payload expired. If the field is omitted entirely the payload never expires, which makes a leaked URL replayable indefinitely — always send it.
    logo_urlOptionalAbsolute http(s) logo URL. Overrides your stored logo for this session.
    accent_colorOptional6-digit hex, leading # (e.g. #1D4ED8). Overrides your stored accent for this session.

    Unknown extra keys are ignored (they are still covered by the signature). Entitlement cannot be asserted in the payload — see “Entitlement model” below.

    4. Embedding the iframe

    https://musicvideomarketplace.com/partner/embed/{CLIENT_ID}?payload={BASE64_JSON}&sig={HEX_HMAC}

    payload is base64 of the signed JSON string, URL-encoded. sig is the lowercase hex HMAC of the raw JSON string. Both are required; without them the embed renders a “missing session” state and no session is created. If payload is not valid base64 we fall back to treating it as the raw JSON string, so sending raw JSON also works — base64 is preferred because it survives URL encoding cleanly.

    Recommended host page pattern

    <iframe
      id="mvm-embed"
      title="Music Video Marketplace Workspace"
      style="width:100%;height:100%;min-height:720px;border:0;border-radius:12px;background:#0F0F10"
      allow="autoplay; fullscreen; clipboard-write"
    ></iframe>
    <script>
      // your endpoint, your auth — it mints a fresh signed URL per load
      fetch('/api/mvm-embed-url')
        .then((r) => r.json())
        .then(({ embedUrl }) => { document.getElementById('mvm-embed').src = embedUrl; });
    </script>

    Sizing & responsiveness

    • The embed is a full-application surface, not a widget: it lays itself out to 100dvh of the iframe viewport and scrolls internally. Give the iframe a real height — a flex/grid cell that fills your page, or an explicit height. Do not rely on content-based auto-height; the iframe will collapse.
    • Minimum usable height is about 720px for the Workspace editor (timeline + preview + panels). Below roughly 620px height or 480px width the embed switches to a compact layout, and below 420px height / 320px width to a “tiny” layout; the editor timeline is cramped at those sizes.
    • We never resize your iframe and never post height messages to your page — sizing is entirely yours. Full-screen-style surfaces such as export history are rendered as modals inside the iframe, so they need no host cooperation and no extra height.
    • allow="autoplay; fullscreen; clipboard-write" is required for video playback, full-screen preview and copy-to-clipboard actions to work.
    • Do not sandbox the frame without allow-scripts allow-same-origin allow-popups allow-forms; the Workspace needs script execution, its own origin storage, and popups for checkout.

    5. embed_mode — catalog vs workspace

    Your partner record carries an embed_mode column with two values. It is returned as embed_mode in the verification response and decides what the embed renders after sign-in.

    ModeWhat the user getsChoose it when
    workspace
    default for new partners
    Lands straight in the full-height Workspace editor with no video pre-selected (a “sourceless” boot). Catalogue search, filtering, song-matching, clip drag-in, saved projects, export history and checkout all happen inside the editor. Host chrome is deliberately absent so the editor keeps the full iframe height.You want one surface that does everything, and your users are creating edits — this is the mode we develop against and the one to pick unless you have a reason not to.
    catalogThe legacy flow: a browse/recommendations landing page, a video detail view, a checkout view, a purchases view, and the editor opened per video from there. View state persists in the iframe's sessionStorage per user identity.You mainly want catalogue discovery and purchase inside your product, with editing as a secondary step.

    Configuration is admin-side, not self-serve. There is no partner UI and no payload field for it — partners.embed_mode is set by Music Video Marketplace on request, and it cannot be overridden per session or per user from your side. Ask us to switch it; the change takes effect on the next session.

    6. Branding

    logo_url and accent_color are part of the signed payload, so they are covered by the same HMAC signature as the identity fields and cannot be injected from a browser without your client_secret. There is no separate branding signature and no separate branding endpoint.

    Resolution order, evaluated per field, per session:

    1. Signed payloadlogo_url / accent_color, when present and valid.
    2. Stored configpartners.branding_config.logoUrl / .primary, set by your partner manager.
    3. Default — no partner branding (null).

    Validation

    • logo_url must parse as a URL with an http: or https: protocol. Use https.
    • accent_color must match /^#[0-9a-fA-F]{6}$/. Shorthand (#abc), named colours and rgb() are rejected. Accepted values are normalised to uppercase.
    • Invalid values are ignored, never fatal: the session still boots and falls through to the stored value, then to default. A malformed colour can never lock your users out.

    The verification response echoes what was actually used in branding_source — the fastest way to confirm your fields were accepted:

    "branding_logo_url": "https://cdn.yourbrand.example/logo.svg",
    "branding_accent_color": "#1D4ED8",
    "branding_source": { "logo": "payload", "accent": "branding_config" }
    ValueMeaning
    payloadTaken from your signed payload (present and valid).
    branding_configPayload omitted it or it failed validation — your stored config was used.
    noneNeither available — no partner branding applied.

    Separately, branding_config.mode (returned as branding_mode) selects the theme treatment: branded (MVM look), clean (neutral, no MVM gradient) or whitelabel (theme derived from your accent). Like embed_mode, this is admin-configured.

    7. Verification endpoint

    The embed calls this for you on load. Call it directly only to test a signature from your own backend, or to pre-flight credentials in CI.

    POST https://awfrtgkoramnvpsvrjjv.supabase.co/functions/v1/partner-verify-session
    Content-Type: application/json
    
    {
      "client_id": "pk_partner_demo_0000000000000000",
      "payload": "{\"partner_external_user_id\":\"user-123\",\"email\":\"user@example.com\",\"timestamp\":\"2026-09-02T13:45:00.000Z\"}",
      "signature": "<lowercase hex hmac of that exact payload string>"
    }

    Here payload is the raw JSON string you signed (a JSON object is also accepted and re-serialised, which will usually break the signature — send the string). Base64 is only used in the iframe URL parameter.

    200 OK

    {
      "session_token": "6d638412...cef6",              // opaque, send in request bodies
      "session_expires_at": "2026-09-03T01:47:56.107Z",// 12h from creation, extended on activity
      "linked_user_id": "5f521ef7-...",                // partner_linked_users.id
      "internal_user_id": "4a96ecc1-...",              // resolved MVM auth user, or null
      "partner_id": "acec8b73-...",
      "partner_name": "Rights Hub",
      "embed_mode": "workspace",                       // "workspace" | "catalog"
      "branding_logo_url": null,
      "branding_accent_color": null,
      "branding_source": { "logo": "none", "accent": "none" },
      "partner_type": "purchase",                      // "purchase" | "platform"
      "monthly_download_limit": 1,
      "branding_mode": "whitelabel",                   // "branded" | "clean" | "whitelabel"
      "discount_percentage": 0                         // display only; price is recomputed server-side
    }

    Sessions last 12 hours and are extended on activity. Every subsequent partner API call passes session_token in the JSON body (not as a header), and every call that grants value re-validates the token against the database, so a revoked or expired session stops working immediately even mid-edit.

    8. Error responses

    Every failure is JSON { "error": "<message>" } with a meaningful status code. The strings below are exact and stable — branch on status first, message second. All of these were verified against the live endpoint.

    StatuserrorCause / fix
    400Missing client_id, payload, or signatureOne of the three body fields is absent or empty.
    400Missing partner_external_user_id in payloadSignature was valid but the payload has no user id. Always send a per-user id.
    403Invalid client_idNo partner matches that client_id. Check the URL path segment.
    403Partner is not activeYour partner record is suspended or not yet activated — contact us.
    403Invalid signatureHMAC mismatch: wrong secret, signed the base64 instead of the raw JSON, base64 instead of hex digest, or the JSON string was re-serialised after signing.
    403Payload expiredtimestamp is more than 5 minutes from our server clock, in either direction.
    500Failed to create user link / Failed to create sessionServer-side persistence failure. Retry once; if it repeats, contact us.
    500Internal server errorUnhandled failure (including malformed JSON in the request body).

    Note that an invalid signature returns 403, not 401, and identity resolution failures are non-fatal: if we cannot resolve or create an MVM auth user from the email, the session is still returned with internal_user_id: null rather than an error.

    9. Entitlement model

    Entitlement is resolved host-side and is authoritative. A partner cannot grant it. There is no payload field, no header and no session flag that makes a video owned. Every resolution re-derives ownership from our own records — completed orders, membership claims, per-video add-on grants, admin overrides — keyed to the MVM user your payload resolved to. The editor inside the iframe treats what the host sends as final and re-asks at export time; it never decides ownership itself.

    What a non-entitled user actually sees today

    • They can search the whole catalogue, open any video, and pull clips onto the timeline. Nothing is hidden from browsing because of entitlement.
    • Media for an unowned clip is served at preview grade (quality: "preview", the same preview the public site plays). No master and no signed download URL is minted — s3Url is null. Bucket/key identifiers are still passed for scene-cut lookup only, so a preview clip can still be split on real cut points.
    • Editing is fully unlocked, including multi-clip edits. The purchase decision is deferred to export time: on export we quote every timeline source the user has not paid for and the export is blocked until those are bought.
    • In workspace mode the purchase runs as a modal checkout over the embed, one video at a time in a queue; each success unlocks that source immediately without reloading the editor. A user with no owned sources sees export disabled with a purchase CTA rather than an error.
    • A video that has no preview at all, no source file, or is unavailable/sold-out comes back as { status: "unavailable", reason } and cannot be added — the reason string is shown to the user.

    Practical consequence for you: never build UI that promises a download based on your own records. Ask, then render what we return.

    10. The postMessage contract

    Read this first: the message contract below is between our embed page (the host, running inside your iframe) and the Workspace editor iframe it mounts. Your host page implements none of it. We do not post messages to your page and we do not listen for messages from it — your only integration surface is the signed embed URL. It is documented here because partners embedding the Workspace ask what the surface does, and because it defines exactly which actions are available in each mode.

    Editor → host

    TypeWhen it firesPayload (abridged)Host behaviour
    EDITOR_READYEditor finished booting.Host replies with VIDEO_DATA.
    EDITOR_VIDEO_DATA_ACKEditor accepted a VIDEO_DATA.{ … } diagnosticLogged; handshake considered complete.
    EDIT_COMPLETEAn edit render finished in-editor.edit resultForwarded to the host callback.
    EXPORT_COMPLETEAn export finished.{ exportId, … }Host persists it and replies EXPORT_COMPLETE_ACK with ok.
    STATUS_UPDATEProgress/status text changes.{ status }Surface progress.
    PROJECT_SAVEDProject saved.{ projectId, projectName }Refresh the project list.
    ADD_TO_CARTUser adds an edit to cart.{ videoId, projectId?, projectName? }Consumer site only.
    REQUEST_PURCHASEExport blocked by unpaid sources.{ videoId, videoIds?, item?, price?, reason }Opens checkout; in partner mode a modal queue over the embed.
    REQUEST_VIDEO_PURCHASEPurchase of a specific video requested.{ videoId }Opens checkout for that video.
    NAVIGATE_TO_PURCHASEEditor wants the purchase page.{ videoId }Host navigates / opens checkout.
    NAVIGATE_TO_UPGRADEUpgrade CTA pressed.Consumer site only.
    NAVIGATE_TO_ACCOUNTAccount CTA pressed.Suppressed for partners (accountNavigation: false).
    REQUEST_EXIT_EDITORUser picks “exit editor”.{ videoId?, projectId?, hasUnsavedChanges?, reason? }Suppressed for partners (canExit: false) — the embed is the page.
    REQUEST_OPEN_PROJECTUser picks a saved project / related video.{ videoId?, projectId?, hasUnsavedChanges? }Host re-boots the editor on that project.
    REQUEST_CATALOG_SEARCHCatalogue panel search / song match.{ requestId, mode: "text"|"similar"|"song", query, videoId, limit, filters, sort, cursor, audioProfile? }Answers CATALOG_SEARCH_RESULTS or CATALOG_SEARCH_ERROR. Rate limited ~10 searches / 10s.
    REQUEST_CATALOG_FACETSFilter UI opens.{ requestId }Answers CATALOG_FACETS (categories, energy, atmosphere, orientation).
    REQUEST_CATALOG_COLLECTIONSCollections panel opens.{ requestId }Answers CATALOG_COLLECTIONS.
    REQUEST_ADD_VIDEOClip added / dropped / previewed / project reopened.{ requestId, videoId, intent: "add"|"preview"|"drop"|"open" }Answers VIDEO_SOURCE — master only when entitled, else preview grade.
    REQUEST_CATALOG_STATUSEditor re-checks availability/ownership of timeline sources.{ requestId, videoIds[] }Answers CATALOG_STATUS.
    REQUEST_PURCHASE_QUOTEExport gate needs prices.{ requestId, videoIds[] }Answers PURCHASE_QUOTE (per-item purchaseRequired).
    REQUEST_COLLECTION_QUOTECollection bundle pricing.{ requestId, videoIds[], projectId? }Answers COLLECTION_QUOTE or COLLECTION_QUOTE_ERROR.
    REQUEST_TOKEN_REFRESHEditor JWT near expiry / rejected.Host mints a fresh token from the partner session token and replies TOKEN_REFRESHED.
    OPEN_PARTNER_EXPORTS“Your exports” pressed in the editor top bar (partner sessions).Host opens export history as a modal inside the iframe, so the editor keeps full height.
    REPORT_VIDEO_ISSUEEditor cannot use a source (e.g. no usable master).{ videoId, title?, artist?, reason, hadVimeoUrl, hadS3, reportedAt? }Host logs it server-side and re-verifies the source — no user action needed.

    Host → editor

    TypeWhenPayload
    VIDEO_DATAOn EDITOR_READY, and again whenever boot data changes (token arrives, source presigned, ownership changes).See shape below.
    TOKEN_REFRESHEDAnswer to REQUEST_TOKEN_REFRESH, or when a token first lands after handshake.{ authToken, accessToken }
    PURCHASE_COMPLETECheckout resolved (success / failed / cancelled).{ videoId, status, canExport, ownsSocialClips, ownedVideoIds[], error? }
    EXPORT_COMPLETE_ACKAfter the host persists an EXPORT_COMPLETE.{ exportId, ok }
    RESET_PROJECTHost wants a blank project for the current video.{ videoId }
    Bridge answers: CATALOG_SEARCH_RESULTS, CATALOG_SEARCH_ERROR ({ requestId, code, message }), CATALOG_FACETS, CATALOG_COLLECTIONS, VIDEO_SOURCE, CATALOG_STATUS, PURCHASE_QUOTE, COLLECTION_QUOTE, COLLECTION_QUOTE_ERROR — each echoes the requestId it answers.

    VIDEO_DATA shape (partner session)

    {
      "type": "VIDEO_DATA",
      "payload": {
        "videoId": "",                  // empty in a sourceless workspace boot
        "vimeoUrl": "", "s3Url": "",    // keys always present; empty until a source is picked
        "s3Bucket": "…", "s3Key": "…",  // lookup identifiers only (scene cuts)
        "sessionMode": "catalog",       // "video" | "catalog" (catalog = no source yet)
        "hasSource": false,
        "userId": "<mvm auth user id>",
        "metadata": { "title": "", "artist": "", "duration": 0, "width": 1920, "height": 1080 },
        "currentUser": { "id": "…", "email": "…" },
        "permissions": {
          "canExport": false,
          "entitlementSource": "none",  // membership | purchase | addon | admin | none
          "hidePurchaseCta": false,
          "exportCtaLabel": "Purchase to export"
        },
        "ownedVideoIds": [],            // authoritative per-source purchase entitlement
        "ownership": "preview",         // "owned" | "preview" for the selected video
        "deliverables": { /* per-deliverable entitlement + price */ },
        "authToken": "<short-lived HS256 JWT>",
        "accessToken": "<same>",
        "projects": [], "relatedVideos": [],
        "capabilities": {
          "canSwitchProject": true, "canSwitchVideo": true,
          "canExit": false,           // partner embeds are the whole page
          "catalogSearch": true, "catalogPreviewSources": true, "catalogSongMatch": true,
          "exportPurchaseGate": true,
          "addonDeliverables": false, // partner checkout carries no add-ons yet
          "accountNavigation": false
        },
        "partner": {
          "partnerId": "…", "name": "Rights Hub",
          "logoUrl": null, "accentColor": null,
          "embedMode": "workspace"
        },
        "hostMode": "partner"           // "consumer" on the main site
      }
    }

    The partner object is presentational and informational only — every server call re-derives partner identity from the opaque session token, never from this payload. Likewise ownedVideoIds / permissions are produced by the host from our records; the editor consumes them, it does not assert them.

    Origin validation

    • The host rejects any inbound message whose event.origin is not the editor origin, and logs the rejection. Messages from any other frame are dropped before the type is inspected.
    • Every outbound message is posted with an explicit targetOrigin (the editor origin captured at handshake) — never "*".
    • On your side: if you add a message listener on the page hosting our iframe, check event.origin === "musicvideomarketplace.com" before trusting anything. We currently send you nothing, so a message claiming to come from us is a red flag.

    11. Common integration pitfalls

    Identity mismatch between your payload and your own token issuer

    This is the single most common cause of “it works but everything is empty”. The signed payload's partner_external_user_id (and email) must be the real signed-in end user, and it must be the same subject your own token-issuing / API-calling code uses. If your embed endpoint signs the real user while a service account, QA fixture, or hardcoded “integration user” is used elsewhere in your stack, the session resolves to a different MVM account than the one your other calls act on. Nothing errors: the embed boots, branding is right, the catalogue loads — but purchases, projects and export history belong to the other identity, so the user sees an empty library or an unexpected “not licensed” state on something they just bought.

    Self-diagnosis: read linked_user_id and internal_user_id from the verification response and log them next to your own user id. If two different end users produce the same internal_user_id, or one user produces different ones across loads, your payload identity is not stable. Fix it at the source — do not compensate in the UI.

    Clock skew rejecting valid payloads

    timestamp is compared with an absolute ±5 minute window, so a host clock that is ahead fails just as hard as one behind, and 403 Payload expired looks identical to an expired link. Run NTP on the signing host, generate the timestamp at signing time (not at request time from a cached value), and always emit UTC ISO 8601 with an explicit offset — a local-time string with no zone is interpreted as UTC and silently shifts by your offset.

    Caching or reusing a signed URL

    Each signed URL is good for at most 5 minutes and creates a new session row when used. Never cache the embed URL in your CDN, HTML, or app state — mint it per iframe load from your own endpoint. Reloading a page with a stale URL is the second most common source of 403 Payload expired.

    Signing the wrong bytes

    403 Invalid signature almost always means one of: you signed the base64 instead of the raw JSON; you sent a base64 digest instead of lowercase hex; your HTTP client re-serialised the JSON object (key order/spacing changed) after signing; or the payload was mutated after signing (adding timestamp last is a classic). Build the string once, sign the string, send the string.

    Sharing one identity across users

    Because linkage is keyed on (partner_id, partner_external_user_id), a constant value (your admin id, "user", a tenant id) merges all your users into one MVM account with shared purchases and projects. Similarly, two different external ids with the same email resolve to the same MVM auth user, because email is the linkage key for ownership.

    Iframe with no height, or an over-restrictive sandbox

    A collapsed or 400px-tall iframe makes the Workspace look broken rather than erroring. Give it real height (see §4). A sandbox attribute missing allow-same-origin breaks session storage; missing allow-popups breaks checkout.

    Expecting entitlement to follow your own records

    Sending a “premium” flag, a plan name, or an owned-video list in the payload does nothing — those keys are ignored. Entitlement comes only from MVM orders, memberships and grants (see §9).

    12. Checklist before go-live

    • client_secret lives only in server-side config, never in client JS or a public repo.
    • Payload carries the end user's stable id and email — verified stable across loads and distinct per user.
    • timestamp included, generated at signing time, host clock NTP-synced.
    • Embed URL minted per load; never cached.
    • Branding confirmed via branding_source in the verification response.
    • Iframe has real height (≥720px), allow="autoplay; fullscreen; clipboard-write".
    • Error handling branches on status + error string (§8), with a retry path for 500.
    • embed_mode confirmed with us (workspace unless you need the legacy catalog flow).

    Questions this page does not answer: info@musicvideomarketplace.com. Support is staffed Monday–Friday, 9–5 UK time.

    What's changed

    v2 · 2026-09-03

    Behavioural and contract changes to the embed since launch: embed modes, rolling sessions, self-serve branding in the signed payload, stricter identity handling, and entitlement-gated media resolution.

    • embed_mode introduced (catalogue vs workspace): the verification response now returns embed_mode and the embed renders the catalogue or the full Workspace accordingly.
    • Sessions became rolling: an active session is extended to at least 12 hours on use, and the verification response now returns session_expires_at so hosts can see when a session lapses.
    • Self-serve branding: logo_url and accent_color may now be included in the signed payload and take precedence over the MVM-managed branding_config. Values are validated (http(s) URL / hex colour) and silently ignored if malformed.
    • Test and non-production email domains (example.com, *.test, *.invalid, *.local, mailinator.com and similar) are now rejected outright with HTTP 400 and can never create or rebind an account link.
    • An established link's internal_user_id can no longer be silently overwritten: if a later payload carries a different email for the same partner_external_user_id, the established identity is kept, only last_seen_at is refreshed, and the mismatch is logged. Re-using an external user ID for a different person no longer merges accounts.
    • Session handling hardened on the embed: a signed payload for a different identity now discards the cached session and forces fresh verification, transient network failures are retried (server-side rejections such as an invalid signature are not), and the Workspace shows an explicit reconnect state instead of rendering without credentials.
    • Editor source resolution split: the response now always returns the source bucket/key for scene lookup, plus a new entitled boolean. The presigned media URL is only issued when entitled is true; unentitled videos return a null URL, and unauthenticated calls are rejected with 401.
    • A Workspace in active use is no longer auto-reloaded by a platform deployment: pending updates are deferred while the editor iframe is focused, so in-progress edits are not interrupted.
    Full version history

    v2 · 2026-09-03 view this version download

    Behavioural and contract changes to the embed since launch: embed modes, rolling sessions, self-serve branding in the signed payload, stricter identity handling, and entitlement-gated media resolution.

    v1 · 2026-09-02 view this version download

    First published version of the partner iframe / embed contract, covering the integration as originally shipped: client_id + client_secret credentials, HMAC-SHA256 signed payloads, the embed URL and the session handshake.

    Download this version (HTML, print to PDF)