Using the in-store media display API (Display API)

API Display Media Android Signage Pairing Retail Media Impressions

Purpose of this guide

This guide covers the steps to implement an app that plays a media loop (images and videos) on an in-store Android device using ReceiptRoller's in-store media display API (Display API). It is a guide for delivering ReceiptRoller retail media and campaign videos on a kiosk that doubles as a POS terminal, or on dedicated signage hardware (such as a wall-mounted tablet).

Prerequisites

  • We do not assume an OAuth flow that opens a browser on the Android device. Each device authenticates with a long-lived device token
  • You obtain the device token through the "pairing" process. When the store owner enters the 6-digit code generated on the dashboard into the Android app, a per-device token is issued
  • Base URL: https://receiptroller.io

Endpoints

EndpointPurposeAuthentication
POST /api/v1/displays/pairExchange the 6-digit pairing code for a device tokenNone (the code authenticates)
POST /api/v1/displays/{displayId}/heartbeatReport the device is alive (used for online/offline determination)Device token
GET /api/v1/displays/{displayId}/playlistRetrieve the ordered list of media that should be playedDevice token
POST /api/v1/displays/{displayId}/impressionsReport playback results (plays) in a batchDevice token

Pairing flow

Each device pairs only once. The token is persisted on the Android side and attached to the Authorization: Bearer {token} header on all subsequent requests.

  1. When the store owner registers a new display on the dashboard, a 6-digit pairing code is displayed (valid for 10 minutes, single use)
  2. Enter the code on the Android app's first-run screen
  3. Android calls POST /api/v1/displays/pair and receives a token and a displayId
  4. Android persists the token and displayId locally (encryption recommended)

Request example:

POST /api/v1/displays/pair
Content-Type: application/json
User-Agent: RRDisplay/1.0 (Android 14; Pixel Tablet)

{
  "code": "123456",
  "screenWidth": 1920,
  "screenHeight": 1080,
  "supportsVideo": true,
  "supportsAudio": false
}

Response example (200):

{
  "displayId": "a1b2c3d4-...",
  "organizationId": "7d325d1d-...",
  "deviceToken": "dvc_3f9c1a8b7e2d6f4a1b8c5d2e9f7a3b6c"
}

Error responses:

  • 400 missing_code — the code field was not provided
  • 401 invalid_or_expired_code — the code is wrong, has passed its 10-minute validity, or has already been used

Important:

  • The token can only be obtained from this response. The server stores only a hash of it, so if you lose it, delete the display on the dashboard and pair again
  • A single pairing code can be used only once. When pairing again, issue a new code

Heartbeat

Tells the server that the device is online. The "online/offline" display on the dashboard is based on this timestamp (treated as online if within 5 minutes of the last heartbeat).

POST /api/v1/displays/{displayId}/heartbeat
Authorization: Bearer dvc_3f9c1a8b...
User-Agent: RRDisplay/1.0 (Android 14; Pixel Tablet)

Response example (200):

{
  "ok": true,
  "serverTimeUtc": "2026-06-03T05:21:30Z",
  "nextHeartbeatSeconds": 60
}

Recommended call interval: follow nextHeartbeatSeconds in the response (currently 60). Because the server may change the value depending on polling load, we recommend referencing the response rather than hard-coding it.

Retrieving the playlist

The device retrieves the ordered list of media it should play. Each item includes a SAS-signed direct-access URL (valid for 1 hour).

GET /api/v1/displays/{displayId}/playlist
Authorization: Bearer dvc_3f9c1a8b...

Response example (200):

{
  "displayId": "a1b2c3d4-...",
  "generatedAt": "2026-06-03T05:21:30Z",
  "pollIntervalSeconds": 60,
  "items": [
    {
      "creativeId": "creative-001",
      "name": "Summer new-product campaign",
      "type": "Image",
      "mediaUrl": "https://strprdomnicon.blob.core.windows.net/creatives/...?sv=...&sig=...",
      "durationSeconds": 10,
      "hashHint": "creative-001_638549812340000000"
    },
    {
      "creativeId": "creative-002",
      "name": "New menu intro video",
      "type": "Video",
      "mediaUrl": "https://strprdomnicon.blob.core.windows.net/creatives/...?sv=...&sig=...",
      "durationSeconds": 30,
      "hashHint": "creative-002_638549812350000000"
    }
  ]
}

