# Fields and limits

Every send field is optional except the message. Unknown fields are `400 invalid_field`,
except `version`, `type` and `attachments` in a JSON or form body, which are ignored so that
[Apprise](apprise.md)'s default payload works.
Lengths are **UTF-8 bytes**, not characters. Text fields reject control characters other
than tab and newline.

## Send fields

| Field | Type | Limit | Default | Meaning |
|---|---|---|---|---|
| `message` (aliases `body`, `text`) | string | 1–4,000 bytes, **required** | — | The notification text |
| `title` | string | 250 bytes | none (the app shows the scene's name) | Bold first line |
| `subtitle` | string | 250 bytes | — | Second line |
| `scene` | string | a catalog id, `^[a-z][a-z0-9-]{0,31}$` | `notice` | The animation (see [Scenes](#scenes)) |
| `scene_version` | integer | a version the catalog lists | the scene's current version | Pins an animation version |
| `scene_params` | object | 20 keys, depth 2, 2 KB serialized; validated against the scene's schema | — | The scene's parameters |
| `bell_color` | string | `gold`, `red`, `blue`, `green`, or `#RRGGBB` | `gold` | The fox's bell |
| `url` | string | 2,048 bytes, HTTPS with a DNS host name | — | A link the full-screen scene offers as its own button, showing the domain first; never opened automatically or by the tap itself |
| `url_title` | string | 1–64 bytes; only together with `url` | — | The link's button title |
| `image` | string | 2,048 bytes, HTTPS with a DNS host name | — | An image attachment |
| `level` | string or integer | see [Levels](#levels-and-pushover-priorities) | `active` | How it interrupts |
| `group` | string | 1–64 bytes | — | Groups notifications into one thread |
| `tag` | string | 1–64 bytes, `[A-Za-z0-9._-]` | — | A newer send with the same tag replaces the older notification on the devices it reaches (keep the `target` the same) |
| `sound` | string | `default` or `none` | `default` | Whether it plays a sound |
| `ttl` | integer | 1 to 2,592,000 seconds (30 days) | 86,400 (24 hours) | The latest time delivery may happen |
| `target` | string | 1–64 bytes: `auto`, `iphone`, `watch`, `vision`, or a device name or id (exact, case-sensitive) | the token's default, or `auto` | Which devices get it; `ios`, `watchos` and `visionos` are not keywords |

Other limits:

| What | Limit |
|---|---|
| The whole request | 16 KB: path, query string, headers and body, as sent |
| `Idempotency-Key` header | 1–255 printable ASCII characters, scoped to the token; a retry with the same key and the byte-identical request returns the original response for 24 hours |
| URLs (`url`, `image`) | HTTPS only; a DNS host name (punycode for international names), an optional port 1–65535, no user name or password, no IP addresses or all-numeric hosts |

`expires_at` (from `ttl`) is only an upper deadline for delivery: each device gets at most 5
attempts, with backoff of about 2, 4, 8 and 16 seconds, however long `ttl` is, and nothing is
sent after it. A message stays in the inbox history for 30 days either way.

## Levels and Pushover priorities

`level` takes a NoticeFox level or a Pushover priority, as a JSON number or a string:

| Input | `level` | iOS interruption level | APNs priority |
|---|---|---|---|
| `passive`, `-2`, `-1` | passive | passive | 5 |
| `active`, `0` (default) | active | active | 10 |
| `time-sensitive`, `1`, `2` | time-sensitive | time-sensitive | 10 |

V1 has no critical alerts, so Pushover's emergency priority `2` is time-sensitive and doesn't
repeat.

## Aliases, precedence and conflicts

- `body` and `text` are aliases of `message`.
- The JSON endpoint (`POST /v1/notifications`) takes only a JSON body.
- The simple endpoint (`/v1/send/{send_key}`) also takes query parameters, a form body, a raw
  text body (the whole body becomes `message`), and the `X-Title` and `X-Level` headers.
  Precedence, highest first: path send key, JSON body, form body, query, headers.
- The same field given twice with **different** values is `400 conflicting_field`: in two
  sources, through two aliases, or as a repeated JSON member or query parameter. The same
  value twice is fine.
- Each source is optional. The sources merge first, and then a message is required, so
  `POST …/v1/send/<key>?message=Done` with the JSON body `{"title": "Build"}` is valid.
- A `GET` of the simple endpoint without a message is `400 missing_field`, so a link preview
  or preloader that fetches a bare token URL creates no notification. A URL that includes a
  message notifies every time it is fetched, previews included.
- A form-typed body with no `=` in it is raw text, exactly as sent: `curl -d "Build
  finished"` works. One with an `=` is parsed as form fields, so send a message containing
  `=` with `--data-urlencode "message=…"` or `Content-Type: text/plain`.
- On the simple endpoint, scene parameters are `param.<name>=<value>`, such as
  `?param.doorState=open`, converted to the scene schema's types. `param[name]` is not
  accepted.
- Retry with the byte-identical request. Some spellings of the same request are treated as
  equal, but not all: a field added on retry, even set to what looks like its default, can
  turn the retry into `409 idempotency_conflict`.

## Scenes

`GET /v1/scenes` returns the current catalog with each version's parameter schema. The V1
scenes:

| Scene | Parameters |
|---|---|
| `notice` (default) | none |
| `rocket` | `color`: `red`, `blue`, `green` or `white` (default `white`); `countdown`: integer 0–10 (default 3) |
| `fireworks` | `bursts`: integer 1–10 (default 3) |
| `garage` | `doorState`: `open`, `closed`, `opening` or `closing` (default `open`) |
| `campfire` | none |

Parameters are optional; the app plays an omitted one with its default. An unknown scene or version is `400 invalid_scene`; a parameter the
scene doesn't take, or a value outside its schema, is `400 invalid_scene_param` with `field`
set to `scene_params.<name>`.
