> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pixalate.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pixalate.com/_mcp/server.

# Enrich one OpenRTB bid request in place

POST https://epg-api.pixalate.com/v1/enrich
Content-Type: application/json

The SSP splice path. The body is a standard OpenRTB 2.6 bid request; only the
documented input fields are read and everything else is preserved. On a hit, the
response is the same bid request with the program-identity fields of `app.content`
— `id`, `title`, `series`, `episode`, `season`, `genre`, `cat`, `len`, `channel`,
`livestream`, `contentrating`, `language` — replaced atomically by the single best
candidate: each is set to the resolved value or removed when resolved as absent, so
no stale program facts from the request survive next to the resolution.
Identity-neutral `app.content` fields and everything outside `app.content` are
preserved untouched, and `app.content.ext.openepg.attribution` is set. `season`
and `cat` belong to the replaced cluster but are never populated in v1, so a hit
always removes them. Preservation is in meaning, not bytes: re-serialization may
change encoding (key order, number formatting), never values — detect misses by
reading the `X-OpenEPG-Outcome` header, not by byte-comparing bodies.

On a miss — and on a degraded resolution where the channel resolved but nothing
airs at the lookup instant — the bid request comes back **unchanged**, HTTP 200.
A channel-only splice next to foreign program fields is never produced.

Nothing is ever added to the body: no confidence, reason, notices, or debug —
anything in this body must be assumed to enter the bidstream. The outcome and the
confidence of the chosen candidate travel in `X-OpenEPG-*` response headers.

Exactly one candidate is used, always; there are no query parameters on this route.
Passing a bid request that carries the needed identifier values (channel id,
channel name, or show title) raises the precision of the enrichment — an app-only
request on an aggregator app is a structured miss, never a guess. A supplied
`ext.pixalate.timestamp` is honored on this route and on its batch variant: the
resolution joins the schedule at that instant instead of at now, under the same
window and clamping rules the lookup route applies. An instant outside the
servable window is a 400 carrying `TIMESTAMP_OUT_OF_WINDOW`, never a silent
substitution of now.


Reference: https://docs.pixalate.com/api-reference/openepg/enrich

## Authentication

