Skip to main content
View as Markdown

Implementing a Pipes2 Endpoints Manifest

When you configure a Pipes2 Endpoint in Zapp Studio you normally enter each endpoint URL and its context keys by hand. The Endpoints Manifest is a single route your DSP exposes that Zapp can read automatically to populate all of those fields — eliminating manual setup and keeping your Studio configuration in sync with your code.

How it works

Your DSP exposes a GET /v1/endpoints-manifest route. When you register that URL in Zapp Studio, Zapp fetches it once and auto-populates every endpoint your service declares — including which context keys each endpoint requires and which query params editors can configure.

The manifest response is a JSON object that maps fully-resolved endpoint URLs to their metadata:

{
"endpoints": {
"https://my-dsp.example.com/my-dsp": {
"name": "My DSP — Collection Feed",
"method": "get",
"mime_type": "application/json",
"context_obj": [],
"modifiers": [
{
"id": "number_limit",
"name": "Result Limit",
"params": [{ "key": "limit", "ui": { "label": "Limit", "placeholder": "20" }, "schema": { "type": "number", "default": 20 } }]
}
]
},
"https://my-dsp.example.com/my-dsp/media": {
"name": "My DSP — Video Preload",
"method": "get",
"mime_type": "application/json",
"context_obj": [
{ "key": "quick-brick-login-flow.access_token", "type": "ctx", "required": true }
]
}
}
}

Endpoint descriptor fields

FieldTypeRequiredNotes
namestringyesHuman-readable label shown in Zapp Studio
method"get" or "post"yesHTTP method the Zapp client uses when calling this endpoint
mime_typestringyesContent-Type the endpoint returns. Usually "application/json". Use "application/vnd+applicaster.pipes2+json" when the endpoint returns a Pipes2 feed directly. Use "application/x-www-form-urlencoded" for form-post endpoints.
context_objarraynoList of context keys the Zapp client should inject into requests. Omit or set to [] when no context is needed.
modifiersarraynoList of EndpointModifier objects. Each modifier declares a query param that editors can configure in Zapp Studio when setting up a feed. Omit when the endpoint accepts no configurable query params.

Context object item fields

Each item in context_obj declares one context key:

FieldTypeRequiredNotes
keystringyesContext key name — must match a key from the available context keys or a plugin-supplied key
type"ctx" or "bodyParam"yesDelivery mechanism. "ctx" = sent as base64url-encoded JSON in the ctx query parameter. "bodyParam" = sent as a request body parameter.
requiredbooleannoWhether the key must be present. Defaults to false.
mapperstring or nullnoAn alias the client uses to remap the key name before sending. null means no remapping. Omit unless a non-null alias is needed.

Modifiers

Modifiers let your DSP advertise the optional query parameters an endpoint accepts. Zapp Studio renders a UI control for each modifier, allowing editors to configure query param values when setting up a feed — without needing to know the underlying param names.

EndpointModifier shape

interface ModifierOption {
value: string;
label: string;
}

interface EndpointModifier {
id: string; // unique stable identifier
name: string; // human-readable name shown in Zapp Studio
params: Array<{
key: string; // query param name appended to the feed URL
ui: {
label: string;
placeholder?: string;
options?: ModifierOption[]; // select modifiers only
};
schema: {
type: 'string' | 'number' | 'boolean';
default?: string | number | boolean;
enum?: string[]; // select modifiers only
};
}>;
}

Modifier types

Use the four creator functions from @lib/utils — they produce the correct shape automatically:

FunctionStudio controlUse for
createTextModifierText inputFree-form string params (e.g. asset_type, genre_id)
createNumberModifierNumber inputNumeric params (e.g. limit, page, season number)
createBooleanModifierToggleBoolean flags (e.g. include_total, loadPlaylist)
createSelectModifierDropdownParams with a fixed set of values (e.g. sort order, filter)

All four accept:

