# Your first cue in **five minutes**.

Signal is a cued-message API. You open a stream, then call `/cue` with a patient's last response and get back the next-right message — chosen by Uphealth's sequencing engine from a corpus of federally-sourced content. This page goes from a sandbox key to a live cue in four calls.

> **Before you begin**
>
> You need a Discovery sandbox key `up_sandbox_…` — it is free, self-serve, and shows once on your dashboard at signup, no sales call. Everything below runs against the sandbox: a curated five-topic subset of the federally-sourced corpus, no PHI.
>
> [Get a sandbox key →](https://uphealth.us/signup)

### Set your key

Export the key so the samples can read it from the environment. Never hard-code a key in client source.

```bash
$ export UPHEALTH_KEY="up_sandbox_44a053ec…"
```

```javascript
// Node 18+ — read the key from the environment, never hard-code it
const KEY  = process.env.UPHEALTH_KEY;
const BASE = 'https://api.uphealth.us/v1/signal';
```

```python
# Python 3 with requests
import os, requests

KEY  = os.environ["UPHEALTH_KEY"]
BASE = "https://api.uphealth.us/v1/signal"
```

```ruby
# Ruby 3 — stdlib only, no gems
require 'net/http'; require 'json'

KEY  = ENV['UPHEALTH_KEY']
BASE = 'https://api.uphealth.us/v1/signal'
```

### Create a stream

POST a `template_id` to open a stream — the response carries the **first cue synchronously, no second call for the opening message. Any live template id is accepted — a sandbox stream always plays the curated corpus, so start with** `general_wellness_daily`.

```bash
$ curl -X POST https://api.uphealth.us/v1/signal/streams \
  -H "Authorization: Bearer $UPHEALTH_KEY" \
  -H "Content-Type: application/json" \
  -d ' { "template_id": "general_wellness_daily" } '
```

```javascript
const res = await fetch(`${BASE}/streams`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ template_id: 'general_wellness_daily' })
});
const stream = await res.json();
const cue = stream.first_cue;  // the opening message — no second call
```

```python
res = requests.post(
  f"{BASE}/streams",
  headers={"Authorization": f"Bearer {KEY}"},
  json={"template_id": "general_wellness_daily"}
)
stream = res.json()
cue = stream["first_cue"]  # the opening message
```

```ruby
uri = URI("#{BASE}/streams")
req = Net::HTTP::Post.new(uri, { "Authorization" => "Bearer #{KEY}", "Content-Type" => "application/json" })
req.body = { template_id: "general_wellness_daily" }.to_json
stream = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
```

**201 Created**

```json
{
  "stream_id": 1,
  "state": "awaiting_feedback",
  "first_cue": {
    "cue_event_id": 9,
    "message_id": 1,
    "message_class": "sandbox_message",
    "kind": "fact",
    "body": "Heart disease is the leading cause of death in the United States for both men and women.",
    "source_url": "https://www.cdc.gov/heart-disease/data-research/facts-stats/index.html",
    "source_quote": "Heart disease is the leading cause of death for men, women, and people of most racial and ethnic groups.",
    "receptivity": 0.5,
    "safety": {
      "verdict": "included", "reason": "unrestricted",
      "matched_tags": [], "gating_tags": [], "excluded_tags": []
    },
    "arc": null,
    "day_of_stream": 1,
    "cued_at": "2026-05-30T01:24:34Z"
  },
  "next_cue_eligible_at": null,
  "_meta": { "api_version": "1.0", "no_medical_advice": true, "delivery_mode": "display", … }
}
```

### Read the cue response

Every cue carries the message to render plus the fields your routing logic needs. These are the ones you use on every call:

| Field | What it is |
| --- | --- |
| `first_cue.kind` | fact · list · tip — the message type, for your render label (Know it / Did it / Check it). |
| `first_cue.body` | The message text to render in your app or send through your channel. |
| `first_cue.source_url` | The federal source (CDC / NIH / HHS / USPSTF / Dietary Guidelines). Show it — provenance is the trust. |
| `first_cue.source_quote` | The exact sentence from that source the message is drawn from. |
| `first_cue.receptivity` | An advisory float 0.00–1.00 — the engine's estimate of how receptive the patient is right now (0.50 on a brand-new stream). Read it or ignore it. |
| `first_cue.safety` | The audience-safety verdict for this message — always included on a returned cue, with the tags that admitted it. PHI-free. |
| `first_cue.arc` | Curated-arc position on spine-backed paid streams — null on every sandbox cue. first_cue.day_of_stream (the advisory wall-clock day) rides alongside it. |
| `state` | awaiting_feedback — the next /cue needs the patient's response to this message first. |
| `_meta` | The legal disclosure block (no_medical_advice, delivery_mode, license) returned on every response. |

> Receptivity and audience safety are applied **server-side** to choose this message — and they ride along on the cue: an advisory `receptivity` float and a PHI-free `safety` verdict. Both are advisory; you read them or ignore them. See [Receptivity & safety](https://uphealth.us/docs/rs-score) for how to read them.

### Submit feedback, get the next cue

POST the patient's response to the prior message. `response_action` is one of the engagement archetypes — `did_it` · `check_it` · `know_it` · `no_response` and more, listed on [Cues and feedback](https://uphealth.us/docs/concepts-cues#actions). The `idempotency_key` lets you retry safely.

```bash
$ curl -X POST https://api.uphealth.us/v1/signal/streams/1/cue \
  -H "Authorization: Bearer $UPHEALTH_KEY" \
  -H "Content-Type: application/json" \
  -d ' { "feedback": { "response_action": "did_it",
              "idempotency_key": "550e8400-e29b-41d4-a716-446655440000" } } '
```

```javascript
const res = await fetch(`${BASE}/streams/1/cue`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    feedback: { response_action: 'did_it', idempotency_key: crypto.randomUUID() }
  })
});
const next = (await res.json()).next_cue;
```

```python
import uuid
res = requests.post(
  f"{BASE}/streams/1/cue",
  headers={"Authorization": f"Bearer {KEY}"},
  json={"feedback": {
    "response_action": "did_it",
    "idempotency_key": str(uuid.uuid4())
  }}
)
next_cue = res.json()["next_cue"]
```

```ruby
require 'securerandom'
req = Net::HTTP::Post.new("#{BASE}/streams/1/cue", headers)
req.body = { feedback: {
  response_action: "did_it", idempotency_key: SecureRandom.uuid
} }.to_json
next_cue = JSON.parse(http.request(req).body)["next_cue"]
```

**200 OK**

```json
{
  "stream_id": 1,
  "state": "awaiting_feedback",
  "idempotent": false,
  "normalized_action": null,
  "next_cue": {
    "cue_event_id": 11,
    "message_id": 13,
    "message_class": "sandbox_message",
    "kind": "fact",
    "body": "The 2025-2030 federal Dietary Guidelines call for prioritizing whole, healthy, and nutritious foods.",
    "source_url": "https://dietaryguidelines.gov",
    "source_quote": "Prioritize whole, healthy, and nutritious foods with the new Dietary Guidelines for Americans!",
    "receptivity": 0.67,
    "safety": {
      "verdict": "included", "reason": "unrestricted",
      "matched_tags": [], "gating_tags": [], "excluded_tags": []
    },
    "arc": null,
    "day_of_stream": 1,
    "cued_at": "2026-05-30T01:24:35Z"
  },
  "_meta": { "api_version": "1.0", "no_medical_advice": true, "delivery_mode": "display", … }
}
```

That is the loop — one `/cue` call per active patient per day. The engine sequences across topics from the feedback signal, so the next message adapts to what landed.

### Know the one rule

Call `/cue` without a `feedback` block and you get a `409 feedback_required` — this is structural, not a policy. The engine cannot pick the next-right message without the patient's signal on the last one.

```bash
# POST /cue with an empty body — no feedback block
$ curl -X POST https://api.uphealth.us/v1/signal/streams/1/cue \
  -H "Authorization: Bearer $UPHEALTH_KEY" -d '{}'
```

```javascript
// no feedback block → 409 feedback_required
await fetch(`${BASE}/streams/1/cue`, { method: 'POST', headers: { 'Authorization': `Bearer ${KEY}` }, body: '{}' });
```

```python
# no feedback → 409
requests.post(f"{BASE}/streams/1/cue", headers={"Authorization": f"Bearer {KEY}"}, json={})
```

```ruby
# no feedback → 409
req = Net::HTTP::Post.new(uri); req.body = '{}'; http.request(req)
```

**409 Conflict**

```json
{
  "error": "feedback_required",
  "stream_state": "awaiting_feedback",
  "last_cue_event_id": 11
}
```

## What is next

- [Core concepts](https://uphealth.us/docs/concepts-streams) — Streams, enrollment, the cue response, and Display vs Deliver modes.
- [API reference](https://uphealth.us/docs/ref-create) — Every endpoint — create, get, cue, and sandbox topics — with request, response, and errors.
- [Sample data](https://uphealth.us/docs/sandbox) — The five sandbox topics, the federal content behind them, and how a stream ends.
- [Sandbox → production](https://uphealth.us/docs/live-promote) — Promote to a live key, turn on Deliver mode, and the BAA gate.

---

_Uphealth Signal developer docs · [View on uphealth.us](https://uphealth.us/docs/quickstart) · OpenAPI spec: https://uphealth.us/openapi/signal-v1.yaml · Verified against API v1 on 2026-07-22._
