Nike Media Platform

Platform Overview & API Guide  ·  Last updated March 2026

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:

EndpointExample
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

OperationPayload (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}}}
Tip: Always use -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

PatternDSL
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"}}}]}}}
Pagination note: You must include a sort array in your query to receive a lastEvaluatedKey in the response. Without sorting, pagination will not function.

Querying Livestream Status

Public GraphQL Endpoint  ·  No authentication required

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.

Endpoint: https://stage.api.nmp.nike.com/public/graphql
No 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

StatusMeaning
PENDINGLivestream created; infrastructure provisioning has not started
CREATINGAWS infrastructure (MediaLive, MediaPackage) is being provisioned
STARTINGInfrastructure ready; channel is being started
RUNNINGChannel is active and streaming
STOPPINGChannel stop has been requested
STOPPEDChannel has stopped; stream is no longer active
ARCHIVINGPost-event VOD archive is being generated
ARCHIVEDArchive complete; VOD available via vodPath / vodAlias
DELETINGResources are being torn down
DELETEDAll resources have been removed
FAILEDAn 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:

FieldDescription
idOpaque endpoint identifier
formatStream format — HLS or DASH
urlFull playback URL to pass to the player

If DRM is enabled (drm.enabled: true), supply a license server URL corresponding to drm.endpoint:

drm.endpointLicense server
EXTERNALexternal.drm.nmp.nike.com
INTERNALinternal.drm.nmp.nike.com
EXTERNAL_AUTHexternal-auth.drm.nmp.nike.com

Polling Recommendations

The endpoint has no WebSocket or subscription support. Clients needing real-time status updates should poll:

PhaseSuggested 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:

FieldDescription
vodPathS3/CDN path to the archived video
vodAliasNMP alias — use with the standard video delivery pipeline

Error Cases

ScenarioResponse
ID not found"data": { "getLivestream": null }
Livestream deleted"data": { "getLivestream": null }
Malformed GraphQLHTTP 400 with errors array

VTT Auto-Approve

Subtitle Upload  ·  Requires authentication

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

ParameterTypeRequiredDescription
keyStringYesFormat: {video-alias}/{vtt-filename} e.g. my-video/en.vtt
typeAssetTypeYesMust be vtt for auto-approve to work
autoApproveBooleanNoSet 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
  }
}
Notes:
  • The autoApprove parameter only applies when type is vtt.
  • If omitted or false, the transcript requires manual approval.
  • The parent video must exist before uploading subtitles.

Bulk Download API

Asset Downloads  ·  Image Transformations  ·  ZIP Delivery

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.

Supported image formats: jpg, jpeg, png, webp, tiff, heif, heic, raw, gif, avif

Prerequisites

  • NMP Image Translation Module v1.5.7 or higher
  • Assets uploaded to the NMP origin bucket
  • NMP_URL environment 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:

FieldTypeRequiredDescription
namestringNoName for the download request. Auto-generated if omitted.
assetsstring[]YesArray of unique asset identifiers (1–100 items)
contentsobject[]YesArray 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 assets: If an asset is not in the origin bucket it will be skipped and listed in a missing_objects.json file inside the ZIP.

On-Demand Video Transcoding

GraphQL Mutation  ·  requestRendition

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)

outputTypeCodecContainer
MP4_H264H.264MP4
MP4_H265H.265MP4
WEBM_VP9VP9WebM

PDP Profile (no audio)

outputTypeCodecContainerQuality
MP4_H264H.264MP4Standard
MP4_H264_ECOH.264MP4Economy (~70% bitrate)
MP4_H265H.265MP4Standard
MP4_H265_ECOH.265MP4Economy
WEBM_VP9VP9WebMStandard
WEBM_VP9_ECOVP9WebMEconomy
Economy quality: ~70% of standard bitrate, ~30% smaller file size. Useful for thumbnail players or lower-bandwidth scenarios.

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.

References