- `x-api-key` header (required) — Your Pixalate-provided API key, in the request header on every route. Credentials are never accepted in query parameters. An API key may be obtained by signing up for an account on the [Pixalate developer website](https://developer.pixalate.com).
- `Authorization` header (basic auth, required) — Username/password alternative to the API-key header. Headers only — credentials are never accepted in query parameters.

## Servers

- `https://epg-api.pixalate.com/v1` (Production., default)
- `https://dev-epg-api.pixalate.com/v1` (Development sandbox.)

## Request

### Body (application/json)

This endpoint expects an OpenRtbBidRequest.

- `app` (OpenRtbBidRequestApp, required)
- `device` (OpenRtbBidRequestDevice, required)
- `ext` (OpenRtbBidRequestExt, optional)

## Response

### 200

Enrichment performed. On a hit, the echoed bid request carries the resolved program in `app.content`; on a miss or a degraded resolution, the bid request is returned unchanged. Read the `X-OpenEPG-Outcome` header to distinguish the two without diffing the body.

- `app` (OpenRtbBidRequestApp, required)
- `device` (OpenRtbBidRequestDevice, required)
- `ext` (OpenRtbBidRequestExt, optional)

## Errors

### 400 Bad Request Error

Malformed request — a non-conforming body, a missing or empty required field, a read field of the wrong type, a timestamp violation (unparseable, more than 5 minutes in the future, or older than the 7-day window), a non-null `ext.pixalate` that is not itself an object, or an exceeded batch cap.

- `code` (enum, required) — Machine-readable error code. A code names a product condition the HTTP status cannot express; where there is no such distinction the code is the status name, so branch on the status first and on the code only for the values below. RATE_LIMITED is a 429 from a request-rate ceiling and clears within the window. BATCH_TOO_LARGE means the batch exceeded the documented item cap — split and retry. TIMESTAMP_OUT_OF_WINDOW means the instant requested lies outside the 7-day coverage window or more than 5 minutes in the future.
  - Allowed values: `BAD_REQUEST`, `BATCH_TOO_LARGE`, `TIMESTAMP_OUT_OF_WINDOW`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `PAYLOAD_TOO_LARGE`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`
- `message` (string, required)

### 401 Unauthorized Error

Missing or unknown credential.

- `code` (enum, required) — Machine-readable error code. A code names a product condition the HTTP status cannot express; where there is no such distinction the code is the status name, so branch on the status first and on the code only for the values below. RATE_LIMITED is a 429 from a request-rate ceiling and clears within the window. BATCH_TOO_LARGE means the batch exceeded the documented item cap — split and retry. TIMESTAMP_OUT_OF_WINDOW means the instant requested lies outside the 7-day coverage window or more than 5 minutes in the future.
  - Allowed values: `BAD_REQUEST`, `BATCH_TOO_LARGE`, `TIMESTAMP_OUT_OF_WINDOW`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `PAYLOAD_TOO_LARGE`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`
- `message` (string, required)

### 403 Forbidden Error

Known credential without entitlement to this product.

- `code` (enum, required) — Machine-readable error code. A code names a product condition the HTTP status cannot express; where there is no such distinction the code is the status name, so branch on the status first and on the code only for the values below. RATE_LIMITED is a 429 from a request-rate ceiling and clears within the window. BATCH_TOO_LARGE means the batch exceeded the documented item cap — split and retry. TIMESTAMP_OUT_OF_WINDOW means the instant requested lies outside the 7-day coverage window or more than 5 minutes in the future.
  - Allowed values: `BAD_REQUEST`, `BATCH_TOO_LARGE`, `TIMESTAMP_OUT_OF_WINDOW`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `PAYLOAD_TOO_LARGE`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`
- `message` (string, required)

### 413 Content Too Large Error

The request body exceeded the maximum number of bytes this route accepts. Applies to the single (non-batch) operations; well below what a real payload from this route ever produces, so it signals a malformed or abusive request rather than an ordinary large one.

- `code` (enum, required) — Machine-readable error code. A code names a product condition the HTTP status cannot express; where there is no such distinction the code is the status name, so branch on the status first and on the code only for the values below. RATE_LIMITED is a 429 from a request-rate ceiling and clears within the window. BATCH_TOO_LARGE means the batch exceeded the documented item cap — split and retry. TIMESTAMP_OUT_OF_WINDOW means the instant requested lies outside the 7-day coverage window or more than 5 minutes in the future.
  - Allowed values: `BAD_REQUEST`, `BATCH_TOO_LARGE`, `TIMESTAMP_OUT_OF_WINDOW`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `PAYLOAD_TOO_LARGE`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`
- `message` (string, required)

### 429 Too Many Requests Error

Rate limit exceeded. `Retry-After` says when the window clears.

- `code` (enum, required) — Machine-readable error code. A code names a product condition the HTTP status cannot express; where there is no such distinction the code is the status name, so branch on the status first and on the code only for the values below. RATE_LIMITED is a 429 from a request-rate ceiling and clears within the window. BATCH_TOO_LARGE means the batch exceeded the documented item cap — split and retry. TIMESTAMP_OUT_OF_WINDOW means the instant requested lies outside the 7-day coverage window or more than 5 minutes in the future.
  - Allowed values: `BAD_REQUEST`, `BATCH_TOO_LARGE`, `TIMESTAMP_OUT_OF_WINDOW`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `PAYLOAD_TOO_LARGE`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`
- `message` (string, required)

### 503 Service Unavailable Error

Serving infrastructure degraded — the API fails closed and never masquerades as a data miss. Retry after the indicated delay.

- `code` (enum, required) — Machine-readable error code. A code names a product condition the HTTP status cannot express; where there is no such distinction the code is the status name, so branch on the status first and on the code only for the values below. RATE_LIMITED is a 429 from a request-rate ceiling and clears within the window. BATCH_TOO_LARGE means the batch exceeded the documented item cap — split and retry. TIMESTAMP_OUT_OF_WINDOW means the instant requested lies outside the 7-day coverage window or more than 5 minutes in the future.
  - Allowed values: `BAD_REQUEST`, `BATCH_TOO_LARGE`, `TIMESTAMP_OUT_OF_WINDOW`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `PAYLOAD_TOO_LARGE`, `RATE_LIMITED`, `SERVICE_UNAVAILABLE`
- `message` (string, required)

## Types

### OpenRtbBidRequestApp

- `bundle` (string, required) — Required. App store bundle/ID. Alone it fully resolves single-channel apps; on an aggregator (multi-channel) app with no channel or show identifier alongside it, the result is a structured CHANNEL_UNRESOLVED miss, never a guess.
- `content` (OpenRtbBidRequestAppContent, optional)

### OpenRtbBidRequestDevice

- `ua` (string, required) — Required. Client User-Agent; the platform/OS signal is derived from it when device.os gives no signal.
- `os` (string, optional) — Explicit OS; overrides the UA-derived value. Unrecognized values yield no OS signal (all-platform weighting) rather than an error — unlike the parameterized lookup route, where an out-of-vocabulary os is rejected.
- `devicetype` (integer, optional) — Optional OpenRTB device type. Accepted and retained for forward compatibility; not consulted by resolution during the beta.

### OpenRtbBidRequestExt

- `pixalate` (OpenRtbBidRequestExtPixalate, optional)

### OpenRtbBidRequestAppContent

- `title` (string, optional) — Show/program title. Fuzzy match, scoped to the declared app's catalog; a resolved show always carries its channel association. When the channel path and the show path disagree, the channel + timestamp result wins and this input is marked CONFLICT in verbose debug (on the lookup route). Capped at 256 characters; longer values are truncated.
- `channel` (OpenRtbBidRequestAppContentChannel, optional)

### OpenRtbBidRequestExtPixalate

- `timestamp` (datetime, optional) — ISO 8601 UTC instant to resolve against. Absent means now. Up to 5 minutes in the future is clamped to now with a `TIMESTAMP_CLAMPED` notice; older than 7 days, or further ahead than the clamp, is a 400 carrying `TIMESTAMP_OUT_OF_WINDOW`. The lookup route reads the same instant from its own flat `timestamp` field.

### OpenRtbBidRequestAppContentChannel

- `id` (string, optional) — Native/platform channel identifier, matched exactly against known identifiers (substring containment as a fallback). Strictly supersedes channel.name — when present, the name is not consulted.
- `name` (string, optional) — Channel display name (fuzzy name match). Consulted only when channel.id is absent. Capped at 256 characters; longer values are truncated.

## Examples

### Hit: app.content replaced with the resolved program

**Response**

```json
{
  "app": {
    "bundle": "com.pluto.tv",
    "content": {
      "title": "SportsCenter",
      "channel": {
        "id": "eg-Tj5r8w",
        "name": "ESPN"
      },
      "contentrating": "TV-PG",
      "ext": {
        "openepg": {
          "attribution": "pixalate-openepg/v1"
        }
      },
      "genre": "Sports",
      "id": "ec-4Fq0X2"
    }
  },
  "device": {
    "ua": "Roku/DVP-13.0.0"
  }
}
```

**SDK Code**

```python Hit: app.content replaced with the resolved program
import requests

url = "https://epg-api.pixalate.com/v1/enrich"

headers = {"x-api-key": "<apiKey>"}

response = requests.post(url, headers=headers)

print(response.json())
```

```javascript Hit: app.content replaced with the resolved program
const url = 'https://epg-api.pixalate.com/v1/enrich';
const options = {method: 'POST', headers: {'x-api-key': '<apiKey>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Hit: app.content replaced with the resolved program
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://epg-api.pixalate.com/v1/enrich"

	req, _ := http.NewRequest("POST", url, nil)

	req.Header.Add("x-api-key", "<apiKey>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Hit: app.content replaced with the resolved program
require 'uri'
require 'net/http'

url = URI("https://epg-api.pixalate.com/v1/enrich")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<apiKey>'

response = http.request(request)
puts response.read_body
```

```java Hit: app.content replaced with the resolved program
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://epg-api.pixalate.com/v1/enrich")
  .header("x-api-key", "<apiKey>")
  .asString();
```

```php Hit: app.content replaced with the resolved program
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://epg-api.pixalate.com/v1/enrich', [
  'headers' => [
    'x-api-key' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp Hit: app.content replaced with the resolved program
using RestSharp;

var client = new RestClient("https://epg-api.pixalate.com/v1/enrich");
var request = new RestRequest(Method.POST);
request.AddHeader("x-api-key", "<apiKey>");
IRestResponse response = client.Execute(request);
```

```swift Hit: app.content replaced with the resolved program
import Foundation

let headers = ["x-api-key": "<apiKey>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://epg-api.pixalate.com/v1/enrich")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Miss: bid request returned unchanged

**Response**

```json
{
  "app": {
    "bundle": "com.pluto.tv",
    "content": {
      "channel": {
        "id": "espn"
      }
    }
  },
  "device": {
    "ua": "Roku/DVP-13.0.0"
  }
}
```

**SDK Code**

```python Miss: bid request returned unchanged
import requests

url = "https://epg-api.pixalate.com/v1/enrich"

headers = {"x-api-key": "<apiKey>"}

response = requests.post(url, headers=headers)

print(response.json())
```

```javascript Miss: bid request returned unchanged
const url = 'https://epg-api.pixalate.com/v1/enrich';
const options = {method: 'POST', headers: {'x-api-key': '<apiKey>'}};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Miss: bid request returned unchanged
package main

import (
	"fmt"
	"net/http"
	"io"
)

func main() {

	url := "https://epg-api.pixalate.com/v1/enrich"

	req, _ := http.NewRequest("POST", url, nil)

	req.Header.Add("x-api-key", "<apiKey>")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Miss: bid request returned unchanged
require 'uri'
require 'net/http'

url = URI("https://epg-api.pixalate.com/v1/enrich")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<apiKey>'

response = http.request(request)
puts response.read_body
```

```java Miss: bid request returned unchanged
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://epg-api.pixalate.com/v1/enrich")
  .header("x-api-key", "<apiKey>")
  .asString();
```

```php Miss: bid request returned unchanged
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://epg-api.pixalate.com/v1/enrich', [
  'headers' => [
    'x-api-key' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp Miss: bid request returned unchanged
using RestSharp;

var client = new RestClient("https://epg-api.pixalate.com/v1/enrich");
var request = new RestRequest(Method.POST);
request.AddHeader("x-api-key", "<apiKey>");
IRestResponse response = client.Execute(request);
```

```swift Miss: bid request returned unchanged
import Foundation

let headers = ["x-api-key": "<apiKey>"]

let request = NSMutableURLRequest(url: NSURL(string: "https://epg-api.pixalate.com/v1/enrich")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```

### Aggregator app with a channel identifier

**Request**

```json
{
  "app": {
    "bundle": "com.pluto.tv",
    "content": {
      "channel": {
        "id": "espn"
      }
    }
  },
  "device": {
    "ua": "Roku/DVP-13.0.0"
  }
}
```

**Response**

```json
{
  "app": {
    "bundle": "com.pluto.tv",
    "content": {
      "title": "SportsCenter",
      "channel": {
        "id": "eg-Tj5r8w",
        "name": "ESPN"
      },
      "contentrating": "TV-PG",
      "ext": {
        "openepg": {
          "attribution": "pixalate-openepg/v1"
        }
      },
      "genre": "Sports",
      "id": "ec-4Fq0X2"
    }
  },
  "device": {
    "ua": "Roku/DVP-13.0.0"
  }
}
```

**SDK Code**

```python Aggregator app with a channel identifier
import requests

url = "https://epg-api.pixalate.com/v1/enrich"

payload = {
    "app": {
        "bundle": "com.pluto.tv",
        "content": { "channel": { "id": "espn" } }
    },
    "device": { "ua": "Roku/DVP-13.0.0" }
}
headers = {
    "x-api-key": "<apiKey>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript Aggregator app with a channel identifier
const url = 'https://epg-api.pixalate.com/v1/enrich';
const options = {
  method: 'POST',
  headers: {'x-api-key': '<apiKey>', 'Content-Type': 'application/json'},
  body: '{"app":{"bundle":"com.pluto.tv","content":{"channel":{"id":"espn"}}},"device":{"ua":"Roku/DVP-13.0.0"}}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```go Aggregator app with a channel identifier
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://epg-api.pixalate.com/v1/enrich"

	payload := strings.NewReader("{\n  \"app\": {\n    \"bundle\": \"com.pluto.tv\",\n    \"content\": {\n      \"channel\": {\n        \"id\": \"espn\"\n      }\n    }\n  },\n  \"device\": {\n    \"ua\": \"Roku/DVP-13.0.0\"\n  }\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("x-api-key", "<apiKey>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(res)
	fmt.Println(string(body))

}
```

```ruby Aggregator app with a channel identifier
require 'uri'
require 'net/http'

url = URI("https://epg-api.pixalate.com/v1/enrich")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<apiKey>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"app\": {\n    \"bundle\": \"com.pluto.tv\",\n    \"content\": {\n      \"channel\": {\n        \"id\": \"espn\"\n      }\n    }\n  },\n  \"device\": {\n    \"ua\": \"Roku/DVP-13.0.0\"\n  }\n}"

response = http.request(request)
puts response.read_body
```

```java Aggregator app with a channel identifier
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://epg-api.pixalate.com/v1/enrich")
  .header("x-api-key", "<apiKey>")
  .header("Content-Type", "application/json")
  .body("{\n  \"app\": {\n    \"bundle\": \"com.pluto.tv\",\n    \"content\": {\n      \"channel\": {\n        \"id\": \"espn\"\n      }\n    }\n  },\n  \"device\": {\n    \"ua\": \"Roku/DVP-13.0.0\"\n  }\n}")
  .asString();
```

```php Aggregator app with a channel identifier
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://epg-api.pixalate.com/v1/enrich', [
  'body' => '{
  "app": {
    "bundle": "com.pluto.tv",
    "content": {
      "channel": {
        "id": "espn"
      }
    }
  },
  "device": {
    "ua": "Roku/DVP-13.0.0"
  }
}',
  'headers' => [
    'Content-Type' => 'application/json',
    'x-api-key' => '<apiKey>',
  ],
]);

echo $response->getBody();
```

```csharp Aggregator app with a channel identifier
using RestSharp;

var client = new RestClient("https://epg-api.pixalate.com/v1/enrich");
var request = new RestRequest(Method.POST);
request.AddHeader("x-api-key", "<apiKey>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"app\": {\n    \"bundle\": \"com.pluto.tv\",\n    \"content\": {\n      \"channel\": {\n        \"id\": \"espn\"\n      }\n    }\n  },\n  \"device\": {\n    \"ua\": \"Roku/DVP-13.0.0\"\n  }\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift Aggregator app with a channel identifier
import Foundation

let headers = [
  "x-api-key": "<apiKey>",
  "Content-Type": "application/json"
]
let parameters = [
  "app": [
    "bundle": "com.pluto.tv",
    "content": ["channel": ["id": "espn"]]
  ],
  "device": ["ua": "Roku/DVP-13.0.0"]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://epg-api.pixalate.com/v1/enrich")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```