M Motionworks Population Intelligence

Displays On Demand asset lifecycle

This page covers the CRUD lifecycle of a Displays On Demand asset: create, list, filter, summarize, read, refine, and archive. For the credit costs associated with each operation, see Purchase flow and credits. For the measurement queue and poll contract, see Measurements.

All endpoints are on the v2 base https://api2.mworks.com under the /v2/displays/* path. The base per-endpoint cost is 1 credit (displays_asset_crud); measurement purchases carry additional credits. Full request and response schemas are in the Displays API reference.

Identifier semantics

Every asset carries two identifiers with distinct issuance rules:

  • asset_id: a Motionworks-issued string identifier (for example vcaJDVDYWSSMHAXVEFYW1DVD2PPFD). It is assigned at creation and is stable for the life of the asset. All CRUD and measurement endpoints key off asset_id.
  • display_id: a Motionworks-issued String. It is minted only when the asset reaches final_measured. Before that state, display_id is absent. Once minted, it makes the asset joinable to the broader Displays inventory surface (including Viewcast Profiles).

This split exists because an asset in draft or draft_measured state is not yet a published display. The display_id is the join key to the published inventory world.

The request shape

The create request body is a strict object with classification, anchor, and related_parties at the top level. Optional fields are display (the display descriptor) and user_reference (free-text caller reference). Unknown fields return 400.

Field Type Meaning
classification String Roadside or Place-Based (immutable after creation). Required.
anchor Object { "lat": number, "lon": number } reference point for the face, used for viewshed construction. Both lat and lon are NUMERIC. Required.
related_parties Array Caller-side identifiers. Each entry is a flat object with role (for example 'owner'), party_name (required), party_display_id (from the face's face_id; together with spot_id it forms the duplicate-guard key, so a pair already registered to another of your assets returns 409), and spots[] (each with spot_id, from the face's spot_id). Required.
display Object Display descriptor (for example height_in, width_in, digital, orientation, media_name). Sparse; all sub-fields optional.
user_reference String Free-text caller reference; null is accepted. Optional.

The anchor and display attributes can be refined after creation (subject to the 100 m guardrail below). The classification field is immutable: once set at creation, a PATCH that changes it returns 400.

Create an asset

POST /v2/displays/assets

Creation is synchronous. The endpoint returns 200 OK with the created asset. There is no Idempotency-Key header: a plain retry creates a second asset, subject to the 409 duplicate-face guard below.

curl -X POST https://api2.mworks.com/v2/displays/assets \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{
    "classification": "Roadside",
    "anchor": { "lat": 34.0901, "lon": -118.3839 },
    "related_parties": [
      {
        "role": "owner",
        "party_name": "Console caller",
        "party_display_id": "my-face-4471",
        "spots": [ { "spot_id": "my-spot-9902" } ]
      }
    ],
    "display": {
      "orientation": 180.0,
      "height_in": 120.0,
      "width_in": 240.0,
      "digital": false
    }
  }'

Response (200):

{
  "asset_id": "vcaJDVDYWSSMHAXVEFYW1DVD2PPFD",
  "state": "draft",
  "row_version": 1
}

Duplicate-face guard: if your organization already has an asset with the same party_display_id and spot_id (carried in the related_parties[] entry with role: 'owner'), the create returns 409. Both fields are optional; if you omit them, the guard cannot fire and a retry creates a second asset. Send both identifiers whenever you have them.

No create-time enrichment: asset creation writes the asset and its ledger row and returns. The enrichment dispatch fires from the measurement purchase, not from create.

List and filter assets

GET /v2/displays/assets

Supports filtering by state and classification. Returns a paginated list of assets.

curl https://api2.mworks.com/v2/displays/assets?state=draft \
  -H "Authorization: Bearer ***"

Common query parameters include state (draft, draft_measured, final_measured) and classification (Roadside, Place-Based).

Summary counts

GET /v2/displays/assets/summary

Returns aggregate counts of assets by state. Useful for dashboards and for confirming that create and archive operations moved counts as expected.

Read a single asset

GET /v2/displays/assets/{asset_id}

Returns the full asset record, including current state, display attributes, row_version, and (if present) the display_id.

curl https://api2.mworks.com/v2/displays/assets/vcaJDVDYWSSMHAXVEFYW1DVD2PPFD \
  -H "Authorization: Bearer ***"

Refine an asset

PATCH /v2/displays/assets/{asset_id}

You can update the anchor location and display attributes while the asset is in draft state (and before a final measurement is purchased). Two guardrails apply:

  1. 100 m anchor-move limit: the new anchor must be within 100 m (haversine distance) of the existing anchor. A PATCH that moves the anchor farther returns 400.
  2. Classification immutability: classification cannot be changed after creation. A PATCH that attempts to change it returns 400.

Concurrency is handled for you: the API guards each write with the asset's current version. You do not send row_version in the request body; it is rejected with 400 (unsupported fields). The allowed request-body fields are anchor, related_parties, display, user_reference, publication, and auto_progress_final. A publication toggle is mutually exclusive with any refine field; combining them in one request returns 400. If another caller modified the asset in the meantime, the PATCH returns 409 (stale version); re-read the asset and retry.

Display merge semantics: the display object is merged per-key on PATCH, following RFC 7386 (JSON Merge Patch). Keys present in the request overwrite the stored value; keys absent from the request are no-ops and keep their stored value. An explicit null on a display key deletes that key: this is the only way to clear a previously-set display field. display: {} is a valid no-op. The merged result is validated against the strict schema, so unknown keys still return 400. A single-key refine such as display: { "height_in": 130.0 } updates only height_in and leaves sibling fields intact.

By contrast, related_parties keeps wholesale-replace semantics: a PATCH that includes it replaces the entire stored array.

curl -X PATCH https://api2.mworks.com/v2/displays/assets/$ASSET_ID \
  -H "Authorization: Bearer ***" \
  -H "Content-Type: application/json" \
  -d '{
    "anchor": { "lat": 34.0902, "lon": -118.3840 },
    "display": { "height_in": 130.0 }
  }'

Response (200): the updated asset record with a server-incremented row_version.

A PATCH within 100 m returns 200; a PATCH that changes classification returns 400; a PATCH that toggles publication while the asset is in draft returns 400.

Archive an asset

DELETE /v2/displays/assets/{asset_id}

Archives the asset. After deletion, a GET on the same asset_id returns 404. The archive is reflected immediately in summary counts.

curl -X DELETE https://api2.mworks.com/v2/displays/assets/$ASSET_ID \
  -H "Authorization: Bearer ***"

Response: 204 No Content. A subsequent GET returns 404.

DELETE returns 204, an immediate GET returns 404, and summary counts decrement accordingly.

What to read next