Build an extension

An extension is a small web page that runs inside ScorePlay. It appears in a place in the product, such as a card on the homepage, a panel on a collection page or a button in the media toolbar. ScorePlay sends it the data its manifest asks for, and it can ask ScorePlay to do a few things, such as show a message or open a page.

These docs cover version 1 of the extensions SDK. Read What an extension is first, then follow the quick start.

Building outside ScorePlay? Today extensions are built in ScorePlay's own repository, by ScorePlay and the partners we work with directly. A way to preview an extension from your own machine, and a process to send us an extension for review and publishing, are on the way. Until then, talk to us and we will set you up. Everything below about the manifest, the data, the SDK and tokens applies the same way.

What an extension is

Quick start

1. Create the folder

In the scoreplay-extensions repository, add a folder under app/extensions/ with two files:

app/extensions/my-extension/
  manifest.json
  index.html

2. Write the manifest

{
  "id": "acme.media-notes",
  "name": "Media notes",
  "version": "0.1.0",
  "publisher": "Acme",
  "sdk": 1,
  "surfaces": {
    "media.toolbar.actions": {
      "panel": "NotesPanel",
      "title": "Notes",
      "when": "media.type == 'video'"
    }
  },
  "panels": {
    "NotesPanel": { "entry": "index.html", "height": "auto" }
  },
  "context": ["media.id", "media.name"]
}

3. Write the panel

Load the SDK and the stylesheet with root-relative paths, so the same file works in every version folder:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <script src="/sdk/v1/bridge.js"></script>
    <link rel="stylesheet" href="/sdk/v1/panel.css" />
  </head>
  <body>
    <div class="sp-stack">
      <p id="name" class="sp-muted">Loading…</p>
      <button type="button" class="primary" id="save">
        Save note
      </button>
    </div>

    <script>
      ScorePlay.ready(context => {
        document.getElementById('name').textContent = context.media?.name ?? ''
      })

      document.getElementById('save').addEventListener('click', () => {
        ScorePlay.toast('Note saved', 'success')
      })
    </script>
  </body>
</html>

4. Preview it

cd app
pnpm install
pnpm dev

Open http://localhost:4100/__dev/preview.html and pick your extension. The page frames your panel the way ScorePlay does, sends it sample data and logs every request it makes. You do not need ScorePlay or a backend running. See Preview a panel.

5. Release it

  1. Open a pull request. The checks fail if the manifest is not valid or if a changed extension keeps its version.
  2. Merge it. CI publishes the new version to dev.scoreplay.build and scoreplay.build.
  3. Register the version in the catalog, allow the extension for a company and switch it on. See Versions and releasing.

Manifest reference

Field Type Required Notes
id string yes Lowercase letters and digits joined by single dots or hyphens, for example acme.media-notes. Never sdk or site. It cannot change.
name string yes Shown to users, for example as the panel title and on the company Extensions screen.
version string yes Semver without a v and without build metadata: 1.2.0, 1.2.0-beta.1. Bump it for every change.
publisher string no Shown as "By …" on the company Extensions screen.
sdk number no The SDK major you build against: 1.
surfaces object yes Surface id → contribution. See below.
panels object yes Panel id → panel. See below.
context string[] no The data fields the panel receives. See Surfaces and data.

A contribution (surfaces.<surface id>)

Field Type Notes
panel string Required. A key of panels.
title string The card title or the button label. Defaults to the panel title, then the extension name.
icon string Media toolbar only. The only named icon today is dubbing; any other value shows the extension icon.
order number Lower comes first. Default 100.
when string A condition that decides when the contribution shows. See when conditions.
height number or "auto" Overrides the panel height on this surface.

A panel (panels.<panel id>)

Field Type Notes
entry string Required. The HTML file, relative to your folder, for example index.html. It must stay inside your folder.
title string The frame title, and a fallback for the contribution title.
height number or "auto" "auto" (the default) grows the frame to fit the page. A number is a fixed height in pixels, at most 4000.
width number or string A number of pixels, or "480px" or "50%". The default is the full width.

When ScorePlay ignores something

ScorePlay checks every manifest when it loads it and writes warnings to the browser console, prefixed [extensions]:

The repository's build is stricter: it fails on a bad id or version, a missing entry file, or a surface that names an undeclared panel.

Surfaces and data

Surface What it is Data it offers
dashboard.widget A card on the homepage. None.
collection.detail.top A panel above the media grid on a collection page. collection.id, collection.name, collection.fullPath
media.toolbar.actions An icon button in the media details toolbar that opens the panel in a dialog. media.id, media.type, media.name, media.isGrowingMedia, media.scheduleId, media.fileFormat, media.width, media.height, media.duration, media.validated, media.labelId, media.isPrivate, media.aiRun, media.collectionId

Every surface also offers the user and the company: user.name, user.email, user.user_type, company.id and company.subdomain.

The rules

The data is fixed while the panel is open. When it changes, for example when the user moves to another media, ScorePlay reloads the panel with the new data.

when conditions

