Using the in-store media display API (Display API)
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
| Endpoint | Purpose | Authentication |
|---|---|---|
POST /api/v1/displays/pair | Exchange the 6-digit pairing code for a device token | None (the code authenticates) |
POST /api/v1/displays/{displayId}/heartbeat | Report the device is alive (used for online/offline determination) | Device token |
GET /api/v1/displays/{displayId}/playlist | Retrieve the ordered list of media that should be played | Device token |
POST /api/v1/displays/{displayId}/impressions | Report playback results (plays) in a batch | Device 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.
- When the store owner registers a new display on the dashboard, a 6-digit pairing code is displayed (valid for 10 minutes, single use)
- Enter the code on the Android app's first-run screen
- Android calls
POST /api/v1/displays/pairand receives a token and adisplayId - 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 provided401 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
itemsarray, and when you reach the end, loop back to the beginning - Caching — as long as
hashHintdoes not change, there is no need to re-download the media file. Local caching is recommended. WhenhashHintchanges, re-fetch it - SAS URL expiry —
mediaUrlis valid for 1 hour. Attempting to download with an expired URL returns 403, so re-fetch the playlist at thepollIntervalSecondsinterval 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,
itemsis 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 flag —
trueif the slot was played to the end,falseif 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 recordedduplicates— number already processed (duplicates from resends)rejected— number that could not be processed, e.g. due to a missing required fielderrors— an array of rejection reasons (for debugging; contents may change between versions)
Typical implementation flow (Android)
- First run — if no token exists locally, show the pairing screen. Enter code →
POST /pair→ obtain token → save - Steady-state run loop
- Call
GET /playlistat startup and every 60 seconds thereafter - Re-download items whose
hashHintdiffers from last time; load unchanged items from cache - Play images/videos full-screen in the order received, advancing after each
durationSeconds - When you reach the last item, return to the beginning
- Call
- In parallel — call
POST /heartbeatevery 60 seconds to tell the server the device is alive - In parallel — record each played slot in a local queue and batch-send with
POST /impressionsevery 60 seconds - Error handling
401— token invalid. Return to the pairing screen403— 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
frequencyCapPerHouris 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
-
Using the Store Information APIA guide to the REST API for fetching and updating a store's basic information (store name, store type, contact details, and address). Lets you implement a store information editing screen from token-authenticated clients such as staff apps.
-
PosTransactionDto specification — field reference for transaction dataA complete field reference for PosTransactionDto, the canonical model for the transaction data ReceiptRoller handles. For each category — identifiers, dates, amounts, line items, payments, staff, status, and CRM linkage — it summarizes the field names, types, meanings, and how each POS vendor populates them. A reference for developers and external-system integrators. Also referenced from the Smaregi and Square mapping articles.
-
Purchase and receipt data integrationExplains the structure of the purchase and receipt data ReceiptRoller handles, how to retrieve it, related scopes, Webhooks, and common use cases.
-
Using the Business Hours APIA guide to ReceiptRoller's Business Hours API (/api/v1/stores/{storeId}/business-hours). Covers retrieving and updating per-day business hours, registering special business days (temporary closures and hour changes), configuring a store's workable hours (the upper bound for shift creation), and determining whether the store is currently open.
-
Using the Orders / OMS APIA guide to CRUD operations on the orders under a business account using ReceiptRoller's Orders / OMS API (/api/v1/orders). Covers creating, updating, transitioning status (confirm, process, cancel), and deleting orders, plus the flow for Android / iOS apps and server integrations.