Home What's New All Guides →

Nike Media Platform

Developer Documentation  ·  Last updated May 2026

Welcome to the Nike Media Platform (NMP) developer documentation. The API Reference below is the authoritative source for all available queries, mutations, and types — auto-generated from live introspection. Use the Usage Examples to get started quickly, and explore the feature guides for images, livestreaming, uploads, and video.

⚡ Quick Start

Three steps to your first successful API call.

Step 1 Get a token Use the Okta client credentials grant with your client_id + client_secret, or grab a short-lived JWT from Pulse Token Tools for local testing.
Step 2 Pick your environment Use the Configure button above (or press ⌘E) to select your tenant. Every URL and code example on this page updates automatically.
Step 3 Make a request POST to /graphql with Authorization: Bearer <token> and Nike-Client-Id: <client.id>. The API Reference is just below — browse all queries and mutations, then jump to Usage Examples for copy-paste requests.

Tenant URLs

Use the Configure button in the top-right of the nav (or press ⌘E) to select your environment — every code example on this page updates automatically. The table below shows the active values:

EndpointValue
CloudFront domain (asset delivery)assets.nmp.nike.com
API via Okta  (/graphql)api.assets.nmp.nike.com/graphql
API via OSCAR  (/oscar/graphql)api.assets.nmp.nike.com/oscar/graphql

Authentication

NMP's API supports two authentication methods depending on your integration type:

MethodEndpointUse when
Okta/graphqlStandard Nike OAuth2 integrations and personal tokens
OSCAR/oscar/graphqlService-to-service integrations using the OSCAR auth platform

Okta Token

For automated integrations, use the client credentials grant against the Nike Okta server:

// lang:bash
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
Personal token for local testing: Grab a short-lived JWT from Pulse Token Tools. When using a Pulse token, your Nike-Client-Id must be nike.cx.pulse-token.

Pass the token as a bearer on all GraphQL requests to https://api.assets.nmp.nike.com/graphql. You must also include a Nike-Client-Id header set to the same client ID used to generate the token:

// lang:bash
curl --request POST \
  --url https://api.assets.nmp.nike.com/graphql \
  --header 'Authorization: Bearer <okta-jwt>' \
  --header 'Nike-Client-Id: <client.id>' \
  --header 'Content-Type: application/json' \
  --data '{ "query": "{ ... }" }'

OSCAR Token

Service-to-service integrations using OSCAR authenticate against the OSCAR token endpoint, then send requests to /oscar/graphql.

Step 1 — Build the Basic auth credential

The Authorization header requires your OSCAR appId and secret joined by a colon and base64-encoded:

// lang:bash
# Shell
echo -n 'myAppId:mySecret' | base64
# → bXlBcHBJZDpteVNlY3JldA==
// lang:js
// JavaScript
const credential = btoa('myAppId:mySecret');
// → "bXlBcHBJZDpteVNlY3JldA=="

Step 2 — Exchange for an access token

// lang:bash
curl --request POST \
  --url https://oscar.oauth.nikecloud.com/oauth/access_token/v1 \
  --header 'Authorization: Basic <base64(appId:secret)>' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data grant_type=client_credentials \
  --data scope=media:nike.media.platform::update

The response contains an access_token. Pass it as a bearer to the OSCAR-protected endpoint:

// lang:bash
curl --request POST \
  --url https://api.assets.nmp.nike.com/oscar/graphql \
  --header 'Authorization: Bearer <oscar-access-token>' \
  --header 'Content-Type: application/json' \
  --data '{ "query": "{ ... }" }'
Note: The /oscar/graphql endpoint is only available for environments that have an API Gateway domain configured. See the Tenant URLs table above for your environment's value.

API Reference

Auto-generated from live introspection  ·  Select environment above to switch schemas

Browse all available queries, mutations, and types. Select an environment with the Configure button to load that environment's schema. Each operation links to its usage example below.

Loading schema reference…

API Usage Examples

The following examples map directly to operations in the schema reference above. Send all requests to https://api.assets.nmp.nike.com/graphql with your bearer token. No CloudFront cookies required for metadata queries.

// lang:graphql
query BasicSearch($query: String!) {
  search(query: $query) {
    lastEvaluatedKey
    count
    assets {
      assetPath
      key
      contentType
    }
  }
}
// lang:json
{
  "query": "{\"query\":{\"match_all\":{}},\"sort\":[{\"createdAt\":\"desc\"}],\"size\":100}"
}
// lang:graphql
query KeywordSearch($query: String!) {
  search(query: $query) {
    lastEvaluatedKey
    count
    assets {
      assetPath
      key
      contentType
      metadata
    }
  }
}
// lang:json
{
  "query": "{\"query\":{\"multi_match\":{\"query\":\"jordan campaign\",\"fields\":[\"key\",\"metadata.Title\",\"metadata.Description\"]}},\"sort\":[{\"createdAt\":\"desc\"}],\"size\":25}"
}
// lang:graphql
query DateRangeSearch($query: String!) {
  search(query: $query) {
    lastEvaluatedKey
    count
    assets {
      assetPath
      key
      contentType
      createdAt
    }
  }
}
// lang:json
{
  "query": "{\"query\":{\"bool\":{\"must\":[{\"range\":{\"createdAt\":{\"gte\":\"2025-01-01T00:00:00Z\",\"lte\":\"2025-07-01T00:00:00Z\"}}}]},\"sort\":[{\"createdAt\":\"desc\"}],\"size\":50}"
}

search — Filter by Content Type

// lang:graphql
query ContentTypeSearch($query: String!) {
  search(query: $query) {
    lastEvaluatedKey
    count
    assets {
      assetPath
      key
      contentType
    }
  }
}
// lang:json
{
  "query": "{\"query\":{\"term\":{\"contentType\":\"video/mp4\"}},\"sort\":[{\"createdAt\":\"desc\"}],\"size\":50}"
}

search — Pagination

If the response includes lastEvaluatedKey, pass it as search_after in your next query. A sort array is required for pagination to work.

// lang:graphql
query PaginatedSearch($query: String!) {
  search(query: $query) {
    lastEvaluatedKey
    count
    assets {
      assetPath
      key
      contentType
    }
  }
}
// lang:json
{
  "query": "{\"query\":{\"match_all\":{}},\"sort\":[{\"createdAt\":\"desc\"}],\"size\":100,\"search_after\":[1751904936429]}"
}

getAsset — Fetch a Single Asset

Retrieve full metadata for one asset by its ID or S3 key path.