Operational guidance:

  • Playback order — play in the order of the items array, and when you reach the end, loop back to the beginning
  • Caching — as long as hashHint does not change, there is no need to re-download the media file. Local caching is recommended. When hashHint changes, re-fetch it
  • SAS URL expirymediaUrl is valid for 1 hour. Attempting to download with an expired URL returns 403, so re-fetch the playlist at the pollIntervalSeconds interval to obtain fresh URLs
  • durationSeconds — images are 10 seconds (default); videos are their actual length. The app should switch to the next item after the specified number of seconds
  • Empty playlist — if no creative has yet been approved on the owner side, items is an empty array. When empty, we recommend showing a black screen or a "preparing" placeholder

Reporting playback results (impressions)

For each played slot, the Android app batch-sends "when, which creative, and how much was played" to the server. Phase 3 / t-e9a2f501.

POST /api/v1/displays/{displayId}/impressions
Authorization: Bearer dvc_3f9c1a8b...
Content-Type: application/json

{
  "events": [
    {
      "eventId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
      "creativeId": "creative-001",
      "campaignId": "camp-001",
      "placementId": "place-001",
      "playedAtUtc": "2026-06-03T05:20:00Z",
      "durationPlayedMs": 10000,
      "completed": true
    },
    {
      "eventId": "b2c3d4e5-f678-9012-3456-7890abcdef01",
      "creativeId": "creative-002",
      "campaignId": "camp-001",
      "placementId": "place-001",
      "playedAtUtc": "2026-06-03T05:20:10Z",
      "durationPlayedMs": 30000,
      "completed": true
    }
  ]
}

Response example (200):

{
  "accepted": 2,
  "duplicates": 0,
  "rejected": 0,
  "errors": []
}

Operational guidance:

  • Send frequency — we recommend sending every 60 seconds. Sending on every single slot play would create excessive network load
  • Batch limit — up to 500 events per request. Exceeding this returns 400 batch_too_large
  • eventId is generated by the client — UUID v4 recommended. The server uses this ID to guarantee idempotency. Even if you resend the same batch after going offline, the server counts the second occurrence silently as duplicates
  • completed flagtrue if the slot was played to the end, false if it was cut off partway. Used for completion-rate analysis
  • Offline handling — when the network fails, keep the events in a local queue and resend after recovery. Thanks to idempotency, duplicate sends are safe
  • placementId / campaignId are optional — they are not directly included in the playlist items, but if the Android app understands the Phase 2 playlist structure, providing them increases the granularity of analytics. It is still accepted if they are empty

Response fields:

  • accepted — number of events newly recorded
  • duplicates — number already processed (duplicates from resends)
  • rejected — number that could not be processed, e.g. due to a missing required field
  • errors — an array of rejection reasons (for debugging; contents may change between versions)

Typical implementation flow (Android)

  1. First run — if no token exists locally, show the pairing screen. Enter code → POST /pair → obtain token → save
  2. Steady-state run loop
    1. Call GET /playlist at startup and every 60 seconds thereafter
    2. Re-download items whose hashHint differs from last time; load unchanged items from cache
    3. Play images/videos full-screen in the order received, advancing after each durationSeconds
    4. When you reach the last item, return to the beginning
  3. In parallel — call POST /heartbeat every 60 seconds to tell the server the device is alive
  4. In parallel — record each played slot in a local queue and batch-send with POST /impressions every 60 seconds
  5. Error handling
    • 401 — token invalid. Return to the pairing screen
    • 403 — the token and the displayId in the path do not match (a bug). Take a log and re-pair
    • Network failure — keep playing the last-fetched playlist as-is, retain the impression queue, and retry with backoff

Security

  • The device token is long-lived. Store it in the device's encrypted storage such as the Android Keystore
  • The server stores only a SHA-256 hash of the token. A lost token is invalidated by deleting the display on the dashboard and re-pairing
  • One token = one display. Reusing the same token across multiple devices causes the dashboard's "last heartbeat" and "current resolution" to be overwritten per device, resulting in confusion

The future of this API

The current playlist supports "time-of-day and store targeting plus weighting loops driven by campaign × placement". Future phases will add:

  • A playback-results dashboard for store owners (impression count, completion rate, per-display fan-out)
  • Enforcement of frequency caps (a placement's frequencyCapPerHour is currently configurable but is a no-op until the Phase 5 implementation)

The API schema maintains forward compatibility — the response shape of the four existing endpoints will not change in the future (additional fields are possible).

Related information

Published: 2026-06-03 Updated: 2026-07-05
このトピックについて
開発者API
機能の詳細を見る
Tags
API (22) OAuth (15) Android (10) iOS (9) Webhook (6) Troubleshooting (5) api (5) App registration (4) POS Integration (4) Reference (4)
Related articles