Nike Media Platform
Welcome to the Nike Media Platform (NMP) developer documentation. This guide covers authentication, asset retrieval, image transformations, and the GraphQL metadata API.
Tenant URLs
Before making requests, identify your NMP tenant's base URLs:
| Endpoint | Example |
|---|---|
| CloudFront domain (asset delivery) | <my.cloudfront.domain> |
| API Gateway URL (metadata) | <my.api.gateway.url> |
Authentication
Okta Token Generation
All API requests require a valid bearer token. Obtain one using the client credentials grant:
curl --request POST \
--url https://nike.okta.com/oauth2/aus27z7p76as9Dz0H1t7/v1/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data client_id=<client.id> \
--data client_secret=<client.secret> \
--data grant_type=client_credentials \
--data scope=media:nike.media.platform::read
CloudFront Cookie Retrieval
Use the JWT token to obtain CloudFront cookies, required for all asset and image requests:
curl --request GET \
--url https://d2je8axf9oddc5.cloudfront.net/auth/ \
--header 'Authorization: Bearer <jwt>' \
--header 'Nike-Client-Id: zetcom.nmp.dna'
Asset Retrieval API
Downloading Originals
Make a GET request to your CloudFront domain, using the S3 key prefixed with /originals:
curl -L \
-H "Cookie: <CloudFront cookies>" \
"<my.cloudfront.domain>/originals/PNGs/Apparel/AP1163_3.png" -o output.png
Generating Image Derivatives
Provide a Sharp.js compatible edits payload, base64-encoded, as a request query parameter:
curl -L \
-H "Cookie: <CloudFront cookies>" \
"<my.cloudfront.domain>/originals/path/to/image.png?request=<base64-payload>" \
-o output.png
Supported Edit Operations
| Operation | Payload (before base64) |
|---|---|
| Grayscale | {"outputFormat":null,"edits":{"grayscale":true}} |
| Resize | {"outputFormat":null,"edits":{"resize":{"width":200}}} |
| Crop | {"outputFormat":null,"edits":{"extract":{"left":1200,"top":0,"width":1200,"height":1200}}} |
-L with curl to follow redirects. The API may issue a 302 to a /t path when generating a new derivative. Do not hardcode /t in your request URL.Node.js Example
const axios = require('axios');
const fetch = require('node-fetch');
const OKTA_URL = 'https://nike.okta.com/oauth2/aus27z7p76as9Dz0H1t7/v1/token';
const CF_AUTH = 'https://<my.cloudfront.domain>/auth/';
const BASE_URL = 'https://<my.cloudfront.domain>';
async function getBearerToken(clientId, clientSecret) {
const params = new URLSearchParams({
client_id: clientId, client_secret: clientSecret,
grant_type: 'client_credentials',
scope: 'media:nike.media.platform::read'
});
const { data } = await axios.post(OKTA_URL, params);
return data.access_token;
}
async function getCloudFrontCookies(token, clientId) {
const res = await fetch(CF_AUTH, {
headers: { 'Authorization': `Bearer ${token}`, 'Nike-Client-Id': clientId }
});
const raw = res.headers.raw()['set-cookie'] || [];
return raw.filter(c => /CloudFront/.test(c)).map(c => c.split(';')[0]).join('; ');
}
async function downloadAsset(assetPath, outputFile, cookies) {
const res = await fetch(`${BASE_URL}/originals/${assetPath}`, {
headers: { 'Cookie': cookies }
});
const fs = require('fs');
const stream = fs.createWriteStream(outputFile);
await new Promise((res, rej) => { res.body.pipe(stream); stream.on('finish', res); });
}
Metadata API
The Metadata API is a GraphQL endpoint for searching and retrieving asset metadata. Only a bearer token and client ID are required — no CloudFront cookies.
Basic Search
query BasicSearch($query: String!) {
search(query: $query) {
lastEvaluatedKey
count
assets {
assetPath
key
contentType
}
}
}
// Variables
{
"query": "{\"query\":{\"match_all\":{}},\"sort\":[{\"createdAt\":\"desc\"}],\"size\":100}"
}
Querying by Date Range
{
"query": "{\"query\":{\"bool\":{\"must\":[{\"range\":{\"createdAt\":{\"gte\":\"2025-01-01T00:00:00Z\",\"lte\":\"2025-07-01T00:00:00Z\"}}}]}}}"
}
Pagination
If the response includes lastEvaluatedKey, pass it as search_after in your next query. A sort array is required for pagination to work.
{
"query": { "match_all": {} },
"sort": [ { "createdAt": "desc" } ],
"size": 100,
"search_after": [1751904936429]
}
OpenSearch Query DSL
All metadata queries use the OpenSearch Query DSL passed as a JSON string in the query GraphQL variable.
Common Patterns
| Pattern | DSL |
|---|---|
| Match all | {"query":{"match_all":{}},"size":100} |
| Multi-field search | {"query":{"multi_match":{"query":"png","fields":["key","metadata.Title"]}}} |
| Phrase prefix | {"query":{"multi_match":{"type":"phrase_prefix","query":"nike","fields":["key"]}}} |
| Date range | {"query":{"bool":{"must":[{"range":{"createdAt":{"gte":"2025-01-01T00:00:00Z"}}}]}}} |
sort array in your query to receive a lastEvaluatedKey in the response. Without sorting, pagination will not function.Querying Livestream Status
The public GraphQL endpoint lets you check the status and playback details of a livestream without any authentication. Intended for client-side player integrations, countdown pages, and any consumer that needs to know whether a stream is live, scheduled, or archived.
https://stage.api.nmp.nike.com/public/graphqlNo
Authorization header is required. Only the getLivestream query is available.
Full Query
query GetLivestream($id: String!) {
getLivestream(id: $id) {
id
name
prettyName
description
status
isPreStream
startsAt
endsAt
originEndpoints {
id
format
url
}
vodPath
vodAlias
drm {
enabled
endpoint
}
transcriptionEnabled
translationsEnabled
warmupStartMinutes
createdAt
updatedAt
archivedAt
}
}
{ "id": "my-event-slug" }
Minimal Status-Only Query
query {
getLivestream(id: "my-event-slug") {
status
isPreStream
startsAt
endsAt
}
}
curl Example
curl -X POST https://0vv6ino8pi.execute-api.us-west-2.amazonaws.com/prod/public/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { getLivestream(id: \"my-event-slug\") { id status isPreStream startsAt endsAt originEndpoints { format url } } }"
}'
Example Response
{
"data": {
"getLivestream": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "RUNNING",
"isPreStream": false,
"startsAt": "2026-04-01T18:00:00.000Z",
"endsAt": "2026-04-01T20:00:00.000Z",
"originEndpoints": [
{ "format": "HLS", "url": "https://live.nmp.nike.com/out/v1/abc123/index.m3u8" },
{ "format": "DASH", "url": "https://live.nmp.nike.com/out/v1/abc123/index.mpd" }
]
}
}
}
Livestream Status Values
| Status | Meaning |
|---|---|
PENDING | Livestream created; infrastructure provisioning has not started |
CREATING | AWS infrastructure (MediaLive, MediaPackage) is being provisioned |
STARTING | Infrastructure ready; channel is being started |
RUNNING | Channel is active and streaming |
STOPPING | Channel stop has been requested |
STOPPED | Channel has stopped; stream is no longer active |
ARCHIVING | Post-event VOD archive is being generated |
ARCHIVED | Archive complete; VOD available via vodPath / vodAlias |
DELETING | Resources are being torn down |
DELETED | All resources have been removed |
FAILED | An operation failed |
The isPreStream Flag
isPreStream is true when the backend MediaLive channel is already RUNNING (warming up) but the viewer-facing startsAt time has not been reached.
status = RUNNING AND isPreStream = true → show countdown
status = RUNNING AND isPreStream = false → show live player
Playback URLs & DRM
originEndpoints is populated once the stream reaches RUNNING. Each entry contains:
| Field | Description |
|---|---|
id | Opaque endpoint identifier |
format | Stream format — HLS or DASH |
url | Full playback URL to pass to the player |
If DRM is enabled (drm.enabled: true), supply a license server URL corresponding to drm.endpoint:
drm.endpoint | License server |
|---|---|
EXTERNAL | external.drm.nmp.nike.com |
INTERNAL | internal.drm.nmp.nike.com |
EXTERNAL_AUTH | external-auth.drm.nmp.nike.com |
Polling Recommendations
The endpoint has no WebSocket or subscription support. Clients needing real-time status updates should poll:
| Phase | Suggested interval |
|---|---|
Pre-event (waiting for RUNNING) | Every 30 seconds |
Live (watching isPreStream flip) | Every 10 seconds |
Post-event (waiting for ARCHIVED) | Every 60 seconds |
VOD / Archive Playback
After the event ends and the stream moves to ARCHIVED, the recorded video is available via:
| Field | Description |
|---|---|
vodPath | S3/CDN path to the archived video |
vodAlias | NMP alias — use with the standard video delivery pipeline |
Error Cases
| Scenario | Response |
|---|---|
| ID not found | "data": { "getLivestream": null } |
| Livestream deleted | "data": { "getLivestream": null } |
| Malformed GraphQL | HTTP 400 with errors array |
VTT Auto-Approve
The autoApprove parameter lets you automatically approve VTT (subtitle) files upon upload, bypassing the manual approval workflow.
API Reference — getPresignedURL
query GetPresignedURL($key: String!, $type: AssetType, $autoApprove: Boolean) {
getPresignedURL(key: $key, type: $type, autoApprove: $autoApprove) {
url
}
}
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
key | String | Yes | Format: {video-alias}/{vtt-filename} e.g. my-video/en.vtt |
type | AssetType | Yes | Must be vtt for auto-approve to work |
autoApprove | Boolean | No | Set to true to skip manual approval. Default: false |
Step 1 — Get Presigned URL
curl --request POST \
--url <your-api-gateway-url>/graphql \
--header 'Authorization: Bearer <your-jwt-token>' \
--header 'Content-Type: application/json' \
--data '{
"query": "query GetPresignedURL($key: String!, $type: AssetType, $autoApprove: Boolean) { getPresignedURL(key: $key, type: $type, autoApprove: $autoApprove) { url } }",
"variables": {
"key": "my-video-alias/en.vtt",
"type": "vtt",
"autoApprove": true
}
}'
Step 2 — Upload the VTT File
Use the presigned URL returned in Step 1 to upload the file via PUT:
curl --request PUT \
--url "<presigned-url>" \
--header 'Content-Type: text/vtt' \
--data-binary @./en.vtt
Verifying Transcript Status
query GetTranscriptStatus($parentVideoKey: String!) {
transcripts(parentVideoKey: $parentVideoKey) {
key
status
approved
sourceLang
}
}
- The
autoApproveparameter only applies whentypeisvtt. - If omitted or
false, the transcript requires manual approval. - The parent video must exist before uploading subtitles.
Bulk Download API
The Download Request API allows you to download up to 100 assets from the origin bucket in bulk, with optional per-asset image transformations. The API responds with a 302 redirect to a ZIP file download URL.
jpg, jpeg, png, webp, tiff, heif, heic, raw, gif, avifPrerequisites
- NMP Image Translation Module v1.5.7 or higher
- Assets uploaded to the NMP origin bucket
NMP_URLenvironment variable set to your CloudFront or custom domain- Authentication enabled: call
/auth/first to set CloudFront cookies
Request Schema
Send a POST to ${NMP_URL}/image/download with a JSON body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Name for the download request. Auto-generated if omitted. |
assets | string[] | Yes | Array of unique asset identifiers (1–100 items) |
contents | object[] | Yes | Array of named edit configurations to include in the ZIP |
Example 1 — Basic Download (originals)
curl -L --output ~/Downloads/my-download.zip \
--url ${NMP_URL}/image/download \
--header "Content-Type: application/json" \
--data '{
"name": "my-download",
"assets": ["path/to/asset.jpg", "asset2.png", "other/asset3.webp"],
"contents": [
{ "name": "originals" }
]
}'
Example 2 — With Transformations
curl -L --output ~/Downloads/resized.zip \
--url ${NMP_URL}/image/download \
--header "Content-Type: application/json" \
--data '{
"name": "resized-thumbnails",
"assets": ["path/to/asset.jpg", "path/to/asset2.png"],
"contents": [
{
"name": "originals"
},
{
"name": "thumbnails-300w",
"edits": {
"resize": { "width": 300 }
}
}
]
}'
missing_objects.json file inside the ZIP.On-Demand Video Transcoding
Request specific video renditions by alias, profile, and output format. The API returns a CDN URL immediately; the video file is generated asynchronously in the background.
API Signature
mutation {
requestRendition(
alias: String! # Required: video alias identifier
profile: VideoProfile! # Required: STRAVA or PDP
outputType: RenditionOutputType! # Required: see table below
): OnDemandRenditionResult!
}
Response Type
type OnDemandRenditionResult {
urls: [String!]! # CDN URL(s) — currently always 1 URL
profile: VideoProfile!
outputType: RenditionOutputType!
alias: String!
}
Profiles & Output Types
STRAVA Profile (with audio)
| outputType | Codec | Container |
|---|---|---|
MP4_H264 | H.264 | MP4 |
MP4_H265 | H.265 | MP4 |
WEBM_VP9 | VP9 | WebM |
PDP Profile (no audio)
| outputType | Codec | Container | Quality |
|---|---|---|---|
MP4_H264 | H.264 | MP4 | Standard |
MP4_H264_ECO | H.264 | MP4 | Economy (~70% bitrate) |
MP4_H265 | H.265 | MP4 | Standard |
MP4_H265_ECO | H.265 | MP4 | Economy |
WEBM_VP9 | VP9 | WebM | Standard |
WEBM_VP9_ECO | VP9 | WebM | Economy |
Example — Request H.264 Rendition
mutation {
requestRendition(
alias: "f068c98a1d3569c9"
profile: STRAVA
outputType: MP4_H264
) {
urls
profile
outputType
}
}
Response
{
"data": {
"requestRendition": {
"urls": ["https://cdn.nmp.nike.com/video/f068c98a1d3569c9_h264.mp4"],
"profile": "STRAVA",
"outputType": "MP4_H264"
}
}
}
FAQ & Best Practices
How do I request originals instead of derivatives?
Use the /originals prefix in the asset path. Without it, requests may resolve to a cached derivative if one exists.
How do I match metadata results to asset retrieval?
The assetPath field returned by the metadata API matches the resource path used in the asset retrieval API.
How are image edits specified?
As a JSON payload (Sharp.js compatible), base64-encoded, passed as the request query parameter on the asset URL.
Do pagination queries require sorting?
Yes. You must include a sort array in your OpenSearch query to receive a lastEvaluatedKey in the response. Without sorting, pagination will not work.