Asset ID / Key
// lang:graphql
query GetAsset($id: String!) {
  getAsset(id: $id) {
    assetPath
    key
    contentType
    createdAt
    lastModifiedAt
    metadata
    url
    ... on VideoAsset {
      derivativeAssets {
        edges {
          node {
            assetPath
            contentType
            publishedUrl
          }
        }
      }
    }
  }
}
// lang:json
{
  "id": "videos/campaign/jordan-spring-2025.mp4"
}
// lang:json
{
  "data": {
    "getAsset": {
      "assetPath": "source/videos/campaign/jordan-spring-2025.mp4",
      "key": "videos/campaign/jordan-spring-2025.mp4",
      "contentType": "video/mp4",
      "createdAt": "2025-03-15T10:22:00Z",
      "lastModifiedAt": "2025-03-15T10:22:00Z",
      "metadata": { "Title": "Jordan Spring 2025", "Campaign": "Spring" },
      "url": "https://assets.nmp.nike.com/videos/campaign/jordan-spring-2025.mp4"
    }
  }
}

getAsset — Get Alt Text for an Image

Alt text is stored on the classification field, which is only available on ImageAsset. Use an inline fragment (... on ImageAsset) to access it. Alt text is set manually by editors or auto-generated by AI classification.

Asset ID / Key
// lang:graphql
query GetImageAltText($id: String!) {
  getAsset(id: $id) {
    assetPath
    key
    contentType
    ... on ImageAsset {
      classification {
        altText
      }
    }
  }
}
// lang:json
{
  "id": "images/products/air-max-270-white.jpg"
}
// lang:json
{
  "data": {
    "getAsset": {
      "assetPath": "source/images/products/air-max-270-white.jpg",
      "key": "images/products/air-max-270-white.jpg",
      "contentType": "image/jpeg",
      "classification": {
        "altText": "Nike Air Max 270 in white and grey on a clean studio background"
      }
    }
  }
}
Note: classification and altText are only present on ImageAsset. If the asset is a video, audio, or other type, the inline fragment body will be skipped and classification will not appear in the response. altText will be null if it has not yet been set.

getProductLabels — Product Labels for an Image

Retrieve the product style codes detected on (or manually assigned to) an image, with confidence scores and approval status. Look up by either imagePath (source object path) or cloudinaryId (asset ID — unique ID from Cloudinary). Provide exactly one of the two. classificationJsonUrl is populated only when at least one label is approved.

Example 1 — Lookup by cloudinaryId

Example using curl:

// lang:bash
curl --request POST \
  --url https://api.assets.nmp.nike.com/graphql \
  --header 'Authorization: Bearer <your-jwt-token>' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "query LabelsByCloudinaryId($cloudinaryId: String!) { getProductLabels(cloudinaryId: $cloudinaryId) { imagePath cloudinaryId classificationJsonUrl productLabels { styleCode confidence approved } } }",
    "variables": {
      "cloudinaryId": "169af936-c279-446d-8404-659900d234e8"
    }
  }'

Response example:

// lang:json
{
  "data": {
    "getProductLabels": {
      "imagePath": "fifa_wc_2026/IB5106-635_169af936-c27.png",
      "cloudinaryId": "169af936-c279-446d-8404-659900d234e8",
      "classificationJsonUrl": "https://assets.nmp.nike.com/fifa_wc_2026/IB5106-635_169af936-c27/classification.json",
      "productLabels": [
        { "styleCode": "IB5106-635", "confidence": 0.9851000000000001, "approved": true  },
        { "styleCode": "HF7310-004", "confidence": 0.8506398750441886, "approved": true  },
        { "styleCode": "FQ7860-008", "confidence": 0.8327771214434809, "approved": false }
      ]
    }
  }
}

Example 2 — Lookup by imagePath

Use this when you already know the source object path (e.g. you just uploaded the asset and have the S3 key). The same asset is returned, regardless of which key you look up by.

Example using curl:

// lang:bash
curl --request POST \
  --url https://api.assets.nmp.nike.com/graphql \
  --header 'Authorization: Bearer <your-jwt-token>' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "query LabelsByImagePath($imagePath: String!) { getProductLabels(imagePath: $imagePath) { imagePath cloudinaryId classificationJsonUrl productLabels { styleCode confidence approved } } }",
    "variables": {
      "imagePath": "fifa_wc_2026/IB5106-635_169af936-c27.png"
    }
  }'

Response example:

// lang:json
{
  "data": {
    "getProductLabels": {
      "imagePath": "fifa_wc_2026/IB5106-635_169af936-c27.png",
      "cloudinaryId": "169af936-c279-446d-8404-659900d234e8",
      "classificationJsonUrl": "https://assets.nmp.nike.com/fifa_wc_2026/IB5106-635_169af936-c27/classification.json",
      "productLabels": [
        { "styleCode": "IB5106-635", "confidence": 0.9851000000000001, "approved": true  },
        { "styleCode": "HF7310-004", "confidence": 0.8506398750441886, "approved": true  },
        { "styleCode": "FQ7860-008", "confidence": 0.8327771214434809, "approved": false }
      ]
    }
  }
}

Example 3 — Image with no approved labels

When no labels have been approved on the asset, classificationJsonUrl is null. AI-detected labels may still be present with approved: false; an empty productLabels array means no labels of any kind exist for the asset.

Response example:

// lang:json
{
  "data": {
    "getProductLabels": {
      "imagePath": "editorial/2026/spring/lifestyle-shot.jpg",
      "cloudinaryId": "8f2a1b9e-4c5d-6e7f-8091-234567890abc",
      "classificationJsonUrl": null,
      "productLabels": [
        { "styleCode": "DR0420-001", "confidence": 0.74, "approved": false }
      ]
    }
  }
}
Note: Provide either imagePath or cloudinaryId, not both. The query returns an error if neither is supplied or if no asset is found.

getRenditions — Video Renditions

Retrieve all transcoded renditions for a video — MP4, WebM, and HLS — by alias or mapId.

Alias
// lang:graphql
query GetRenditions($alias: String, $mapId: String) {
  getRenditions(alias: $alias, mapId: $mapId) {
    parent {
      key
      contentType
    }
    renditions {
      assetPath
      contentType
      publishedUrl
    }
  }
}
// lang:json
{
  "alias": "jordan-spring-2025"
}
// lang:json
{
  "data": {
    "getRenditions": {
      "parent": {
        "key": "videos/campaign/jordan-spring-2025.mp4",
        "contentType": "video/mp4"
      },
      "renditions": [
        {
          "assetPath": "derivatives/videos/jordan-spring-2025/hls/master.m3u8",
          "contentType": "application/x-mpegURL",
          "publishedUrl": "https://assets.nmp.nike.com/derivatives/videos/jordan-spring-2025/hls/master.m3u8"
        },
        {
          "assetPath": "derivatives/videos/jordan-spring-2025/mp4/1080p.mp4",
          "contentType": "video/mp4",
          "publishedUrl": "https://assets.nmp.nike.com/derivatives/videos/jordan-spring-2025/mp4/1080p.mp4"
        }
      ]
    }
  }
}

getPresignedURL — Upload an Asset

Get a presigned S3 URL, then PUT directly to S3 — no proxy, no size limits.

