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
- A web page in a sandbox. Each extension is an HTML page, served from
scoreplay.build, inside a sandboxed frame. It has no access to the rest of ScorePlay, to cookies or to the signed-in session. - Surfaces. A surface is a place in the product where extensions can appear. There are three: the homepage dashboard, the top of a collection page and the media toolbar.
- Panels. A panel is one HTML page of your extension. Each surface you contribute to shows one of your panels.
- The manifest.
manifest.jsonsays which surfaces your extension appears on, which panel each one shows, when it shows, and which data it receives. - Data. ScorePlay sends a panel only the fields its manifest declares, for example the media name. Nothing else is shared.
- Requests. A panel asks ScorePlay to do things through
bridge.js: show a toast, open a link, go to a ScorePlay page, close itself or give it a token. - Tokens. A panel can ask for a token that lasts 5 minutes and proves which company and which extension the request comes from. Your own backend checks it.
- Versions. Each version of an extension is published once and never changes. ScorePlay decides which version each company uses.
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
- Open a pull request. The checks fail if the manifest is not valid or if a changed extension keeps its version.
- Merge it. CI publishes the new version to
dev.scoreplay.buildandscoreplay.build. - 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 whole extension is skipped when
id,nameorversionis missing, whenidis not the extension's registered key, or whensurfacesorpanelsis not an object. - A contribution is dropped when its surface is unknown, its
paneldoes not exist or itswhendoes not parse. - A field with a wrong value (for example a
heightof0) is dropped and the rest of the manifest is kept.
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.
collection.idandmedia.idare numbers.collection.fullPathis the collection's breadcrumb path.media.typeis one ofvideo,photo,application,audio,file,folderorclip. Write'photo', never'image'.media.isGrowingMediaistruewhile a live recording is still being written andfalseotherwise.media.scheduleIdis the id of the stream schedule that recorded the media, ornull. When a recording ends,isGrowingMediaturnsfalseand ScorePlay checks everywhencondition again: a contribution whose condition is now false hides, and its open panel closes. An open panel that declaresmedia.isGrowingMediaincontextalso closes, because its data changed.media.fileFormatis the original file's format in lowercase, such as'mxf'or'mp4', ornullwhen ScorePlay does not know it.media.widthandmedia.heightare the size in pixels: the video's, or the original file's for a photo.media.durationis the video's length in whole seconds. Each isnullwhen unknown.media.validated,media.isPrivateandmedia.aiRunare alwaystrueorfalse.aiRunistrueonce ScorePlay's AI has processed the media.media.labelIdandmedia.collectionIdare numbers, ornullwhen the media has no label or is in no collection.- A field ScorePlay does not have is
nullor left out.
The rules
- A panel receives only the fields its manifest declares.
"context": ["media.id", "media.name"]sends{ "media": { "id": 123, "name": "Goal.mp4" } }. - A path can select a group.
"user"sendsuser.name,user.emailanduser.user_type. - Without
context, a panel receives the id of the surface's object only:{ "collection": { "id": … } }on a collection page,{ "media": { "id": … } }in the media toolbar, and{}on the dashboard. "context": []sends{}.- User and company fields are personal data. They are sent only when declared, and the company's admins see every declared field before they switch an extension on.
- A field your surfaces do not offer is ignored with a warning.
- ScorePlay also adds
tokento the data. See Tokens and calling an API.
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
- Comparisons:
==,!=,<,<=,>,>=.===and!==mean the same as==and!=. Equality is strict:'12'is not equal to12.<,<=,>and>=are false unless both sides are numbers. - Combining:
&&and||.&&comes before||. There is no!and no grouping with parentheses. - Values: strings in single or double quotes (no escapes), numbers,
true,false,null, and dotted paths such asmedia.type. - A path on its own reads as true or false:
collection.fullPathis true when it has a value. - A missing path reads as
null. user.can('permission')is true when the signed-in user has that permission. It takes exactly one argument.media.hasTag('name')is true when the media has a tag with that name, ignoring case. It works onmedia.toolbar.actionsonly. Hidden tags, players and sponsors never match. Onlywhencan see tags: a panel cannot declare them incontext.- Errors hide the contribution. A condition that does not parse is dropped when the manifest loads. One that fails while it runs (for example
media.can('x')) hides the contribution. Both log a warning.
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.
- The URL must start with
https://, or be a ScorePlay path such as/media. - ScorePlay refuses sign-in and account pages, and any URL whose query mentions
tokenorcompany_id.
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:
- No API keys, access tokens, passwords or signing keys in an extension, not even for test accounts.
- No customer data and no customer names in files, ids or manifests.
To call an API that needs a key, call your own backend, which keeps the key.
How the token works
- The panel calls
ScorePlay.getToken(). - ScorePlay creates a token for the signed-in user, the company and your extension. It lasts 5 minutes.
- The panel sends it to your backend:
Authorization: Bearer <token>. - 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:
- The signature, using the key whose
kidmatches the token's header. Keys can rotate, so cache the key set for at most an hour. ississcoreplay.audcontains your extension'sid.exphas not passed.
Then use:
org: the company id. Only return that company's data.sub: the id of the user who opened the panel.
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:
- Check permission on your backend for every request. Decide whether user
subin companyorgmay read or change that resource, the same way you would without the panel. Do not let your backend's API key do something the user could not do in ScorePlay. whenconditions are not security. They only decide whether ScorePlay shows the panel. A user can get a token and call your backend even if yourwhencondition would hide the panel from them.- Never trust ids from the request alone. A media or collection id sent by the panel can be changed. Check that it belongs to company
orgbefore you use it.
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:
- answer with
Access-Control-Allow-Origin: *, because the origin isnull - answer the preflight (
OPTIONS) and allow theAuthorizationheader - not rely on cookies: the panel sends none
Which hosts a panel can call
- On
dev.scoreplay.builda panel can call anyhttps://host, so you can try your own backend, Windmill or a third-party API while you build. - On
scoreplay.builda panel can only call its own host andhttps://*.scoreplay.io(which includes the ScorePlay API). Every other host your extension calls must be reviewed and added before it goes to prod, because that list decides where customer data can be sent. Tell the platform team which hosts you need when you ask for a prod release.
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:
- cannot read cookies or use
localStorage,sessionStorageor IndexedDB - cannot submit a
<form>: use atype="button"click handler instead - cannot open popups or follow links itself: use
ScorePlay.open - cannot load scripts, styles or fonts from other hosts: put them in your folder
- cannot be framed by sites other than ScorePlay
A panel opened directly in a browser tab gets no data and no token, so it can do nothing.
Versions and releasing
- A version never changes. Each version is published to
/<id>/<version>/once. To ship a change, bumpversion. The pull request checks fail if you change files and keep the version. - Pre-releases go to dev only.
1.2.0-dev.1is published todev.scoreplay.buildbut not toscoreplay.build. Drop the suffix to release to prod. - Registering. A published version reaches nobody until it is added to the catalog. Publishing from this repo registers it right after the upload. A new extension is added to the catalog once in ScorePlay's admin tool first; until then its versions are published but not registered. ScorePlay reads the version's
manifest.jsonfromscoreplay.buildwhen it registers it. - Allowing and switching on. ScorePlay staff allow an extension for a company. It starts off, and the company's admins switch it on from Configure → Extensions, where they also see the data it receives.
- Updates. A company gets the newest stable version of the major it uses, automatically. Pre-releases are never picked automatically. ScorePlay staff can pin a company to a version, and can retire a version, which moves companies back to the newest version that is not retired.
- Breaking changes. A new major (
2.0.0) is a breaking change: companies stay on the old major until they are moved.
Preview a panel
With pnpm dev running, open http://localhost:4100/__dev/preview.html:
- Pick the extension, the surface and the light or dark theme.
- Data the panel receives shows exactly what was sent, picked from sample ScorePlay data by the same rules as ScorePlay.
- Edit the sample and press Send again to try other values, for example a
media.typeofphoto. - What the panel asked for lists every request: toast, open, navigate, close and token.
getToken()returnsdev-token, which is not a real token: a backend that checks tokens rejects it.- The panel only receives the sample data its surface offers (for example, no
mediaon the dashboard), as in ScorePlay. - The preview does not check
whenconditions.
Troubleshooting
My extension does not appear
- The ScorePlay feature flag for extensions is on for the company.
- The extension is allowed for the company and switched on (Configure → Extensions).
- The version you expect is registered, and the company is not pinned to another one.
- Its
whencondition is true for this page and this user. - The browser console has no
[extensions]warning about your manifest. - On the media toolbar, the button only shows in the media library's media details.
My panel is blank
- Load
bridge.js: with"height": "auto"the frame stays hidden until the bridge reports a height. - Start your code in
ScorePlay.ready. - Look in the console for a security header error: a script, style, font, image or call to a host that is not allowed.
A request to my backend fails
- Your backend answers
Access-Control-Allow-Origin: *and the preflight allowsAuthorization. - The panel sends
credentials: 'omit'. - The host is one a panel may call. See Which hosts a panel can call.
- The token has not expired: call
getToken()again and retry once.