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.

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 out of scope. Long descriptions scroll inside the takeover with a fade gradient signalling that there's more.

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.

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
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).

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.

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

  • Account settings can suppress delivery. Disabled devices, muted scopes, and quiet hours all flow through the same getEffective check. Alerts come back with delivered: false, reason: "blocked_by_settings". Prompts come back with status: "blocked". In both cases 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.