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
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Human-readable label shown in Zapp Studio |
method | "get" or "post" | yes | HTTP method the Zapp client uses when calling this endpoint |
mime_type | string | yes | Content-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_obj | array | no | List of context keys the Zapp client should inject into requests. Omit or set to [] when no context is needed. |
modifiers | array | no | List 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:
| Field | Type | Required | Notes |
|---|---|---|---|
key | string | yes | Context key name — must match a key from the available context keys or a plugin-supplied key |
type | "ctx" or "bodyParam" | yes | Delivery mechanism. "ctx" = sent as base64url-encoded JSON in the ctx query parameter. "bodyParam" = sent as a request body parameter. |
required | boolean | no | Whether the key must be present. Defaults to false. |
mapper | string or null | no | An 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:
| Function | Studio control | Use for |
|---|---|---|
createTextModifier | Text input | Free-form string params (e.g. asset_type, genre_id) |
createNumberModifier | Number input | Numeric params (e.g. limit, page, season number) |
createBooleanModifier | Toggle | Boolean flags (e.g. include_total, loadPlaylist) |
createSelectModifier | Dropdown | Params with a fixed set of values (e.g. sort order, filter) |
All four accept:
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | yes | Unique stable identifier |
name | string | yes | Label shown in Zapp Studio |
key | string | yes | Query param name |
label | string | yes | Short UI label |
defaultValue | any | no | Pre-filled value |
placeholder | string | no | Input placeholder text |
options | ModifierOption[] | 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
- Deploy your DSP so the manifest route is reachable over HTTPS.
- In Zapp Studio, go to Data Sources → Endpoints.
- Add the manifest URL, e.g.
https://my-dsp.example.com/v1/endpoints-manifest. - Zapp fetches the manifest and auto-populates all declared endpoints, their context-key requirements, and any editor-configurable modifier fields.
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
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | yes | Human-readable label shown in Zapp Studio |
method | "get" or "post" | yes | HTTP method the Zapp client uses when calling this endpoint |
mime_type | string | yes | Content-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_obj | array | no | List 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:
| Field | Type | Required | Notes |
|---|---|---|---|
key | string | yes | Context key name — must match a key from the available context keys or a plugin-supplied key |
type | "ctx" or "bodyParam" | yes | Delivery mechanism. "ctx" = sent as base64url-encoded JSON in the ctx query parameter. "bodyParam" = sent as a request body parameter. |
required | boolean | no | Whether the key must be present. Defaults to false. |
mapper | string or null | no | An 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
- Deploy your DSP so the manifest route is reachable over HTTPS.
- In Zapp Studio, go to Data Sources → Endpoints.
- Add the manifest URL, e.g.
https://my-dsp.example.com/v1/endpoints-manifest. - Zapp fetches the manifest and auto-populates all declared endpoints and their context-key requirements.
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.