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

```json
{
  "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](./available-context-keys.md) 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](./available-context-keys.md) 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

```typescript
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:

```typescript
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:

```typescript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
// 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:

```javascript
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](./available-context-keys.md) 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:

```json
{
  "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](./available-context-keys.md) 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](./available-context-keys.md) 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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
// 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:

```javascript
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](./available-context-keys.md) 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.