when decides whether a contribution shows. ScorePlay checks it every time the surface renders, against all of the surface's data, the user and the company, even fields your manifest does not declare.

media.type == 'video' && user.can('can_do_media_run_video_ai')
media.type == 'video' && media.isGrowingMedia == true
media.fileFormat == 'mxf' && media.validated == true
media.type == 'photo' && media.hasTag('Rights managed')
collection.id > 0

Permissions for user.can

These are the permissions most useful to an extension. Ask the platform team if you need another one.

Permission What it allows
can_do_media_download Download media
can_do_media_edit Edit photos
can_do_media_share Share media
can_do_media_delete Delete media
can_do_view_media_tags View tags
can_do_media_edit_tag Add tags to media and remove them
can_clip_video Clip video
can_do_media_run_video_ai Run video AI
can_do_media_view_transcription View transcriptions
can_view_events View collections
can_do_admin_event_edit Edit collections
can_do_upload_upload Upload
can_edit_extensions Switch extensions on and off

SDK reference

Load bridge.js from /sdk/v1/bridge.js. It adds window.ScorePlay.

ScorePlay.ready(callback)

Calls callback(context) once, when ScorePlay has sent the data. If the data has already arrived, it calls it straight away. Start your panel here.

ScorePlay.ready(context => {
  render(context.media)
})

ScorePlay.toast(message, severity)

Shows a message in ScorePlay, titled with your extension's name. message is at most 200 characters. severity is success (the default), info, warning or error.

ScorePlay.open(url)

Opens a URL in a new tab. A sandboxed panel cannot open links itself, so use this for every link.

ScorePlay.navigate({ to, id })

Moves the current ScorePlay tab to a page, without reloading the app:

to Goes to
media The media with that id
collection The collection with that id

id is a whole number above 0. In the media toolbar dialog, the dialog closes after navigating.

ScorePlay.close()

Closes the dialog. It works in the media toolbar only; elsewhere there is nothing to close.

ScorePlay.getToken()

Returns a promise of a new token (a string), or null if ScorePlay could not create one. See Tokens and calling an API.

Size

With "height": "auto", bridge.js reports your page's height whenever it changes, and ScorePlay sizes the frame to fit, up to 4000 pixels. The frame stays hidden until the first report, so always load bridge.js. Do not set html or body to height: 100%.

Theme

ScorePlay sends its theme to the panel, and bridge.js sets it as --sp- CSS variables on <html>, with data-theme="light" or "dark". The theme follows the company's brand colour and the user's dark mode.

panel.css (/sdk/v1/panel.css) styles a panel like ScorePlay using those variables: text, buttons, fields, cards, badges, alerts, tabs, lists, empty states and loading states. It is optional. The style guide shows every class and every variable.

Within SDK v1, variables and classes are only added, never renamed or removed. Their values may change.

Tokens and calling an API

Never put a secret in an extension

Everything on scoreplay.build is public. Anyone can open your files, and anything a browser runs can be read in its developer tools. So:

To call an API that needs a key, call your own backend, which keeps the key.

How the token works

  1. The panel calls ScorePlay.getToken().
  2. ScorePlay creates a token for the signed-in user, the company and your extension. It lasts 5 minutes.
  3. The panel sends it to your backend: Authorization: Bearer <token>.
  4. Your backend checks the token. If it is valid, your backend calls the API it needs with its own key.

ScorePlay also sends a first token in the data, as context.token, but it may have expired by the time you use it. Call getToken() before each call, or when your backend answers 401.

Checking a token

The token is a JWT signed with RS256. Check it against ScorePlay's public keys:

Public keys (dev) https://media.dev.scoreplay.io/.well-known/jwks.json
Public keys (prod) https://media.scoreplay.io/.well-known/jwks.json

Check all of these:

Then use:

A valid token is not permission

A token proves who is asking: this user, in this company, through this extension. It does not prove they may do what they ask. Anyone who can open your panel can also take its token and call your backend directly, so:

Calling your backend from a panel

The panel runs in a sandbox, so its requests come from origin null.

const callBackend = async (path, token) =>
  fetch(`https://backend.example.com${path}`, {
    credentials: 'omit',
    headers: { Authorization: `Bearer ${token}` },
  })

let response = await callBackend('/notes', await ScorePlay.getToken())
if (response.status === 401) {
  response = await callBackend('/notes', await ScorePlay.getToken())
}

Your backend must:

Which hosts a panel can call

Images and fonts

A panel can show images from its own folder, from https://*.scoreplay.io (which includes the ScorePlay CDN, cdn.scoreplay.io and cdn.dev.scoreplay.io) and from data: URLs. Images on other hosts are blocked, including media that a company keeps in its own storage.

Fonts load from scoreplay.build only. panel.css already loads the ScorePlay fonts.

What a panel cannot do

The sandbox and the security headers on scoreplay.build mean a panel:

A panel opened directly in a browser tab gets no data and no token, so it can do nothing.

Versions and releasing

Preview a panel

With pnpm dev running, open http://localhost:4100/__dev/preview.html:

Troubleshooting

My extension does not appear

My panel is blank

A request to my backend fails