Asset Key
// lang:graphql
query GetPresignedURL($key: String!, $type: AssetType) {
  getPresignedURL(key: $key, type: $type) {
    url
    fields
  }
}
// lang:json
{
  "key": "videos/brand/new-campaign.mp4",
  "type": "video"
}
// lang:bash
# Use the url + fields from the response to PUT directly to S3
curl -X PUT "$PRESIGNED_URL" \
  -H "Content-Type: video/mp4" \
  --upload-file new-campaign.mp4

updateAssetMetadata — Update Asset Metadata

Attach or replace arbitrary key-value metadata on any asset.

Asset ID / Key
// lang:graphql
mutation UpdateAssetMetadata($id: String!, $metadata: JSON!) {
  updateAssetMetadata(id: $id, metadata: $metadata) {
    assetPath
    metadata
    lastModifiedAt
  }
}
// lang:json
{
  "id": "videos/campaign/jordan-spring-2025.mp4",
  "metadata": {
    "Title": "Jordan Spring 2025",
    "Campaign": "Spring",
    "Season": "SS25",
    "Approved": true
  }
}
// lang:json
{
  "data": {
    "updateAssetMetadata": {
      "assetPath": "source/videos/campaign/jordan-spring-2025.mp4",
      "metadata": {
        "Title": "Jordan Spring 2025",
        "Campaign": "Spring",
        "Season": "SS25",
        "Approved": true
      },
      "lastModifiedAt": "2025-05-06T14:30:00Z"
    }
  }
}

updateAssetUserTags / updateAssetCategories

Tag assets for discovery and filtering. Tags and categories are indexed and searchable via OpenSearch.

// lang:graphql
mutation UpdateUserTags($id: String!, $tags: [String!]!) {
  updateAssetUserTags(id: $id, tags: $tags) {
    assetPath
    userTags
  }
}

mutation UpdateCategories($id: String!, $categories: [String!]!) {
  updateAssetCategories(id: $id, categories: $categories) {
    assetPath
    categories
  }
}
// lang:json
{
  "id": "images/products/shoe-001.jpg",
  "tags": ["running", "footwear", "SS25"],
  "categories": ["Product", "Footwear"]
}

requestRendition — Request Video Transcoding

Trigger on-demand transcoding for an uploaded video. Poll getMediaConvertJobStatus to track progress.

// lang:graphql
mutation RequestRendition($key: String!, $profile: VideoProfile) {
  requestRendition(key: $key, profile: $profile) {
    jobId
    status
  }
}
// lang:json
{
  "key": "videos/campaign/jordan-spring-2025.mp4",
  "profile": "PDP"
}
// lang:graphql
query PollTranscodeJob($jobId: String!) {
  getMediaConvertJobStatus(jobId: $jobId) {
    jobId
    status
    percentComplete
    outputGroupDetails {
      outputDetails {
        outputFilePaths
        durationInMs
      }
    }
  }
}

suggestUserTags / suggestCategories — Typeahead

Power search-as-you-type UIs with suggestions for existing tags, categories, and metadata keys.

// lang:graphql
query Suggestions($prefix: String!) {
  suggestUserTags(prefix: $prefix, limit: 10)
  suggestCategories(prefix: $prefix, limit: 10)
  suggestMetadataKeys(prefix: $prefix, limit: 10)
}
// lang:json
{
  "prefix": "jor"
}
// lang:json
{
  "data": {
    "suggestUserTags": ["jordan", "jordan-retro", "jordan-brand"],
    "suggestCategories": ["Jordan", "Jordan Retro"],
    "suggestMetadataKeys": []
  }
}

getActivityHistory — Activity Audit

Audit who did what to an asset, or see everything a particular user has done.

Asset ID / Key
// lang:graphql
query ActivityHistory(
  $nmp_id: String
  $email: String
  $startTime: String
  $endTime: String
) {
  getActivityHistory(
    nmp_id: $nmp_id
    email: $email
    startTime: $startTime
    endTime: $endTime
  ) {
    assetId
    action
    email
    timestamp
    changes
  }
}
// lang:json
{
  "nmp_id": "videos/campaign/jordan-spring-2025.mp4",
  "startTime": "2025-05-01T00:00:00Z",
  "endTime": "2025-05-07T00:00:00Z"
}
// lang:json
{
  "data": {
    "getActivityHistory": [
      {
        "assetId": "videos/campaign/jordan-spring-2025.mp4",
        "action": "UPDATE_METADATA",
        "email": "jane.doe@nike.com",
        "timestamp": "2025-05-06T14:30:00Z",
        "changes": { "Title": "Jordan Spring 2025" }
      }
    ]
  }
}

OpenSearch Query DSL (for search)

The search query accepts an OpenSearch Query DSL JSON string as its query argument. This gives you the full power of OpenSearch filtering, sorting, and aggregation over asset metadata.

Common Patterns

PatternDSL
Match all{"query":{"match_all":{}},"size":100}
Keyword (multi-field){"query":{"multi_match":{"query":"nike","fields":["key","metadata.Title","metadata.Description"]}}}
Phrase prefix{"query":{"multi_match":{"type":"phrase_prefix","query":"jordan","fields":["key"]}}}
Exact field match{"query":{"term":{"contentType":"image/jpeg"}}}
Date range{"query":{"bool":{"must":[{"range":{"createdAt":{"gte":"2025-01-01T00:00:00Z"}}}]}}}
Combined (keyword + date){"query":{"bool":{"must":[{"multi_match":{"query":"campaign","fields":["key"]}},{"range":{"createdAt":{"gte":"2025-01-01T00:00:00Z"}}}]}}}
Sort by field{"query":{"match_all":{}},"sort":[{"createdAt":"desc"}],"size":50}
Pagination note: You must include a sort array in your query to receive a lastEvaluatedKey in the response. Without sorting, pagination will not function.

Error Codes

NMP returns standard HTTP status codes plus GraphQL-level errors in the response body. Here are the most common ones you'll encounter.

HTTP Status Codes

StatusMeaningCommon cause & fix
200 OK Success Always returned for GraphQL — check the errors array in the body even on 200s.
400 Bad Request Malformed request Invalid JSON body, missing query field, or a GraphQL parse error. Check the errors[].message field.
401 Unauthorized Missing or invalid token No Authorization header, expired token, or invalid signature. Re-fetch a token from Okta and verify Nike-Client-Id matches the token's cid claim.
403 Forbidden Token valid, permission denied Your OAuth app does not have the required scope for this operation (e.g. attempting a mutation with a read-only token), or the asset is restricted to a different tenant.
404 Not Found Asset or resource missing The S3 key or asset ID does not exist in the current tenant. Verify you are using the correct environment and the asset path is accurate.
429 Too Many Requests Rate limited Back off and retry with exponential jitter. Batch requests where possible and avoid polling faster than necessary.
500 Internal Server Error Server-side failure Transient errors are safe to retry with backoff. If persistent, report in #nike-media-platform-support with the request ID from the response header.
503 Service Unavailable Downstream dependency unavailable Typically a Lambda cold-start or OpenSearch timeout. Retry after a brief delay.

