Displays Search
Displays Search finds advertising faces by a closed set of typed filters. It searches two corpora: the public out-of-home inventory (the Geopath-licensed corpus of measured displays) and the private corpus of your organization's own display assets. One request walks both: with publication omitted, your own assets are served first, to exhaustion, then the public inventory continues the same walk, and the two legs are merged into one result set for one credit. Each result is one canonical display face, stamped with the corpus it came from.
The endpoint is on the v2 base https://api2.mworks.com under the /v2/displays/* path. For the full endpoint reference, see Displays API reference.
Endpoint
POST /v2/displays/search
- Auth: required. Send an org-scoped API key (
X-API-Keyheader) or a signed-in session (Authorization: Bearer ***. A request with no credentials gets 401; an anonymous trial session gets 403FEATURE_NOT_LICENSED. - Licensing: Display Search must be enabled for your organization; unlicensed organizations get 403
FEATURE_NOT_LICENSED. - Cost: 1 credit per call (
displays_search), covering both legs of a merged walk.
Corpora and the merged walk
The publication field routes the search between the two corpora:
| Value | Corpus |
|---|---|
| Omitted | Fan-out over both corpora, merged. Your organization's own assets are walked first, to exhaustion, then the public inventory continues the same walk. One request, one credit. |
private |
Your organization's own display assets, across every lifecycle state. |
public |
The public out-of-home inventory (measured displays). |
Omitting publication is not the same as public. On a merged walk, results from both corpora can appear in one response; each row's publication field names the corpus it came from. Results are deduplicated across corpora with your own copy preferred: if the public inventory contains a display you also own, your organization's row is the one served.
Most filters are honored on both legs. A few are per-leg and 400 when the public leg is in scope: direct, urbanicity, elevation, and the private-only related_parties keys (media_name, status_type, unknown other_ids keys). The state filter is honored with all three lifecycle values on the private leg and honored as a constant on the public leg (public rows are all final_measured, so a draft-only request is a truthful empty result there, not an error).
Request body
The request body is a required strict object: a JSON body must be present, and unknown keys return 400. An explicit empty body {} returns the first page of the merged walk; a request with no body returns 400. Every filter is named for a canonical display field, so a value you read out of a response feeds straight back into the filter of the same name.
List-valued filters accept a single value or an array; an empty array returns 400. Range filters take a { "min": ..., "max": ... } object with at least one bound.
| Filter | Type | Meaning |
|---|---|---|
media_type_name |
String or Array | Media type: one or more of Display, Panel, Junior Poster, Poster, Bulletin, Mural. |
classification_type_name |
String or Array | Roadside or Place-Based, one or both. |
construction_type_name |
String or Array | Construction type (the 14-value closed set; for example Freestanding, Exterior Wall, Rooftop). |
illumination_type_name |
String or Array | Frontlit, Sunlit Ambient, Backlit or Artificial Ambient. |
digital |
Boolean | true for digital faces only, false for static only. Omit for both. |
placement_type_name |
String or Array | Placement type. Free string: your own vocabulary on your assets, the Geopath vocabulary on the public corpus; values are not mapped between the two. |
urbanicity |
String or Array | Urban or rural setting around the face. Your assets only; the public corpus rejects it with 400. |
place_id |
String or Array | Motionworks place-library id, exact equality. |
rotating, full_motion, partial_motion, interactive, audio |
Boolean | Display capability flags. |
height_in, width_in |
{ min, max } |
Face dimension range in inches; at least one bound required. |
elevation |
{ min, max } |
Elevation range. Your assets only; the public corpus rejects it with 400. |
orientation |
{ min, max } |
Facing range in degrees clockwise from north. Each bound must be within 0 to 360; out-of-range values return 400 (rejected, not clamped). |
related_parties |
Array | Party constraints; see the next section. |
geography |
Object | { "bbox": { "north": ..., "south": ..., "east": ..., "west": ... } } only. north must be greater than or equal to south. Geo attributes remain on results but are not searchable beyond the bounding box. |
publication |
String | Corpus router: omitted fans out over both corpora (see above); private scopes to your assets; public scopes to the public inventory. |
state |
String or Array | Asset lifecycle state: draft, draft_measured, final_measured. Honored with all three values on your assets; on the public corpus honored as a constant (see above). This filter is the asset lifecycle and is unrelated to the geographic state field on results (for example IL). |
direct |
Boolean | Direct-add copies only. Honored on your assets; sending it whenever the public corpus is in scope returns 400 naming it. |
limit |
Integer | Page size. Default 50, maximum 200. |
cursor |
String | Opaque cursor from the previous page's meta.pagination.next_cursor; see Pagination. |
Two name traps to know. status_type appears on results but has no matching filter. And state is a homonym: the state field on a result is geographic (for example IL), while the state filter is the asset lifecycle; a row's lifecycle state never appears on results, it is carried by status_type.
Rejected keys (strict schema)
The body is closed at every level. The following return 400, naming the offending key:
- Retired pre-MA-191 filter names:
media_type,classification_type,construction,illumination,place_type,place_nameas a filter,party,party_scoped_identifiers, andgeography.resolved_ids. - Free-text search:
q,query. - Raw query pass-through:
dsl. - Catalog status:
catalog_status. - Any other key not listed in the table above.
related_parties: AND, OR, no NOT
related_parties is an array of party-constraint elements. Each element names one organization constraint, with these keys:
| Key | Meaning |
|---|---|
party_name |
Party name. Matching is exact and case-sensitive: the value must equal the stored party name character for character. |
division |
The party's sub-entity. |
role |
One of owner, operator, manager, reseller, agency, platform, venue, registry, measurement_provider. |
party_display_id |
The party's own identifier for the display (a non-empty array of values). |
other_ids |
Generic party identifier lookup by key: frame, face and layout are the known keys on the public corpus (unknown keys 400 there); any key is honored on your assets. Values are non-empty arrays. |
spot_id |
Party-scoped spot ids, as a non-empty array; integer values are coerced to strings. Honored on both corpora. The public corpus indexes numeric ids only: a non-numeric value is rejected with 400 on a publication: "public" request, and on an omitted-publication (merged) request the 400 surfaces once the walk reaches the public leg, so a merged first page filled by your own assets can return 200 and the reject can arrive on a later page. |
media_name |
Party-authored media label. Your assets only; 400 on the public corpus. |
status_type |
Party vocabulary status. Your assets only; 400 on the public corpus. |
Identifier values must not contain (, ) or , (these are rejected at validation).
How the elements combine:
- AND within one element. Every key inside one element must hold on the same party.
{ "role": "owner", "party_name": "Clear Channel" }matches displays where one single party is both the owner and named Clear Channel. - OR across elements. Multiple elements are a disjunction. A display matches if any one element is satisfied by some party; two elements need not be satisfied by the same party.
- OR inside a key's values. An array value inside one key is a single terms clause:
{ "party_name": ["Clear Channel", "Outfront"] }matches a party named either, still within that one element's party.
There is no NOT operator anywhere in the grammar. For closed-vocabulary filters (media_type_name, classification_type_name, illumination_type_name, state) express a complement by listing every value except the one you want to exclude; open-vocabulary fields cannot be complemented.
Worked example
This request asks for digital Bulletins owned by Clear Channel's Los Angeles division, or any display where OUTFRONT is the manager:
{
"digital": true,
"media_type_name": "Bulletin",
"related_parties": [
{ "party_name": "Clear Channel", "division": "CLEAR CHANNEL - LOS ANGELES", "role": "owner" },
{ "party_name": "OUTFRONT", "role": "manager" }
]
}
A display matches when digital AND media_type_name hold on the row, and some single party satisfies the whole first element (name AND division AND role together), or some party satisfies the second element.
Example request
curl -X POST https://api2.mworks.com/v2/displays/search \
-H "X-API-Key: ***" \
-H "Content-Type: application/json" \
-d '{
"media_type_name": "Bulletin",
"limit": 1
}'
This request fans out over both corpora (no publication key), so the first page can carry your organization's own Bulletin assets and then public inventory Bulletins in one merged walk.
Response
The response is { "data": [...], "meta": {...}. Each row in data is one canonical display face with a publication field naming its corpus. Rows from both corpora take the same canonical form, with one structural difference: private-corpus rows omit the geo-enrichment and venue fields the public corpus carries (cbsa_code, cbsa_name, county_id, county_name, dma_id, dma_name, location_description, openooh_venue_id, place_name, place_type_name, postal_code, primary_artery, state), and additionally omit any display-attribute field the asset's own record does not populate. Optional fields are omitted rather than nulled on both corpora.
{
"data": [
{
"display_id": "dspABCD1234EFGH5678",
"publication": "public",
"status_type": "Active",
"media_type_name": "Bulletin",
"classification_type_name": "Roadside",
"digital": false,
"height_in": 168,
"width_in": 576,
"orientation": 180,
"lat": 34.0901,
"lon": -118.3839,
"primary_artery": "Sunset Blvd",
"dma_name": "Los Angeles",
"sources": [
{ "source_id": "operator-feed", "observed_at": "2026-08-30T06:15:00Z" }
],
"last_modified": "2026-08-30T06:15:00Z",
"related_parties": [
{
"role": "owner",
"party_name": "Great Outdoor",
"division": "GREAT OUTDOOR - LOS ANGELES",
"party_display_id": "GO-LA-0042",
"source_id": "operator-feed",
"observed_at": "2026-08-30T06:15:00Z"
}
]
}
],
"meta": {
"pagination": { "next_cursor": "eyJ2Ijoz...", "has_more": true, "count": 1 },
"provenance": {
"source": "geopath-displays+viewcast-asset-library",
"source_doc": "https://docs.mworks.com/docs/displays",
"methodology_version": "v1.0",
"data_vintage": "2026-08-30",
"data_freshness": "on-demand",
"data_latency_days": 0,
"data_maturity": "production"
},
"request_id": "req_e8281bd0f5a1",
"credits_used": 1
}
}
data[], one row per advertising face, with the canonical display fields: dimensions in inches, media, construction and illumination types, location, and the related parties with their own identifiers.meta.pagination, cursor pagination; see the next section.meta.provenance, the data source and vintage behind the answer. On a fan-out request this is the combined stamp (as above); a request scoped to one corpus carries that leg's single stamp.meta.request_id, correlates the call for support.meta.credits_used, credits charged for the call.
Pagination
meta.pagination carries next_cursor, has_more, and count (rows on this page). There is deliberately no total count. Page forward by passing next_cursor back as cursor on the next call, with the same filters, until has_more is false. Results are capped at 10,000 per query.
Cursors are versioned and corpus-aware. A cursor carries the walk state for the search that minted it, and three rules hold:
- A cursor is bound to the request's filter set. Replay it with different filters and the request returns 400; restart pagination with a fresh search instead.
- A cursor is tamper-evident. A cursor that was edited, or minted by a different search, returns 400 rather than serving results.
- A cursor is bound to its corpus walk. A cursor minted for one corpus walk (for example a
publication: "private"search) returns 400 when replayed on a different walk (for example an omitted-publication merged search or apublication: "public"search).
A cursor minted before the current grammar version also returns 400, asking you to restart pagination without it.
Known boundaries
Stated plainly, these are the current limits of the search surface:
- Cross-page dedup is partial. On a merged walk, deduplication is evaluated against the private rows served on the page where your private corpus hands off to the public walk. If your private corpus spans multiple pages, a display you own that also appears in the public inventory can appear once more at that handoff. Full-corpus deduplication is a planned follow-up.
direct: truematches no rows today. The direct-add predicate on the private corpus is being re-derived, so adirect: truerequest currently returns an empty result set rather than the intended direct-add copies.- The 10,000-result cap is silent. A walk that exhausts the cap ends with
has_more: falselike any other exhausted walk; there is no separate truncation signal yet. - Public-row timestamps are under correction. On public rows,
last_modifiedandsources[].observed_atare currently stamped with the request time rather than a document-grounded vintage; treat them as request stamps, not source-record change times, until the correction lands. (Private rows carry the asset's actual times.)
What to read next
- Displays On Demand - overview and endpoint map.
- Asset lifecycle - create, list, refine, archive recipes.
- Purchase flow and credits - the credit model.
- Displays API reference - auto-built endpoint documentation.