Back home

API Reference

cueyou API guide

cueyou turns an Android phone into a programmable, full-screen takeover surface. Your code POSTs to the cueyou API, the server pushes via FCM, and the phone takes over its own screen with a title, a markdown description, an optional source chip, and a palette driven by urgency.

This document covers the public REST API. There is also a tRPC surface at /trpc/* used by the Android app and dashboard — that is not part of the public contract.

What's new

  • Rich HTML content — send an html body with an alert or prompt and it renders on the phone, with buttons that can answer a prompt, and an optional fullscreen mode where your page is the entire screen.
  • Delivery modes, quiet hours and a timed pause — users now choose how cueyou may reach them, and can pause everything for 1, 2, 8 or 24 hours. Calls made while delivery is off still return 200; see Operational notes.

Base URL

http
https://api.cueyou.app

Authentication

Every request needs an API key in the Authorization header:

http
Authorization: Bearer att_xxxxxxxxxxxxxxxxxxxxxxxx

Keys are minted in the dashboard. They start with att_ and are shown only once at creation — store them as secrets. A revoked or unknown key produces 401 {"error":"unauthorized"}. Firebase ID tokens are also accepted by the same handlers but are intended for the first-party app, not for integrations.

Endpoints at a glance

MethodPathPurposeBlocking?
GET/api/devicesList devices registered on the accountNo
POST/api/alertFire a screen-takeover alarm on a deviceNo
POST/api/promptAsk the device a question and wait for an answerYes (long-poll)

All bodies are JSON. All responses are JSON. All timestamps are ISO 8601 strings.

Shared concepts

Both /api/alert and /api/prompt accept the same optional fields for shaping how the takeover looks and feels.

Urgency

urgency is one of "info", "normal", or "critical". Defaults to "normal". It drives the takeover surface palette.

ValuePaletteUse when
infoMuted blueStatus updates, finished long-running tasks, FYIs.
normalIndigoDefault. Decisions and notifications that matter but aren't emergencies.
criticalRedProduction is on fire, destructive action gating, pages.

Source

source is a free-form string up to 80 chars, shown as a chip at the top of the takeover. Use it to identify the caller, e.g. "claude-code · pre-commit" or "github-actions · deploy".

Description & markdown

description is 0–2000 chars and rendered as markdown on the device. The supported subset is: bold, italic, inline code, ordered and unordered lists, fenced code blocks, and links. Images, tables, and raw HTML are not supported inside description. If you need richer layout or interactive buttons, use the separate html field (see Rich HTML content below); description is still shown as the plain-text fallback. Long descriptions scroll inside the takeover with a fade gradient signalling that there's more.

Rich HTML content

Alerts and prompts accept an optional html field. When present and allowed, the phone renders it inside the takeover instead of the Markdown description, so you can lay out a status card, a table, a chart made of CSS, or a set of custom buttons.

Availability: the API key must have Allow HTML content switched on. It is off by default and can only be changed by the account owner inside the cueyou app (an API key cannot change it). A request with html from a key that isn't allowed is rejected with 403 {"error":"html_not_allowed"} and nothing is sent.

FieldTypeRequiredNotes
htmlstringno1–50,000 characters. Sent with POST /api/alert or POST /api/prompt.
fullscreenbooleannoDefault false. Requires html — sending fullscreen: true without html is a 400. See Full-screen pages.

description is still required and is always your fallback: it is shown on older app versions, if the phone is offline when the HTML is fetched, and if the web view can't be shown.

How it appears. The takeover shows a fixed header — "Custom content from <your key's label>" — above your HTML. The label comes from the API key's name in the dashboard, not from anything you send, so recipients can always tell where the content came from. Your page cannot cover or restyle that header.

What your page can and can't do

Your HTML runs in a locked-down, sandboxed web view:

AllowedBlocked
HTML and CSS (inline <style>)Any network request: fetch, XMLHttpRequest, remote images/fonts/scripts/CSS
Inline JavaScript (<script>)Navigation: links, window.open, redirects, <iframe>, <form> submission
Images, fonts and audio embedded as data: URIsFile and content access, camera, microphone, location
Buttons that answer the prompt (below)Autoplaying media (needs a tap)

Because remote resources are blocked, embed everything you need (use data: URIs for images) and keep the page small.

On the lock screen, JavaScript is switched off and a note "Unlock to use buttons" is shown. Static HTML and CSS still render. Scripts and buttons work once the user unlocks the phone.

Buttons that answer a prompt

Inside your page, call the cueyou message channel to answer the prompt. The value comes back to your API call exactly like a text or option answer (status: "answered", answer: <your value>).

text
<div style="font-family: sans-serif; padding: 16px">
  <h2>Deploy v2.18.0 to production?</h2>
  <p>3 services · 12 commits · all checks green</p>

  <button onclick="answer('approve')">Approve</button>
  <button onclick="answer('reject')">Reject</button>
</div>

<script>
  function answer(value) {
    // `cueyou` exists once the phone is unlocked and the bridge is available.
    if (window.cueyou) {
      cueyou.postMessage(JSON.stringify({ type: "answer", value: value }));
    }
  }
</script>

Message format:

FieldTypeNotes
typestringMust be "answer".
valuestringUp to 1,000 characters. Becomes the answer returned to your /api/prompt call.

Messages must be JSON and under 2 KB; anything else is ignored. On alerts (which take no answer) HTML renders normally and answer messages are ignored.

Full-screen pages (fullscreen)

Add "fullscreen": true (per message, next to html) and your page becomes the whole screen: edge-to-edge, with the status and navigation bars hidden, and nothing of cueyou's drawn around it — no title, source chip, option buttons, "Dismiss" button or "Custom content from …" header. Your page is responsible for everything the user sees, including its own close button.

Because nothing identifies the sender on screen, only switch Allow HTML content on for API keys you fully trust.

What still works, invisibly, so a full-screen page can never trap the user:

SituationBehaviour
A ringing or vibrating alertThe first touch anywhere silences it (and is not passed to your page).
The user swipes BackThe alert is dismissed / the prompt is dismissed.
Your page wants to close itselfCall cueyou.postMessage(JSON.stringify({ type: "dismiss" })) from a button's click handler. It only counts right after a real tap.
A promptAnswer with the normal answer message (see above). A prompt is resolved once, whether by an answer or a dismissal.

Layout notes:

  • The page has no default padding (margin: 0). Use env(safe-area-inset-top), env(safe-area-inset-bottom) etc. to stay clear of camera cut-outs and gesture bars.
  • The background is transparent by default and shows the urgency colour; set your own background on html, body for a designed look.
  • On the lock screen scripts are off (as for all HTML): static HTML and CSS still render, but buttons and dismiss only work after unlock — the user can still swipe Back. You can detect this: window.cueyou is undefined when scripts can't reach the app.
  • If the page can't be shown (for example the phone is offline when it tries to fetch it), the normal cueyou takeover is shown instead, using your description.
  • Older app versions ignore fullscreen and show the normal layout.
bash
curl https://api.cueyou.app/api/alert \
  -H "Authorization: Bearer $CUEYOU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Build failed",
    "description": "main is red: 3 tests failing",
    "urgency": "critical",
    "fullscreen": true,
    "html": "<div style=\"min-height:100vh;display:flex;flex-direction:column;justify-content:center;align-items:center;background:#7f1d1d;color:#fff;font-family:sans-serif;text-align:center;padding:env(safe-area-inset-top) 24px env(safe-area-inset-bottom)\"><h1 style=\"font-size:48px;margin:0\">main is red</h1><p>3 tests failing</p><button style=\"margin-top:32px;padding:16px 32px;font-size:18px\" onclick=\"cueyou.postMessage(JSON.stringify({type:\\\"dismiss\\\"}))\">Got it</button></div>"
  }'

Example — prompt with HTML buttons

bash
curl https://api.cueyou.app/api/prompt \
  -H "Authorization: Bearer $CUEYOU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Deploy v2.18.0?",
    "description": "Approve the production deploy (plain-text fallback)",
    "source": "ci · deploy/production",
    "html": "<h2>Deploy v2.18.0?</h2><button onclick=\"cueyou.postMessage(JSON.stringify({type:\\\"answer\\\",value:\\\"approve\\\"}))\">Approve</button> <button onclick=\"cueyou.postMessage(JSON.stringify({type:\\\"answer\\\",value:\\\"reject\\\"}))\">Reject</button>",
    "timeoutMs": 60000
  }' \
  --max-time 70

Example — alert with a styled status card

bash
curl https://api.cueyou.app/api/alert \
  -H "Authorization: Bearer $CUEYOU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Latency spike",
    "description": "p99 latency 4.2s on /checkout",
    "urgency": "critical",
    "html": "<div style=\"font-family:sans-serif\"><h1 style=\"color:#ff5252;margin:0\">4.2s</h1><p>p99 latency on <b>/checkout</b></p></div>"
  }'

Errors added by html

StatusBodyCause
400{"error":"invalid_input","details":...}html empty or longer than 50,000 characters, or fullscreen: true without html
403{"error":"html_not_allowed"}The API key does not have Allow HTML content enabled

Tips

  • Design for a dark, narrow phone screen; use relative units and avoid fixed widths.
  • Don't rely on colours alone — recipients may see the content in bright sunlight or with a colour-vision difference.
  • Keep the plain description meaningful; it is what shows if the HTML can't.

Security and trust

What recipients see and control. HTML content always appears under a header naming the API key that sent it (except in fullscreen mode, where the page is the whole screen and nothing of cueyou's is shown — so only enable HTML for keys you fully trust), runs without network access, and can't navigate away or open other apps. Account owners decide, per API key, whether HTML is allowed at all. Treat your API keys as secrets: a leaked key that is allowed to send HTML could show misleading content on your users' phones, so revoke it immediately if it's exposed.

GET

/api/devices

Returns the devices on the account that owns the API key. Use this to discover the deviceId you'll pass to /api/alert and /api/prompt.

Example

bash
curl https://api.cueyou.app/api/devices \
  -H "Authorization: Bearer $CUEYOU_API_KEY"

Response — 200

json
{
  "devices": [
    {
      "id": "65f3a1b2c4d5e6f7a8b9c0d1",
      "name": "Pixel 9",
      "platform": "ANDROID",
      "lastSeenAt": "2026-05-07T09:14:02.000Z"
    }
  ]
}
POST

/api/alert

Fire-and-forget. The server delivers an FCM push and the phone immediately takes over its screen. The HTTP call returns as soon as FCM accepts the push — it does not wait for the user to dismiss.

Request body

FieldTypeRequiredNotes
deviceIdstringnoDevice id from GET /api/devices. Omit to fan out to every Android device on the account.
titlestringyes1–100 chars; shown large on the takeover screen
descriptionstringyes0–2000 chars, rendered as markdown. See Shared concepts.
urgencystringno"info" | "normal" | "critical". Default "normal". See Shared concepts.
sourcestringno≤ 80 chars. Shown as a chip at the top of the takeover.
htmlstringno1–50,000 chars of HTML rendered on the device. Requires Allow HTML content on the API key. See Rich HTML content.
fullscreenbooleannotrue shows your html as the whole screen with no cueyou UI around it. Requires html. See Full-screen pages.

Example

bash
curl https://api.cueyou.app/api/alert \
  -H "Authorization: Bearer $CUEYOU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Build failed",
    "description": "main is red — last green was 12 minutes ago"
  }'

Example — critical, with markdown and source

bash
curl https://api.cueyou.app/api/alert \
  -H "Authorization: Bearer $CUEYOU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Production 5xx spike",
    "description": "**p99 latency** doubled in the last 5m.\n\n- region: `us-east-1`\n- error rate: 8.4%\n\n[Open dashboard](https://grafana.example.com)",
    "urgency": "critical",
    "source": "datadog · synthetics"
  }'

Response — 200

json
{
  "alertId": "f8b71c98-...",
  "sentAt": "2026-05-07T09:14:02.123Z",
  "delivered": true,
  "deviceCount": 2,
  "blockedCount": 0
}

If the user has the device disabled in their account settings (or quiet hours, etc.), the call still returns 200 but with:

json
{ "alertId": "...", "sentAt": "...", "delivered": false, "reason": "blocked_by_settings" }

Fan-out. When you omit deviceId, the alert is delivered to every Android device registered on the account. deviceCount is how many devices were targeted and blockedCount is how many of those were suppressed by account settings (disabled, quiet hours, muted scopes). An account with zero registered devices still returns 200 with "deviceCount": 0, "blockedCount": 0 — it is not an error.

Errors

StatusBodyCause
400{"error":"invalid_json"}Body wasn't valid JSON
400{"error":"invalid_input","details":...}Schema validation failed
401{"error":"unauthorized"}Missing/invalid/revoked API key
404{"error":"Device not found for this account"}deviceId doesn't belong to this account
403{"error":"html_not_allowed"}html was sent by a key that isn't allowed to send HTML
502{"error":"FCM rejected the push: ..."}Upstream FCM failure
500{"error":"internal_error"}Anything else
POST

/api/prompt

Ask the device a question and block until the user answers, dismisses, or the timeout fires. The server holds the HTTP connection open until a terminal state is reached. Pick this when you actually need a decision back, not just attention.

There are two modes, selected by the shape of the request:

  • Options mode — pass options: (string | Option)[]. The device shows horizontally swipeable cards, one per option; answer will be the picked option's value. Plain strings are still accepted as a shorthand and mixed arrays are fine.
  • Text mode — omit options. The device shows a free-text field; answer is whatever the user typed (up to 1000 chars).

Request body

FieldTypeRequiredNotes
deviceIdstringnoDevice id from GET /api/devices. Omit to fan out to every Android device on the account.
titlestringyes1–100 chars
descriptionstringyes0–2000 chars, rendered as markdown. See Shared concepts.
urgencystringno"info" | "normal" | "critical". Default "normal".
sourcestringno≤ 80 chars. Shown as a chip at the top of the takeover.
options(string | Option)[]no1–10 entries. Plain strings or Option objects (see below). Omit for text mode.
timeoutMsnumberno5,000–900,000 (5s–15m). Default 300000 (5m).
htmlstringno1–50,000 chars of HTML rendered on the device. Requires Allow HTML content on the API key. See Rich HTML content.
fullscreenbooleannotrue shows your html as the whole screen with no cueyou UI around it. Requires html. See Full-screen pages.

The Option object

FieldTypeRequiredNotes
labelstringyes1–200 chars. The text shown on the card.
valuestringno1–100 chars. Returned as answer when picked. Defaults to label.
primarybooleannoMarks this option as the recommended path. Only the first primary: true is treated as primary.

When you pass a plain string, it's equivalent to { label: s, value: s }. The single-option label cap is 200 chars.

Example — yes/no (plain strings, legacy shape)

bash
curl https://api.cueyou.app/api/prompt \
  -H "Authorization: Bearer $CUEYOU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Approve deploy?",
    "description": "v2.18.0 to production",
    "options": ["Yes","No"],
    "timeoutMs": 60000
  }' \
  --max-time 70

With plain strings, answer comes back as "Yes" or "No" verbatim.

Example — structured options with primary, urgency, and source

bash
curl https://api.cueyou.app/api/prompt \
  -H "Authorization: Bearer $CUEYOU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Run destructive migration?",
    "description": "Drops `legacy_events` (≈ 4.2M rows). **Irreversible** without a restore.",
    "urgency": "critical",
    "source": "claude-code · pre-commit",
    "options": [
      { "label": "Yes, run the migration now", "value": "yes", "primary": true },
      { "label": "No, abort", "value": "no" },
      { "label": "Show me the SQL first", "value": "dry-run" }
    ],
    "timeoutMs": 120000
  }' \
  --max-time 130

Long, descriptive labels keep the user oriented; short values keep your code clean.

Example — free text

bash
curl https://api.cueyou.app/api/prompt \
  -H "Authorization: Bearer $CUEYOU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Override reason?",
    "description": "Type a short note for the audit log"
  }' \
  --max-time 310

Response — 200

json
{
  "promptId": "8d2c61aa-...",
  "status": "answered",
  "answer": "yes",
  "sentAt": "2026-05-07T09:14:02.123Z",
  "resolvedAt": "2026-05-07T09:14:11.456Z"
}

status values:

ValueMeaninganswer
answeredUser picked an option / typed a replyPicked option's value (or typed string in text mode)
dismissedUser actively dismissed the promptnull
timeouttimeoutMs elapsed before any user actionnull
blockedPrompt suppressed by account settings; never shown to the usernull

Errors

Same shape as /api/alert. The most likely ones are 401, 404, and 502.

StatusBodyCause
403{"error":"html_not_allowed"}html was sent by a key that isn't allowed to send HTML
Connection budget. Because the server holds the connection open until resolution, set your client's read timeout to at least timeoutMs plus a few seconds of slack — otherwise your client will give up before the server resolves the prompt.

Fan-out & first-responder-wins. When you omit deviceId, the prompt is sent to every Android device on the account and the first one to answer (or dismiss) wins — its response is what you get back. The other phones keep showing the prompt until the user clears it locally, but their input no longer affects the API call. If the account has no registered devices, the request returns 404 immediately rather than long-polling into a void.

Choosing alert vs. prompt

  • Use /api/alert when you only need to grab attention — a build failed, an error spiked, a long task finished. Returns immediately.
  • Use /api/prompt when you need a decision — gating a destructive operation, asking for an override, collecting a free-form note. Blocks until the user responds.

A common pattern is a Claude Code (or CI) hook that calls /api/alert on notifications and /api/prompt on PreToolUse for risky tools so the user can approve from their phone.

Operational notes

  • The user controls delivery. In the app each user chooses one delivery mode — Disabled (drop everything), Muted (full-screen takeover, silent), Vibration (vibrates, no sound) or Sounds (rings) — and can override it per device or per API key. They can also set quiet hours (a daily window during which alerts are dropped) and pause everything for 1, 2, 8 or 24 hours. When delivery is switched off — Disabled, inside quiet hours, or during a pause — alerts come back with delivered: false, reason: "blocked_by_settings" and prompts come back with status: "blocked". Muted and Vibration still deliver. In every case the call returns 200 — handle these as non-errors.
  • Idempotency. There is no idempotency key. Retrying a 5xx may produce duplicate phone alarms. Prefer surfacing the failure to the caller over blind retries.
  • Rate limits. Not enforced server-side today. Treat the phone as the rate limiter — repeated alarms are noisy and will train the user to ignore them.
  • API key hygiene. Each integration should have its own key labelled in the dashboard. Revoke on leak; the next request with that key will get 401.
  • Urgency is visual-only today. urgency drives the takeover palette but does not currently change the sound. Per-tier ringing is a known follow-up.