GraphQL Error Shapes

GraphQL errors are always in the errors array regardless of HTTP status. A partial success (some fields resolved, some errored) returns 200 with both data and errors.

// lang:json
{
  "errors": [{
    "message": "Unauthorized",
    "extensions": { "code": "UNAUTHENTICATED", "http": { "status": 401 } }
  }],
  "data": null
}
// lang:json
{
  "errors": [{
    "message": "Variable \"$id\" of required type \"String!\" was not provided.",
    "locations": [{ "line": 1, "column": 7 }],
    "extensions": { "code": "BAD_USER_INPUT" }
  }],
  "data": null
}
// lang:json
{
  "data": {
    "getAsset": null,
    "search": { "count": 12, "assets": [ { "key": "..." } ] }
  },
  "errors": [{
    "message": "Asset not found",
    "path": ["getAsset"],
    "extensions": { "code": "NOT_FOUND" }
  }]
}

Common extensions.code Values

CodeMeaning
UNAUTHENTICATEDToken missing, expired, or the Nike-Client-Id header does not match the token's cid claim.
FORBIDDENToken is valid but lacks the required scope or tenant permission.
NOT_FOUNDThe requested asset, livestream, or resource does not exist.
BAD_USER_INPUTMissing required argument, wrong type, or invalid enum value.
INTERNAL_SERVER_ERRORUnhandled server error — safe to retry. Include extensions.requestId in any support request.

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 m query parameter directly on the asset's filename URL — without a /originals/ prefix. A HMAC-SHA256 signature is passed as the s parameter. See the Image Transformations section for the complete reference, signing guide, and code examples.

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


Image Transformations

Real-Time Image Processing  ·  Sharp.js-compatible  ·  Edge-cached globally

NMP's image transformation pipeline lets you resize, crop, reformat, and stylize any image on demand via a single URL parameter — no pre-processing pipelines and no derivative storage required. Every transformed image is generated at the edge on first request and cached globally so all subsequent requests are instant.

Try it live: Use the Image Transformation Playground to interactively build transforms and export the exact JSON payload.

URL Structure

Image transformation requests are made directly to the asset's filename on your CloudFront domain — without any path prefix. The base64-encoded JSON payload is passed as the m query parameter, and a signing signature is passed as s:

// lang:http
GET https://assets.nmp.nike.com/<asset-filename>?m=<base64-payload>&s=<signature>

For example:

// lang:text
https://assets.nmp.nike.com/NikeAirmax270.webp?m=eyJlZGl0cyI6eyJqcGVnIjp7InF1YWxpdHkiOjEwMH0sIndlYnAiOnsicXVhbGl0eSI6MTAwfSwiYXZpZiI6eyJxdWFsaXR5Ijo3MH0sInJlc2l6ZSI6eyJ3aWR0aCI6NTAwLCJoZWlnaHQiOjUwMH19fQ==&s=d97837fbf34f7fa8714c0da2ceb6a7ce6ca9bd18a55d516b0ce74c520f5aec85

The m parameter above decodes to:

// lang:json
{
  "edits": {
    "jpeg":  { "quality": 100 },
    "webp":  { "quality": 100 },
    "avif":  { "quality": 70  },
    "resize": { "width": 500, "height": 500 }
  }
}
Signing required: Every transformation request must include a valid s (HMAC-SHA256) signature. The signing secret is available upon request — reach out in Slack at #nike-media-platform-support.

Query Parameters

ParameterRequiredDescription
mYesBase64-encoded JSON transformation payload (the edits object)
sYesHMAC-SHA256 signature of the m value, computed with your tenant's signing secret

Payload Structure

The JSON object encoded in m contains an edits key with one or more transform operations:

KeyTypeDescription
editsobjectOne or more transform operations (see Operations Reference)
outputFormatstring | nullForce a specific output format: "jpeg", "png", "webp", "avif". Set to null or omit to let NMP auto-negotiate the best format for the client.

Encoding & Signing Example

// lang:js
import crypto from 'crypto';

const CF_DOMAIN    = 'https://assets.nmp.nike.com';
const SIGN_SECRET  = '<your-signing-secret>'; // request via #nike-media-platform-support

function buildTransformUrl(assetFilename, edits, outputFormat = null) {
  const payload = { edits };
  if (outputFormat) payload.outputFormat = outputFormat;

  const m = btoa(JSON.stringify(payload));
  const s = crypto.createHmac('sha256', SIGN_SECRET).update(m).digest('hex');

  return `${CF_DOMAIN}/${assetFilename}?m=${m}&s=${s}`;
}

// Example: resize 500×500 with high quality
const url = buildTransformUrl('NikeAirmax270.webp', {
  jpeg:  { quality: 100 },
  webp:  { quality: 100 },
  avif:  { quality: 70  },
  resize: { width: 500, height: 500 },
});
console.log(url);
Originals vs. transformations: Use /originals/<key> only when you need the raw, untransformed source file (e.g. for bulk download or archival). Transformation requests go directly to the filename at the domain root with ?m= & s=.

Supported Source Formats

JPEG, PNG, WebP, AVIF, GIF (animated), TIFF, SVG — with full transparency and animated GIF passthrough.


Named Transformations

Named transformations let you define a reusable preset once — by name — and reference it across all surfaces. Teams share consistent presets without repeating parameters, and any preset can be overridden inline for one-off needs.

To use a named transformation, pass the preset name as the name query parameter on the asset URL (no m payload required):

// lang:http
GET https://assets.nmp.nike.com/<asset-filename>?name=thumbnail-300w&s=<signature>

Inline overrides take precedence over named preset values when both are supplied:

// lang:http
// Use the "thumbnail-300w" preset but override the fit mode inline
GET https://assets.nmp.nike.com/<asset-filename>?name=thumbnail-300w&m=<override-payload>&s=<signature>
Named transformations are managed through the NMP admin API. Contact your platform team to create or update presets.

Operations Reference

All operations live inside the edits object. Multiple operations can be combined in a single request — they are applied in the order listed below.

resize

Scale the image to fit within the given dimensions. Only width or height (or both) are required.

FieldTypeDefaultDescription
widthintegerTarget width in pixels
heightintegerTarget height in pixels
fitstring"inside"One of inside, cover, contain, fill, outside
withoutEnlargementbooleanfalseSkip upscaling — never output an image larger than the source
backgroundobjecttransparentFill color for contain letterboxing: {"r":255,"g":255,"b":255,"alpha":1}

Fit modes:

ModeBehavior
insideScale down to fit entirely within the box. Aspect ratio preserved. Never upscales by default.
coverScale to fill the box. Image is cropped on the longer dimension. Aspect ratio preserved.
containScale to fit inside the box with letterboxing/pillarboxing. Fill color controlled by background.
fillStretch to exact dimensions. Aspect ratio is not preserved.
outsideScale so the smallest dimension matches the box. May exceed the box on the other dimension.
// lang:json
// Resize to max 800px wide, preserving aspect ratio
{
  "edits": {
    "resize": { "width": 800, "fit": "inside", "withoutEnlargement": true }
  }
}

// Scale to fill 400×400 square, cropping the overflow
{
  "edits": {
    "resize": { "width": 400, "height": 400, "fit": "cover" }
  }
}

// Letterbox to 1920×1080 with a white background
{
  "edits": {
    "resize": {
      "width": 1920, "height": 1080, "fit": "contain",
      "background": { "r": 255, "g": 255, "b": 255, "alpha": 1 }
    }
  }
}

extract

Crop a rectangular region from the image. All values are in pixels relative to the top-left origin of the (possibly already resized) image.

FieldTypeRequiredDescription
leftintegerYesX offset from left edge
topintegerYesY offset from top edge
widthintegerYesWidth of the crop region
heightintegerYesHeight of the crop region
// lang:json
// Crop a 1200×1200 square starting 600px from the left
{
  "edits": {
    "extract": { "left": 600, "top": 0, "width": 1200, "height": 1200 }
  }
}
Resize then crop: Apply resize first to scale the image, then use extract to crop the scaled result. Operations are evaluated in the order they appear in the payload.

rotate

Rotate the image clockwise by the given number of degrees. Arbitrary angles expand the canvas; 90/180/270-degree rotations are lossless.

// lang:json
{ "edits": { "rotate": 90 } }   // Rotate 90° clockwise
{ "edits": { "rotate": 270 } }  // Rotate 90° counter-clockwise

flip / flop

Mirror the image along an axis.

OperationEffect
flip: trueVertical mirror (top ↔ bottom)
flop: trueHorizontal mirror (left ↔ right)
// lang:json
{ "edits": { "flop": true } }        // Mirror left–right
{ "edits": { "flip": true } }        // Mirror top–bottom
{ "edits": { "flip": true, "flop": true } }  // Both axes

blur

Apply a Gaussian blur. The value is the blur sigma (standard deviation in pixels). Higher values produce stronger blur.

ValueEffect
0.3Minimum — barely perceptible
3Moderate blur
20Heavy blur
40Maximum — near-opaque soft mask
// lang:json
{ "edits": { "blur": 8 } }   // sigma = 8

sharpen

Apply an unsharp mask. The value is the sharpen sigma. Higher values produce stronger sharpening.

// lang:json
{ "edits": { "sharpen": 2.5 } }   // sigma = 2.5

grayscale

Convert the image to grayscale. Removes all color information.

// lang:json
{ "edits": { "grayscale": true } }

negate

Invert all pixel values (creates a photographic negative effect).

// lang:json
{ "edits": { "negate": true } }

normalize

Stretch the image contrast so the darkest pixel becomes black and the lightest pixel becomes white. Useful for underexposed or low-contrast images.

// lang:json
{ "edits": { "normalize": true } }

modulate

Adjust brightness, saturation, and hue simultaneously. All fields are optional — only include the ones you want to change.

FieldTypeDefaultRangeDescription
brightnessnumber10 – 3Multiplier for luminance. 1 = unchanged, 2 = double, 0.5 = half.
saturationnumber10 – 3Multiplier for color saturation. 0 = grayscale, 2 = vivid.
hueinteger00 – 360Degrees of hue rotation on the color wheel.
// lang:json
// Increase saturation 50%, reduce brightness 20%
{
  "edits": {
    "modulate": { "brightness": 0.8, "saturation": 1.5 }
  }
}

// Shift hue 180° (complementary color palette)
{
  "edits": {
    "modulate": { "hue": 180 }
  }
}

tint

Apply a color tint by multiplying each pixel's RGB channels by the supplied ratios (using 128 as the neutral midpoint). Values below 128 reduce that channel; values above 128 amplify it.

FieldTypeRangeNeutral
rinteger0 – 255128
ginteger0 – 255128
binteger0 – 255128
// lang:json
// Warm amber tint
{ "edits": { "tint": { "r": 220, "g": 150, "b": 80 } } }

// Cool blue tint
{ "edits": { "tint": { "r": 80, "g": 120, "b": 220 } } }

gamma

Apply a gamma correction curve. Values greater than 1 brighten the midtones; values less than 1 darken them. Useful for matching display profiles or compensating for overexposed photos.

ValueEffect
1.0No change
1.5 – 2.0Lift midtones (brighten)
2.2Standard sRGB compensation
3.0Heavy brightening
// lang:json
{ "edits": { "gamma": 2.2 } }

extend

Add padding around the image. Useful for creating uniform square thumbnails or adding a safe-area border without cropping.

FieldTypeDescription
topintegerPixels to add at top
bottomintegerPixels to add at bottom
leftintegerPixels to add at left
rightintegerPixels to add at right
backgroundobjectFill color: {"r":0,"g":0,"b":0,"alpha":1}
// lang:json
// Add a 20px white border on all sides
{
  "edits": {
    "extend": {
      "top": 20, "bottom": 20, "left": 20, "right": 20,
      "background": { "r": 255, "g": 255, "b": 255, "alpha": 1 }
    }
  }
}

trim

Automatically remove edges of the image that match the corner pixel's color (or a supplied color), within a given tolerance. Great for stripping white or transparent borders from product shots.

FieldTypeDefaultDescription
thresholdinteger10Color similarity tolerance (0 – 255). Higher values trim more aggressively.
backgroundstring | objectcorner pixelThe color to trim: CSS color string or {"r":255,"g":255,"b":255}
// lang:json
// Auto-trim white borders
{ "edits": { "trim": { "background": "#ffffff", "threshold": 15 } } }

// Auto-trim using corner-pixel color with tight tolerance
{ "edits": { "trim": { "threshold": 5 } } }

removeBackground

Remove the background from a product or subject image using the AI transforms service, returning the subject isolated on a transparent background. The result is a PNG, WebP, or AVIF with a full alpha channel — ideal for compositing, overlays, and e-commerce product shots.

ValueDescription
trueRemove background with default settings
false / omitNo-op — background is preserved
Format note: removeBackground preserves the alpha channel. Requesting outputFormat: "jpeg" will flatten the alpha onto a white background. Use "png", "webp", or "avif" (or omit outputFormat to let NMP auto-negotiate) to retain transparency.
// lang:json
// Remove background — auto-negotiate best transparent format
{
  "edits": {
    "removeBackground": true,
    "resize": { "width": 800 }
  }
}

// Remove background, force WebP with alpha
{
  "outputFormat": "webp",
  "edits": {
    "removeBackground": true,
    "resize": { "width": 1200, "height": 1200, "fit": "contain" }
  }
}

Output Format & Quality

Automatic Format Negotiation

