Nike Media Platform
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.
Three steps to your first successful API call.
client_id + client_secret, or grab a short-lived JWT from Pulse Token Tools for local testing.
/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:
| Endpoint | Value |
|---|---|
| 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:
| Method | Endpoint | Use when |
|---|---|---|
| Okta | /graphql | Standard Nike OAuth2 integrations and personal tokens |
| OSCAR | /oscar/graphql | Service-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
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": "{ ... }" }'
/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
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.
search — Basic Search
// 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}"
}
search — Keyword Search
// 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}"
}
search — Date Range Filter
// 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.
// 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.
// 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"
}
}
}
}
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 }
]
}
}
}
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.
// 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.
// 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.
// 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"
}
}
}
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.
// 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
| Pattern | DSL |
|---|---|
| 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} |
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
| Status | Meaning | Common 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
| Code | Meaning |
|---|---|
UNAUTHENTICATED | Token missing, expired, or the Nike-Client-Id header does not match the token's cid claim. |
FORBIDDEN | Token is valid but lacks the required scope or tenant permission. |
NOT_FOUND | The requested asset, livestream, or resource does not exist. |
BAD_USER_INPUT | Missing required argument, wrong type, or invalid enum value. |
INTERNAL_SERVER_ERROR | Unhandled 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
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.
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 }
}
}
s (HMAC-SHA256) signature. The signing secret is available upon request — reach out in Slack at #nike-media-platform-support.
Query Parameters
| Parameter | Required | Description |
|---|---|---|
m | Yes | Base64-encoded JSON transformation payload (the edits object) |
s | Yes | HMAC-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:
| Key | Type | Description |
|---|---|---|
edits | object | One or more transform operations (see Operations Reference) |
outputFormat | string | null | Force 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/<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>
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.
| Field | Type | Default | Description |
|---|---|---|---|
width | integer | — | Target width in pixels |
height | integer | — | Target height in pixels |
fit | string | "inside" | One of inside, cover, contain, fill, outside |
withoutEnlargement | boolean | false | Skip upscaling — never output an image larger than the source |
background | object | transparent | Fill color for contain letterboxing: {"r":255,"g":255,"b":255,"alpha":1} |
Fit modes:
| Mode | Behavior |
|---|---|
inside | Scale down to fit entirely within the box. Aspect ratio preserved. Never upscales by default. |
cover | Scale to fill the box. Image is cropped on the longer dimension. Aspect ratio preserved. |
contain | Scale to fit inside the box with letterboxing/pillarboxing. Fill color controlled by background. |
fill | Stretch to exact dimensions. Aspect ratio is not preserved. |
outside | Scale 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.
| Field | Type | Required | Description |
|---|---|---|---|
left | integer | Yes | X offset from left edge |
top | integer | Yes | Y offset from top edge |
width | integer | Yes | Width of the crop region |
height | integer | Yes | Height 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 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.
| Operation | Effect |
|---|---|
flip: true | Vertical mirror (top ↔ bottom) |
flop: true | Horizontal 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.
| Value | Effect |
|---|---|
0.3 | Minimum — barely perceptible |
3 | Moderate blur |
20 | Heavy blur |
40 | Maximum — 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.
| Field | Type | Default | Range | Description |
|---|---|---|---|---|
brightness | number | 1 | 0 – 3 | Multiplier for luminance. 1 = unchanged, 2 = double, 0.5 = half. |
saturation | number | 1 | 0 – 3 | Multiplier for color saturation. 0 = grayscale, 2 = vivid. |
hue | integer | 0 | 0 – 360 | Degrees 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.
| Field | Type | Range | Neutral |
|---|---|---|---|
r | integer | 0 – 255 | 128 |
g | integer | 0 – 255 | 128 |
b | integer | 0 – 255 | 128 |
// 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.
| Value | Effect |
|---|---|
1.0 | No change |
1.5 – 2.0 | Lift midtones (brighten) |
2.2 | Standard sRGB compensation |
3.0 | Heavy 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.
| Field | Type | Description |
|---|---|---|
top | integer | Pixels to add at top |
bottom | integer | Pixels to add at bottom |
left | integer | Pixels to add at left |
right | integer | Pixels to add at right |
background | object | Fill 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.
| Field | Type | Default | Description |
|---|---|---|---|
threshold | integer | 10 | Color similarity tolerance (0 – 255). Higher values trim more aggressively. |
background | string | object | corner pixel | The 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.
| Value | Description |
|---|---|
true | Remove background with default settings |
false / omit | No-op — background is preserved |
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:
- AVIF — smallest file size, best for modern browsers/apps
- WebP — excellent compression, supported everywhere modern
- 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 quality | Range |
|---|---|---|
jpeg | Adaptive (auto) | 1 – 100 |
webp | Adaptive (auto) | 1 – 100 |
avif | Adaptive (auto) | 1 – 100 |
png | Lossless | Compression 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.
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.
| Field | Type | Description |
|---|---|---|
removeBackground | true | Enable 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:
| Format | Alpha | Notes |
|---|---|---|
avif | ✅ Preserved | Best compression. Auto-negotiated for modern clients. |
webp | ✅ Preserved | Wide browser support. |
png | ✅ Preserved | Lossless. Larger files. |
jpeg | ❌ Flattened to white | JPEG 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 }
}
}
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.
addTextOverlay runs before the Sharp pipeline. This means Sharp operations like resize, crop, and format conversion are applied to the already-overlaid image.
Options
| Field | Type | Default | Description |
|---|---|---|---|
text | string | required | The text to render. Use \n for explicit line breaks. Multi-line text is word-wrapped automatically. |
font_size | integer | 48 | Font size in points. |
bold | boolean | false | Use the bold variant of the font. |
italic | boolean | false | Use the italic variant of the font. |
color | string | array | "#ffffff" | Text color. Accepts "#RRGGBB", "#RRGGBBAA", or [R, G, B] / [R, G, B, A]. |
x | integer | string | "center" | Horizontal position in pixels, or "left" / "center" / "right". |
y | integer | string | "center" | Vertical position in pixels, or "top" / "center" / "bottom". |
padding | integer | 20 | Pixel inset from edges when using named positions ("left", "bottom", etc.). |
align | string | "left" | Multi-line alignment: "left", "center", or "right". |
max_width | integer | image width − 2×padding | Word-wrap text at this pixel width. |
line_spacing | integer | 8 | Extra pixels between lines. |
letter_spacing | integer | 0 | Extra pixels between characters (kerning). Negative values tighten spacing. |
word_spacing | integer | 0 | Extra pixels between words, added on top of letter_spacing. |
decoration | string | "none" | Text decoration: "none", "underline", "strikethrough", or "underline_strikethrough". |
background_color | string | array | — | Filled rectangle behind the text block. Same color format as color. Omit to disable. |
background_padding | integer | 6 | Extra pixels around the text block inside the background rectangle. |
opacity | float | 1.0 | Overall text layer opacity (0.0 – 1.0). |
shadow | boolean | false | Draw a soft drop-shadow behind the text. |
shadow_color | string | array | "#000000a0" | Shadow color (same format as color). |
shadow_offset | integer | 4 | Shadow offset in pixels. |
shadow_blur | integer | 6 | Shadow blur radius in pixels. |
stroke_width | integer | 0 | Text outline width in pixels. 0 disables the outline. |
stroke_color | string | array | "#000000" | Outline color (same format as color). |
font_path | string | — | Absolute 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 } }
}
}
text field: Omitting or sending an empty text value returns HTTP 400 — it is a required field.
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 \
"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
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://api.assets.nmp.nike.com/public/graphqlNo
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
| 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.
// 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:
| 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
// lang:graphql
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
// 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
}
}
- 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 https://assets.nmp.nike.com/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)
// 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_objects.json file inside the ZIP.On-Demand Video Transcoding
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)
| 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
// 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 \
"https://assets.nmp.nike.com/originals/PNGs/Apparel/AP1163_3.png?s=<signature>" -o output.png
/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 \
"https://assets.nmp.nike.com/NikeAirmax270.webp?m=<base64-payload>&s=<signature>" \
-o output.webp
Node.js Example
// lang:js
const axios = require('axios');
const fetch = require('node-fetch');
const crypto = require('crypto');
const OKTA_URL = 'https://nike.okta.com/oauth2/aus27z7p76as9Dz0H1t7/v1/token';
const BASE_URL = 'https://assets.nmp.nike.com';
const SIGN_SECRET = '<your-signing-secret>'; // request via #nike-media-platform-support
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;
}
// Sign the asset path (with leading slash) with HMAC-SHA256
function signPath(assetPath) {
return crypto.createHmac('sha256', SIGN_SECRET).update(assetPath).digest('hex');
}
async function downloadAsset(assetPath, outputFile) {
const sig = signPath(`/originals/${assetPath}`);
const res = await fetch(`${BASE_URL}/originals/${assetPath}?s=${sig}`, {
});
const fs = require('fs');
const stream = fs.createWriteStream(outputFile);
await new Promise((resolve, reject) => { res.body.pipe(stream); stream.on('finish', resolve); stream.on('error', reject); });
}
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.
- Open a PR against
main. The CI pipeline (Deploy Staging workflow) builds your lambda and syncs the.zipartifact togpcoe-media-platform-lambdas-staging-artifacts/latest/. - Once the PR build is green, manually trigger the Deploy workflow in media-platform-deployment-techmod-dev via Run workflow.
- After the workflow completes, dev is updated. Lambda function names in dev are prefixed with
gpcoe-cddev.
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:
- Open a PR against
maininmedia-platform-mm. CI builds the front-end bundle and uploads it togpcoe-media-platform-mm-staging-artifacts/latest/. - Once the build passes, manually trigger the Deploy workflow in media-platform-deployment-techmod-dev.
- 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.
- Open a PR in
media-platformwith the Terraform module changes and get it merged tomain. - Cut a new release by pushing a semver tag (e.g.
v2.28.0) frommain. Terraform consumers reference modules by tag. - In
media-platform-deployment-techmod-dev, update the?ref=version in the relevant.tffile — for example:
source = "github.com/nike-gpcoe/media-platform//terraform/modules/metadata-api?ref=2.28.0" - Commit the version bump to
media-platform-deployment-techmod-dev(push tomainor 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)
- Merge your PR to
main. - In the GitHub UI, go to Releases → Draft a new release, create a new tag (e.g.
v1.8.0), and publish the release. - 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.
- In
media-platform-deployment-techmod-dev, openterraform.tfvarsand setbackend_lambdas_versionto the new version, then re-deploy.
UI release (media-platform-mm)
- Merge your PR to
main. - In the GitHub UI, go to Releases → Draft a new release, create a new tag, and publish the release.
- The release workflow builds the front-end bundle and uploads it to the production artifacts bucket under the new tag path.
- In
media-platform-deployment-techmod-dev, openterraform.tfvarsand setfrontend_lambdas_versionto the new version, then re-deploy.