FieldTypeRequiredNotes
idstringyesUnique stable identifier
namestringyesLabel shown in Zapp Studio
keystringyesQuery param name
labelstringyesShort UI label
defaultValueanynoPre-filled value
placeholderstringnoInput placeholder text
optionsModifierOption[]select only[{ value, label }, ...]

Example:

const LIMIT_MODIFIER = createNumberModifier({
id: 'number_limit',
name: 'Result Limit',
key: 'limit',
defaultValue: 20,
label: 'Limit',
placeholder: '20',
});

const SORT_MODIFIER = createSelectModifier({
id: 'select_sort',
name: 'Sort Order',
key: 'order',
options: [
{ value: 'asc', label: 'Ascending' },
{ value: 'desc', label: 'Descending' },
],
label: 'Sort',
});

const endpoints = {
endpoints: {
'https://{{host}}/my-dsp/episodes': {
name: 'My DSP — Episodes',
method: 'get',
mime_type: 'application/json',
context_obj: [],
modifiers: [LIMIT_MODIFIER, SORT_MODIFIER],
},
},
};

Modifiers vs URL path params

Endpoint URLs may also contain {{param_name}} path placeholders — e.g. https://{{host}}/my-dsp/series/{{series_id}}/episodes. These are not modifiers. They are Pipes2 Feed Locator dynamic segments bound to referring-entry fields. Modifiers are query params only — appended as ?key=value by Zapp Studio when an editor configures the feed.

Cloning modifiers when building dynamically

When building the manifest at request time, always deep-clone shared modifier arrays before attaching them to endpoints, so each endpoint gets an independent copy:

const cloneModifiers = (modifiers = []) =>
modifiers.map((modifier) => ({
...modifier,
params: modifier.params.map((param) => ({
...param,
ui: {
...param.ui,
...(param.ui.options ? { options: param.ui.options.map((o) => ({ ...o })) } : {}),
},
schema: {
...param.schema,
...(param.schema.enum ? { enum: [...param.schema.enum] } : {}),
},
})),
}));

// When resolving the manifest:
const cloned = cloneModifiers(value.modifiers || []);
newEndpoints[url] = {
...value,
context_obj: [...value.context_obj],
...(cloned.length ? { modifiers: cloned } : {}),
};

Implementing the manifest route

1. Define a static base manifest

Define each endpoint URL using {{host}} as a placeholder for the service hostname. This keeps the manifest portable across environments:

const endpoints = {
endpoints: {
'https://{{host}}/my-dsp': {
name: 'My DSP — Collection Feed',
method: 'get',
mime_type: 'application/json',
context_obj: [],
modifiers: [LIMIT_MODIFIER, SORT_MODIFIER],
},
'https://{{host}}/my-dsp/media': {
name: 'My DSP — Video Preload',
method: 'get',
mime_type: 'application/json',
context_obj: [],
},
},
};

2. Resolve {{host}} at request time

When the manifest is requested, replace {{host}} with the real hostname from the incoming request URL:

function resolveManifest(endpointsJson, currentRoute) {
const host = new URL(currentRoute).hostname;
const resolved = {};
for (const [url, meta] of Object.entries(endpointsJson.endpoints)) {
resolved[url.replace('{{host}}', host)] = meta;
}
return { endpoints: resolved };
}

Absolute URLs — those that don't contain {{host}} — are passed through unchanged. Use absolute URLs for third-party endpoints your DSP proxies but does not own.

3. Append context keys based on enabled features

Declare only the context keys that are actually enabled for this deployment. Appending them at request time rather than hard-coding them into the static definition keeps the manifest accurate across different configurations:

function buildManifest(baseEndpoints, currentRoute, config) {
const manifest = resolveManifest(baseEndpoints, currentRoute);

const mediaUrl = Object.keys(manifest.endpoints).find(u => u.endsWith('/my-dsp/media'));

// Only declare the auth token key when authentication is enabled
if (config.authEnabled && mediaUrl) {
manifest.endpoints[mediaUrl].context_obj.push({
key: 'quick-brick-login-flow.access_token',
type: 'ctx',
required: true,
});
}

// Only declare the geo token key when geo-restriction is enabled
if (config.geoLocationEnabled) {
for (const url of Object.keys(manifest.endpoints)) {
manifest.endpoints[url].context_obj.push({
key: 'signedDeviceInfoToken',
type: 'ctx',
required: false,
});
}
}

return manifest;
}

4. Register the route

Use your project's versioning convention. Add a // Zapp manifest comment for discoverability:

// Zapp manifest
app.get('/v1/endpoints-manifest', (req, res) => {
const currentRoute = req.protocol + '://' + req.get('host') + req.originalUrl;
const manifest = buildManifest(endpoints, currentRoute, config);
res.json(manifest);
});

Services with multiple API versions

If your service exposes multiple API versions, export a separate manifest for each and register a separate route:

const endpointsV1 = { endpoints: { 'https://{{host}}/v1/my-dsp': { /* ... */ } } };
const endpointsV2 = { endpoints: { 'https://{{host}}/v2/my-dsp': { /* ... */ } } };

// Zapp manifest
app.get('/v1/endpoints-manifest', (req, res) => { /* resolve endpointsV1 */ });
// Zapp manifest
app.get('/v2/endpoints-manifest', (req, res) => { /* resolve endpointsV2 */ });

Registering in Zapp Studio

  1. Deploy your DSP so the manifest route is reachable over HTTPS.
  2. In Zapp Studio, go to Data Sources → Endpoints.
  3. Add the manifest URL, e.g. https://my-dsp.example.com/v1/endpoints-manifest.
  4. Zapp fetches the manifest and auto-populates all declared endpoints, their context-key requirements, and any editor-configurable modifier fields.
tip

Use the narrowest possible Endpoint prefix for your service. Where Endpoint prefixes overlap, only the longest matching Endpoint supplies context keys to a given feed.

Context keys reference

See Available Context Keys for the full list of built-in SDK keys you can declare in context_obj. Plugin-supplied keys (such as quick-brick-login-flow.access_token) are documented in the relevant plugin's manifest.

Your DSP exposes a GET /v1/endpoints-manifest route. When you register that URL in Zapp Studio, Zapp fetches it once and auto-populates every endpoint your service declares — including which context keys each endpoint requires.

The manifest response is a JSON object that maps fully-resolved endpoint URLs to their metadata:

{
"endpoints": {
"https://my-dsp.example.com/my-dsp": {
"name": "My DSP — Collection Feed",
"method": "get",
"mime_type": "application/json",
"context_obj": []
},
"https://my-dsp.example.com/my-dsp/media": {
"name": "My DSP — Video Preload",
"method": "get",
"mime_type": "application/json",
"context_obj": [
{ "key": "quick-brick-login-flow.access_token", "type": "ctx", "required": true }
]
}
}
}

Endpoint descriptor fields

FieldTypeRequiredNotes
namestringyesHuman-readable label shown in Zapp Studio
method"get" or "post"yesHTTP method the Zapp client uses when calling this endpoint
mime_typestringyesContent-Type the endpoint returns. Usually "application/json". Use "application/vnd+applicaster.pipes2+json" when the endpoint returns a Pipes2 feed directly. Use "application/x-www-form-urlencoded" for form-post endpoints.
context_objarraynoList of context keys the Zapp client should inject into requests. Omit or set to [] when no context is needed.

Context object item fields

Each item in context_obj declares one context key:

FieldTypeRequiredNotes
keystringyesContext key name — must match a key from the available context keys or a plugin-supplied key
type"ctx" or "bodyParam"yesDelivery mechanism. "ctx" = sent as base64url-encoded JSON in the ctx query parameter. "bodyParam" = sent as a request body parameter.
requiredbooleannoWhether the key must be present. Defaults to false.
mapperstring or nullnoAn alias the client uses to remap the key name before sending. null means no remapping. Omit unless a non-null alias is needed.