When outputFormat is null (or omitted), NMP automatically serves the most efficient format supported by the client:

  1. AVIF — smallest file size, best for modern browsers/apps
  2. WebP — excellent compression, supported everywhere modern
  3. JPEG or PNG — fallback for older clients

This negotiation happens transparently via the Accept header — your image URLs never change.

Forcing a Specific Format

Set outputFormat at the top level of the payload to override auto-negotiation:

// lang:json
// Force WebP output
{
  "outputFormat": "webp",
  "edits": { "resize": { "width": 800 } }
}

// Force JPEG output (e.g. for download links)
{
  "outputFormat": "jpeg",
  "edits": { "resize": { "width": 2400 } }
}

Supported outputFormat values: "jpeg", "png", "webp", "avif"

Quality Settings (per format)

Per-format quality settings live inside the edits object alongside other operations. You can specify quality for multiple target formats in a single payload — NMP will use whichever matches the negotiated output format. NMP's Adaptive Quality system handles the defaults automatically; only set these when you have a specific override requirement.

Format key (inside edits)Default qualityRange
jpegAdaptive (auto)1 – 100
webpAdaptive (auto)1 – 100
avifAdaptive (auto)1 – 100
pngLosslessCompression level 1–9
// lang:json
// Set explicit quality floors for all three modern formats
{
  "edits": {
    "jpeg":  { "quality": 100 },
    "webp":  { "quality": 100 },
    "avif":  { "quality": 70  },
    "resize": { "width": 500, "height": 500 }
  }
}

// High-quality WebP only
{
  "outputFormat": "webp",
  "edits": {
    "webp":   { "quality": 90 },
    "resize": { "width": 1200 }
  }
}

Adaptive Quality

NMP's AI-driven Adaptive Quality system analyzes each image's visual complexity before delivery and selects the right compression level automatically:

  • Simple product shots on plain backgrounds get aggressive compression
  • Rich editorial photography gets a higher quality floor
  • The result consistently matches or exceeds previous vendor (Cloudinary) output at 20% less bandwidth
  • Analysis completes before delivery — no added latency

AI Transforms

AI transforms are a category of edits operations powered by a dedicated machine-learning service running alongside the image pipeline. Unlike Sharp operations (which are deterministic pixel manipulations), AI transforms use neural network models to understand image content.

AI transforms are available on deployments that have the AI transforms service configured. They are requested identically to other operations — inside the edits object — and can be combined with any other Sharp operation in the same request.

Availability: AI transforms require the AI_TRANSFORMS_SERVICE_URL environment variable to be set on the Lambda. Contact your platform team if background removal is not working in your environment.

removeBackground

Isolates the primary subject of an image and removes its background, returning the subject on a fully transparent canvas. The model is optimized for product photography, athlete shots, and objects against uniform or studio backgrounds.

FieldTypeDescription
removeBackgroundtrueEnable background removal. Any falsy value (false, null, 0) disables it.

Output format & transparency

Background removal produces an image with a full alpha channel. The output format determines how transparency is handled:

FormatAlphaNotes
avif✅ PreservedBest compression. Auto-negotiated for modern clients.
webp✅ PreservedWide browser support.
png✅ PreservedLossless. Larger files.
jpeg❌ Flattened to whiteJPEG has no alpha channel.

Omit outputFormat to let NMP auto-negotiate the best transparent format (AVIF → WebP → PNG) based on the client's Accept header.

Combining with other operations

AI transforms run before the Sharp pipeline. This means you can chain removeBackground (or addTextOverlay) with any resize, crop, or color operation — Sharp receives the already-processed image and applies the remaining edits. When both AI operations are present in the same request, removeBackground runs first, then addTextOverlay is applied to the result.

// lang:json
// Remove background, then resize and contain on a custom canvas
{
  "edits": {
    "removeBackground": true,
    "resize": {
      "width": 800,
      "height": 800,
      "fit": "contain",
      "background": { "r": 0, "g": 0, "b": 0, "alpha": 0 }
    }
  }
}

// Remove background + convert to WebP for a product card
{
  "outputFormat": "webp",
  "edits": {
    "removeBackground": true,
    "resize": { "width": 600, "height": 600, "fit": "inside", "withoutEnlargement": true }
  }
}

// Remove background + grayscale subject (e.g. monochrome hero)
{
  "edits": {
    "removeBackground": true,
    "grayscale": true,
    "resize": { "width": 1200 }
  }
}
Caching: AI-transformed images are cached at the edge identically to all other NMP transformations — the AI service is only called on the first request for a given transform combination.

addTextOverlay

Renders text onto an image using a scalable TrueType font with full control over position, size, color, shadow, and opacity. The text is composited directly onto the source image — the result retains the original image's alpha channel (if any) with the text rendered on top.

Processing order: Like all AI transforms, addTextOverlay runs before the Sharp pipeline. This means Sharp operations like resize, crop, and format conversion are applied to the already-overlaid image.

Options

FieldTypeDefaultDescription
textstringrequiredThe text to render. Use \n for explicit line breaks. Multi-line text is word-wrapped automatically.
font_sizeinteger48Font size in points.
boldbooleanfalseUse the bold variant of the font.
italicbooleanfalseUse the italic variant of the font.
colorstring | array"#ffffff"Text color. Accepts "#RRGGBB", "#RRGGBBAA", or [R, G, B] / [R, G, B, A].
xinteger | string"center"Horizontal position in pixels, or "left" / "center" / "right".
yinteger | string"center"Vertical position in pixels, or "top" / "center" / "bottom".
paddinginteger20Pixel inset from edges when using named positions ("left", "bottom", etc.).
alignstring"left"Multi-line alignment: "left", "center", or "right".
max_widthintegerimage width − 2×paddingWord-wrap text at this pixel width.
line_spacinginteger8Extra pixels between lines.
letter_spacinginteger0Extra pixels between characters (kerning). Negative values tighten spacing.
word_spacinginteger0Extra pixels between words, added on top of letter_spacing.
decorationstring"none"Text decoration: "none", "underline", "strikethrough", or "underline_strikethrough".
background_colorstring | arrayFilled rectangle behind the text block. Same color format as color. Omit to disable.
background_paddinginteger6Extra pixels around the text block inside the background rectangle.
opacityfloat1.0Overall text layer opacity (0.0 – 1.0).
shadowbooleanfalseDraw a soft drop-shadow behind the text.
shadow_colorstring | array"#000000a0"Shadow color (same format as color).
shadow_offsetinteger4Shadow offset in pixels.
shadow_blurinteger6Shadow blur radius in pixels.
stroke_widthinteger0Text outline width in pixels. 0 disables the outline.
stroke_colorstring | array"#000000"Outline color (same format as color).
font_pathstringAbsolute path to a custom TTF/OTF font inside the container. Falls back to bundled fonts, then system DejaVu Sans.

Examples

