Concepts

Messages

Everything in the inbox is a message. POST /messages takes a title (≤180 characters), a Markdown body (≤50,000) and a kind, all end-to-end encrypted by the SDKs and CLI before they leave your machine:

Kind Use
message A notification or update. Delivered, nothing to answer.
report A message meant for careful reading, such as a summary or diff.
files Mostly attachments.
form A request: fields and/or action buttons; waits for an answer.
secret An encrypted secret offered to you (Secrets).

Requests move from pending to answered, expired or cancelled. Messages without a question are delivered. Each send needs an Idempotency-Key: retrying with the same key and body returns the original message; a different body is a 409. The SDKs and CLI generate keys for you; pass your own to survive restarts.

Forms

A form has fields (up to 30):

Type Answer Options
text, textarea string minLength, maxLength, placeholder
number number minimum, maximum, integer
boolean true/false
select, multiselect string / string[] options, other
date YYYY-MM-DD
secret encrypted (Secrets) label
file attachment IDs minItems, maxItems, secret

Every field has id, label, optional required, help, default and nullable.

Choices are options: [{value, label, description?, preview?}]: description shows under the label and preview is Markdown (a mockup or code) shown while the option is selected. other: true adds an Other choice answered with free text: the answer is that text, or one extra item in a multiselect (up to 100 characters). A short header names the field in the card's pager.

Questions

questions asks several multiple-choice questions at once, in the shape of Claude Code's AskUserQuestion input. Each becomes a required choice field q1…qN with Other, whose values are the option labels; cards show one question at a time, the thread shows them as one form. The SDKs' questionAnswers (MCP ask returns it as answers) maps the answer back by question text, multiple choices joined by , . This lets an agent ask through Herald in place of its own question tool; see Agents.

{
  "title": "Plan the sign-in refactor",
  "questions": [
    {
      "question": "Which sign-in method should the app use?",
      "header": "Auth method",
      "options": [
        { "label": "Passkeys", "description": "Phishing-resistant; needs a fallback." },
        { "label": "Magic links", "description": "Simple, but depends on email." }
      ]
    },
    {
      "question": "Which platforms ship first?",
      "header": "Platforms",
      "multiSelect": true,
      "options": [{ "label": "Web" }, { "label": "macOS" }, { "label": "Android" }]
    }
  ]
}

Instead of fields you can send a flat JSON Schema object as schema: scalar properties, enums, string-enum arrays, bounds, defaults and dates, plus x-herald-type: textarea|secret|file|secret-file. Nested objects, pattern and unknown keywords are rejected, so a form never silently drops a constraint.

{
  "title": "Configure the release",
  "schema": {
    "type": "object",
    "required": ["channel"],
    "properties": {
      "channel": { "type": "string", "enum": ["beta", "stable"] },
      "notes": { "type": "string", "x-herald-type": "textarea" }
    }
  }
}

Actions

actions (1–8) are buttons that answer a form in one tap: {id, label, values}, where values is a complete, valid answer to the form (often {}). The answer carries actionId. Actions cannot be combined with secret or file fields. An approve/reject pair can also be answered from the lock screen on paid plans.

Blocks

blocks (up to 10) show structured content above the form:

  • code, diff, json: {type, text, language?, title?}, up to 50,000 characters. language is text (default), sql, javascript, typescript, python or json.
  • table: {columns, rows}, up to 12 columns and 100 rows.
  • image: {attachmentId, alt} for a PNG, JPEG, GIF or WebP attached to the same message.

requiresReview: true on a block or the message opens the full view before answering.

Files

Files are encrypted before upload. Reserve, send the bytes, and attach the result (up to 10 per message):

  1. POST /files with {name: "encrypted", type: "application/octet-stream", size} (the encrypted size, ≤100 MB) returns {id, ...}, valid for 15 minutes.
  2. PUT /files/:id with exactly size encrypted bytes, once.
  3. Send a message whose encrypted content lists the attachment with its real name, type, size and key.

The SDKs' upload and the CLI's --file do all three. Downloading needs the message that carries the file, since it holds the key.

Secrets

Herald moves credentials end-to-end encrypted, so they never appear in message history, notifications or logs.

  • Request a secret: a form with a secret field. Your device encrypts the answer to the asking connection only; it collects it once within 15 minutes.
  • Request a file privately: a file field with secret: true. Only the asking connection can download the upload, and Herald deletes it 15 minutes after the answer.
  • Offer a secret: a kind: "secret" message. When you open it, your device posts a one-time public key signed by your account; the sender checks the signature, encrypts to the key and Herald hands the ciphertext over once.

The CLI and MCP server move secrets only through files, never arguments or output:

herald ask --title 'API token' --field 'token:secret:API token' --out ~/.config/tool/token --deadline 10m
herald ask --title 'Staging kubeconfig' --field 'kubeconfig:secret-file' --out ~/.kube/staging
herald send --secret-file ./generated-password --title 'Staging password'

Output files are created with mode 0600 and must not already exist. Never put credentials in ordinary fields, titles, bodies or logs.

End-to-end encryption

Every account is end-to-end encrypted. The first device you sign in on creates the keys; save a recovery key or passkey when the app asks, so you can unlock new devices. A new device unlocks with a passkey, the recovery key or approval from an unlocked device, where you type the code the new device shows. Every device shows the same safety code.

  • Connections. Each connection holds its own key, certified by your account; its token reads hdr_….<seed>.<fingerprint> and only the hdr_… part is sent to Herald. Messages, answers, task inputs, logs, results, secret offers and files are encrypted on the device that writes them and readable only on your devices and the connections they are addressed to. Herald refuses anything else, including tokens without a key.
  • What Herald sees. Ciphertext, public keys and metadata: sender, times, status, deadlines, whether a record asks for an answer, thread links, destination and type definitions, task failure reasons and sizes. Records carry sealed, titles read "Encrypted message", server search matches only the sender and notifications show the sender with "New message" or "Needs your reply"; your devices decrypt notification previews themselves, and Approve/Reject answers from a notification are encrypted on the device.
  • Keys. GET /keys lists the account statement and connection certificates; /keys/* routes (human sessions) manage enrolment, backups and device approval.
  • Reset. Reset in Settings creates new keys. It deletes all activity and files and revokes every connection; other devices must unlock again.

Threads

Set replyTo to a message ID to continue its conversation. Replies get threadId and threadSequence; GET /messages/:id/thread lists the thread oldest first. A task you dispatch as a reply lets its runner read the thread up to that task.

Deadlines

A request expires at expiresAt (ISO time) or after expiresIn seconds (≤24 hours). The SDKs' timeout and the CLI's --deadline set it. Separately, a client's wait timeout only stops the client: the request stays answerable, and wait resumes with its ID.

Live updates

wss://app.herald.run/ws (scope events:read) sends {"type":"sync","revision":N} whenever your inbox changes. Then read GET /messages?after=<cursor> and advance your cursor to the highest revision seen. Updates coalesce and deletions appear only with tombstones=1, so process idempotently. The SDKs' wait and watch do this for you.