Implementing the manifest route

1. Define a static base manifest

Define each endpoint URL using {{host}} as a placeholder for the service hostname. This keeps the manifest portable across environments:

const endpoints = {
endpoints: {
'https://{{host}}/my-dsp': {
name: 'My DSP — Collection Feed',
method: 'get',
mime_type: 'application/json',
context_obj: [],
},
'https://{{host}}/my-dsp/media': {
name: 'My DSP — Video Preload',
method: 'get',
mime_type: 'application/json',
context_obj: [],
},
},
};

2. Resolve {{host}} at request time

When the manifest is requested, replace {{host}} with the real hostname from the incoming request URL:

function resolveManifest(endpointsJson, currentRoute) {
const host = new URL(currentRoute).hostname;
const resolved = {};
for (const [url, meta] of Object.entries(endpointsJson.endpoints)) {
resolved[url.replace('{{host}}', host)] = meta;
}
return { endpoints: resolved };
}

Absolute URLs — those that don't contain {{host}} — are passed through unchanged. Use absolute URLs for third-party endpoints your DSP proxies but does not own.

3. Append context keys based on enabled features

Declare only the context keys that are actually enabled for this deployment. Appending them at request time rather than hard-coding them into the static definition keeps the manifest accurate across different configurations:

function buildManifest(baseEndpoints, currentRoute, config) {
const manifest = resolveManifest(baseEndpoints, currentRoute);

const mediaUrl = Object.keys(manifest.endpoints).find(u => u.endsWith('/my-dsp/media'));

// Only declare the auth token key when authentication is enabled
if (config.authEnabled && mediaUrl) {
manifest.endpoints[mediaUrl].context_obj.push({
key: 'quick-brick-login-flow.access_token',
type: 'ctx',
required: true,
});
}

// Only declare the geo token key when geo-restriction is enabled
if (config.geoLocationEnabled) {
for (const url of Object.keys(manifest.endpoints)) {
manifest.endpoints[url].context_obj.push({
key: 'signedDeviceInfoToken',
type: 'ctx',
required: false,
});
}
}

return manifest;
}

4. Register the route

Use your project's versioning convention. Add a // Zapp manifest comment for discoverability:

// Zapp manifest
app.get('/v1/endpoints-manifest', (req, res) => {
const currentRoute = req.protocol + '://' + req.get('host') + req.originalUrl;
const manifest = buildManifest(endpoints, currentRoute, config);
res.json(manifest);
});

Services with multiple API versions

If your service exposes multiple API versions, export a separate manifest for each and register a separate route:

const endpointsV1 = { endpoints: { 'https://{{host}}/v1/my-dsp': { ... } } };
const endpointsV2 = { endpoints: { 'https://{{host}}/v2/my-dsp': { ... } } };

// Zapp manifest
app.get('/v1/endpoints-manifest', (req, res) => { /* resolve endpointsV1 */ });
// Zapp manifest
app.get('/v2/endpoints-manifest', (req, res) => { /* resolve endpointsV2 */ });

Registering in Zapp Studio

  1. Deploy your DSP so the manifest route is reachable over HTTPS.
  2. In Zapp Studio, go to Data Sources → Endpoints.
  3. Add the manifest URL, e.g. https://my-dsp.example.com/v1/endpoints-manifest.
  4. Zapp fetches the manifest and auto-populates all declared endpoints and their context-key requirements.
tip

Use the narrowest possible Endpoint prefix for your service. Where Endpoint prefixes overlap, only the longest matching Endpoint supplies context keys to a given feed.

Context keys reference

See Available Context Keys for the full list of built-in SDK keys you can declare in context_obj. Plugin-supplied keys (such as quick-brick-login-flow.access_token) are documented in the relevant plugin's manifest.