// lang:json
// Centered white headline at the bottom of an image
{
  "edits": {
    "addTextOverlay": {
      "text": "JUST DO IT",
      "font_size": 96,
      "color": "#ffffff",
      "x": "center",
      "y": "bottom",
      "align": "center",
      "shadow": true
    }
  }
}

// Semi-transparent label with outline, top-left corner
{
  "edits": {
    "addTextOverlay": {
      "text": "NEW ARRIVAL",
      "font_size": 36,
      "color": "#ffffff",
      "x": "left",
      "y": "top",
      "opacity": 0.85,
      "stroke_width": 2,
      "stroke_color": "#000000"
    }
  }
}

// Multi-line caption, centered, with shadow — then resize for web
{
  "edits": {
    "addTextOverlay": {
      "text": "Spring / Summer 2026\nNew Collection",
      "font_size": 64,
      "color": "#ffffff",
      "align": "center",
      "x": "center",
      "y": "center",
      "shadow": true,
      "shadow_blur": 12,
      "shadow_offset": 6
    },
    "resize": { "width": 1200, "fit": "inside" }
  }
}

// Text overlay on a background-removed product shot
{
  "edits": {
    "removeBackground": true,
    "addTextOverlay": {
      "text": "AIR MAX",
      "font_size": 72,
      "color": "#111111",
      "x": "center",
      "y": "bottom",
      "align": "center"
    },
    "resize": { "width": 800, "height": 800, "fit": "contain",
                "background": { "r": 255, "g": 255, "b": 255, "alpha": 1 } }
  }
}
Missing text field: Omitting or sending an empty text value returns HTTP 400 — it is a required field.

Caching: AI-transformed images are cached at the edge identically to all other NMP transformations — the AI service is only called on the first request for a given transform combination.

Full Examples

Example 1 — Responsive Thumbnail

// lang:js
import crypto from 'crypto';
const SIGN_SECRET = '<your-signing-secret>';

const payload = {
  edits: {
    resize: { width: 400, height: 400, fit: "cover", withoutEnlargement: true }
  }
};
const m = btoa(JSON.stringify(payload));
const s = crypto.createHmac('sha256', SIGN_SECRET).update(m).digest('hex');
const url = `https://assets.nmp.nike.com/products-shoe.jpg?m=${m}&s=${s}`;

Example 2 — Grayscale Editorial Hero

// lang:js
const payload = {
  edits: {
    resize:    { width: 1920, fit: "inside" },
    grayscale: true,
    sharpen:   1.5
  }
};
const m = btoa(JSON.stringify(payload));
const s = crypto.createHmac('sha256', SIGN_SECRET).update(m).digest('hex');
const url = `https://assets.nmp.nike.com/editorial-hero.jpg?m=${m}&s=${s}`;

Example 3 — Crop + Warm Tone

// lang:js
// Resize to 2400px wide, then crop the center 1200×800
const payload = {
  outputFormat: "webp",
  edits: {
    resize:   { width: 2400, fit: "inside" },
    extract:  { left: 600, top: 400, width: 1200, height: 800 },
    modulate: { brightness: 1.05, saturation: 1.2 },
    tint:     { r: 200, g: 160, b: 120 }
  }
};
const m = btoa(JSON.stringify(payload));
const s = crypto.createHmac('sha256', SIGN_SECRET).update(m).digest('hex');
const url = `https://assets.nmp.nike.com/campaign-image.png?m=${m}&s=${s}`;

Example 4 — curl

// lang:bash
# Build and encode the payload
PAYLOAD=$(echo -n '{"edits":{"resize":{"width":800},"grayscale":true}}' | base64)

# Compute HMAC-SHA256 signature (requires your signing secret)
SIG=$(echo -n "${PAYLOAD}" | openssl dgst -sha256 -hmac "${NMP_SIGN_SECRET}" | awk '{print $2}')

# Fetch the transformed image
curl -L \
  -H "Cookie: <CloudFront cookies>" \
  "https://assets.nmp.nike.com/NikeAirmax270.webp?m=${PAYLOAD}&s=${SIG}" \
  -o output.jpg

Example 5 — Background Removal (Product Shot)

// lang:js
const payload = {
  edits: {
    removeBackground: true,
    resize: { width: 800, height: 800, fit: "contain",
              background: { r: 0, g: 0, b: 0, alpha: 0 } }
  }
};
const m = btoa(JSON.stringify(payload));
const s = crypto.createHmac('sha256', SIGN_SECRET).update(m).digest('hex');
const url = `https://assets.nmp.nike.com/shoe-product.png?m=${m}&s=${s}`;

Example 6 — Text Overlay

// lang:js
const payload = {
  edits: {
    addTextOverlay: {
      text: "JUST DO IT",
      font_size: 96,
      color: "#ffffff",
      x: "center",
      y: "bottom",
      align: "center",
      shadow: true,
      shadow_blur: 10,
    },
    resize: { width: 1200, fit: "inside" }
  }
};
const m = btoa(JSON.stringify(payload));
const s = crypto.createHmac('sha256', SIGN_SECRET).update(m).digest('hex');
const url = `https://assets.nmp.nike.com/hero-image.jpg?m=${m}&s=${s}`;

Example 7 — Text Overlay on Removed Background

// lang:js
// removeBackground runs first (AI), then addTextOverlay (AI), then resize (Sharp)
const payload = {
  edits: {
    removeBackground: true,
    addTextOverlay: {
      text: "AIR MAX 270",
      font_size: 56,
      color: "#111111",
      x: "center",
      y: "bottom",
      align: "center",
      stroke_width: 0,
    },
    resize: {
      width: 800, height: 800, fit: "contain",
      background: { r: 255, g: 255, b: 255, alpha: 1 }
    }
  }
};
const m = btoa(JSON.stringify(payload));
const s = crypto.createHmac('sha256', SIGN_SECRET).update(m).digest('hex');
const url = `https://assets.nmp.nike.com/shoe-product.png?m=${m}&s=${s}`;

Example 6 — All Operations Combined

// lang:json
{
  "edits": {
    "resize":    { "width": 1200, "height": 800, "fit": "cover" },
    "rotate":    90,
    "flop":      true,
    "sharpen":   1.8,
    "grayscale": false,
    "negate":    false,
    "normalize": true,
    "modulate":  { "brightness": 1.1, "saturation": 1.3, "hue": 10 },
    "tint":      { "r": 210, "g": 180, "b": 140 },
    "gamma":     1.8,
    "trim":      { "threshold": 10 }
  }
}

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://api.assets.nmp.nike.com/public/graphql
No Authorization header is required. Only the getLivestream query is available.

Full Query

// lang:graphql
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
  }
}
// lang:json
{ "id": "my-event-slug" }

Minimal Status-Only Query

// lang:graphql
query {
  getLivestream(id: "my-event-slug") {
    status
    isPreStream
    startsAt
    endsAt
  }
}

curl Example

// lang:bash
curl -X POST https://api.assets.nmp.nike.com/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

// lang:json
{
  "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.

// lang:text
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

// lang:graphql
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

// lang:bash
curl --request POST \
  --url https://api.assets.nmp.nike.com/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:

// lang:bash
curl --request PUT \
  --url "<presigned-url>" \
  --header 'Content-Type: text/vtt' \
  --data-binary @./en.vtt

Verifying Transcript Status

// lang:graphql
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 https://assets.nmp.nike.com/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)

// lang:bash
curl -L --output ~/Downloads/my-download.zip \
  --url https://assets.nmp.nike.com/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

// lang:bash
curl -L --output ~/Downloads/resized.zip \
  --url https://assets.nmp.nike.com/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. Send mutations to https://api.assets.nmp.nike.com/graphql with your bearer token. The API returns a CDN URL immediately; the video file is generated asynchronously in the background. See the Usage Examples for a complete request/response.

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

// lang:graphql
mutation {
  requestRendition(
    alias: "f068c98a1d3569c9"
    profile: STRAVA
    outputType: MP4_H264
  ) {
    urls
    profile
    outputType
  }
}
// lang:json
{
  "data": {
    "requestRendition": {
      "urls": ["https://cdn.nmp.nike.com/video/f068c98a1d3569c9_h264.mp4"],
      "profile": "STRAVA",
      "outputType": "MP4_H264"
    }
  }
}

Asset Retrieval API

Downloading Originals

Make a GET request to your CloudFront domain with the S3 key prefixed by /originals. All requests must be signed — compute an HMAC-SHA256 of the asset path (including the leading /) using your tenant's signing secret and pass it as ?s=:

// lang:bash
curl -L \
  -H "Cookie: <CloudFront cookies>" \
  "https://assets.nmp.nike.com/originals/PNGs/Apparel/AP1163_3.png?s=<signature>" -o output.png
Signing originals: The value to sign is the asset path including the leading slash — e.g. /originals/PNGs/Apparel/AP1163_3.png. This matches the URI as seen by the signature-validation Lambda, which always begins with /. Use HMAC-SHA256 with your tenant's signing secret (the same secret used for image transforms). Contact #nike-media-platform-support to obtain your secret.

Generating Image Derivatives

For on-the-fly image transformations, request the asset directly at the domain root (no /originals/ prefix) with ?m= (base64 payload) and ?s= (HMAC signature). See the Image Transformations section for the full reference and signing guide.

// lang:bash
curl -L \
  -H "Cookie: <CloudFront cookies>" \
  "https://assets.nmp.nike.com/NikeAirmax270.webp?m=<base64-payload>&s=<signature>" \
  -o output.webp

Node.js Example


Contributing to NMP

This guide is for partnering engineering teams who need to make changes to the Nike Media Platform. It covers environment access, the development workflow for lambdas, UI, and infrastructure, and how to promote changes all the way through to dev.

Getting Access

Before you can work in NMP you need two AD group memberships. Request them through the standard access-request process:

  • App.App.NikeMediaPlatform.Demo.Staging — grants access to the dev environment at https://dev.mm-demo.eadp-nmp-prod.nikecloud.com/
  • Application.AWS.Nike.524943396044.ReadOnly — grants read-only AWS access to the NMP account for debugging (CloudWatch logs, Step Functions, S3, etc.)

Lambda Changes

Lambda source lives in nike-gpcoe/media-platform-lambdas. PRs against main automatically build and upload artifacts to the staging S3 bucket. The dev deployment always pulls from latest in that bucket, so no version bump is needed to test your changes in dev.

  1. Open a PR against main. The CI pipeline (Deploy Staging workflow) builds your lambda and syncs the .zip artifact to gpcoe-media-platform-lambdas-staging-artifacts/latest/.
  2. Once the PR build is green, manually trigger the Deploy workflow in media-platform-deployment-techmod-dev via Run workflow.
  3. After the workflow completes, dev is updated. Lambda function names in dev are prefixed with gpcoe-cddev.
Debugging: Use your Application.AWS.Nike.524943396044.ReadOnly access to inspect CloudWatch logs for any gpcoe-cddev-* function in the NMP AWS account (account ID 524943396044).

UI Changes

The Media Manager front-end lives in nike-gpcoe/media-platform-mm. The flow mirrors the lambda workflow — PRs build to a staging artifacts bucket and dev always pulls from latest:

  1. Open a PR against main in media-platform-mm. CI builds the front-end bundle and uploads it to gpcoe-media-platform-mm-staging-artifacts/latest/.
  2. Once the build passes, manually trigger the Deploy workflow in media-platform-deployment-techmod-dev.
  3. Verify your changes at https://dev.mm-demo.eadp-nmp-prod.nikecloud.com/.

Infrastructure Changes

If your work requires changes to the underlying infrastructure — expanding lambda IAM permissions, adding environment variables, modifying AWS services, etc. — those changes live in the Terraform modules in nike-gpcoe/media-platform.

  1. Open a PR in media-platform with the Terraform module changes and get it merged to main.
  2. Cut a new release by pushing a semver tag (e.g. v2.28.0) from main. Terraform consumers reference modules by tag.
  3. In media-platform-deployment-techmod-dev, update the ?ref= version in the relevant .tf file — for example:
    source = "github.com/nike-gpcoe/media-platform//terraform/modules/metadata-api?ref=2.28.0"
  4. Commit the version bump to media-platform-deployment-techmod-dev (push to main or run the Deploy workflow manually) to apply the changes.

Creating a Release

Once your changes are tested in dev and the PR is approved, create a GitHub Release to publish a versioned artifact that can be pinned in deployments.

NMP follows Semantic Versioning (SemVer): version numbers take the form MAJOR.MINOR.PATCH. Increment PATCH for backwards-compatible bug fixes, MINOR for new backwards-compatible features, and MAJOR for breaking changes.

Lambda release (media-platform-lambdas)

  1. Merge your PR to main.
  2. In the GitHub UI, go to Releases → Draft a new release, create a new tag (e.g. v1.8.0), and publish the release.
  3. Pushing the tag triggers the Release on Tag GitHub Actions workflow, which builds all lambdas and syncs the artifacts to the production S3 buckets under the new tag path.
  4. In media-platform-deployment-techmod-dev, open terraform.tfvars and set backend_lambdas_version to the new version, then re-deploy.

UI release (media-platform-mm)

  1. Merge your PR to main.
  2. In the GitHub UI, go to Releases → Draft a new release, create a new tag, and publish the release.
  3. The release workflow builds the front-end bundle and uploads it to the production artifacts bucket under the new tag path.
  4. In media-platform-deployment-techmod-dev, open terraform.tfvars and set frontend_lambdas_version to the new version, then re-deploy.
Tip: Coordinate lambda, UI, and Terraform module version bumps into the same deployment run to keep all components of the dev environment in sync.