# Queue API Documentation
Base URL: `https://beta-dev_model.dbo.io/app/queue/api`
Auth: pass `_em` + `_pw` on every request, or use a session cookie from `/api/authenticate`.
All responses are `application/json` arrays.
Empty array `[]` = not found, not authorized, or not authenticated — intentionally indistinguishable.
**Access model:** the *FromContact* = pusher (person who adds items to queues); the *ToContact* = queue owner (person whose queue receives items). An access record grants the FromContact the right to push into the ToContact's queue.
**Identifier policy:** every identifier on the wire is an opaque 128-bit UID (`ItemUID`, `LinkUID`, `ShareUID`, `ListUID`, `AccessUID`, and `ContactUID` for people). Integer primary keys are never accepted as params nor returned in responses.
---
## Account
### POST /account/create
Create a new account. Creates a `user` record (core DB) and a `contact` record (queue DB) in one operation.
After creation, call `/api/authenticate` with `_em` + `_pw` to establish a session.
**Params**
| Param | Required | Notes |
|---|---|---|
| `Email` | yes | |
| `FirstName` | yes | |
| `LastName` | no | |
| `Password` | no | |
**Returns — success** (session cookie also set)
```json
[{ "Status": "OK" }]
```
**Returns — duplicate email**
```json
[{ "error": "Email already in use" }]
```
---
### GET /account/view
Returns the logged-in user's profile. Also used as an auth gate — returns `[]` if not authenticated.
**Returns**
```json
[{
"FirstName": "Alex",
"LastName": "Chen",
"Email": "alex.queue@dbo.io",
"AvatarPath": null
}]
```
---
### POST /account/passkey
Request a one-time sign-in code by email. Looks up the user matching `Email`, then mails them their platform-managed `user.PassKey` value via SES from `noreply@addtoqueue.com`. The emailed code is consumed on the next successful `/api/authenticate` with `_em` + `_pk`.
**Params**
| Param | Required | Notes |
|---|---|---|
| `Email` | yes | account email |
**Returns** — opaque by design (no enumeration leak)
```json
[{ "Status": "OK" }]
```
The same `{"Status":"OK"}` response is returned whether or not an account exists for the supplied email. If the email matches an active user, an email is sent; otherwise the request is a silent no-op.
**Sign-in with the code:**
```
POST /api/authenticate
_em=alex.queue@dbo.io
_pk=<6-or-more-char code from email>
```
The platform regenerates `PassKey` on every successful authentication, so each emailed code is single-use.
---
## Auth
```
POST /api/authenticate
_em=alex.queue@dbo.io
_pw=Alex
```
Sets a session cookie. All endpoints also accept inline `_em` + `_pw`.
---
### Logout
```
GET /api/authenticate?_logout=true
```
Invalidates the current session cookie. Idempotent — safe to call when not signed in (no-op). Returns a 2xx; response body is not used by clients. Form-POST and GET both work; the iOS and web clients use GET via `fetch(..., { credentials: 'same-origin' })`.
---
## Access
### GET /contact/list
Your contacts — both granted connections and pending invites you've created — in one query. The endpoint walks the `access` table starting from rows where you're the `FromContact`, joining left to the recipient so pending invites (which have no recipient yet) come through as rows with an empty `ContactUID` (`""`) and `FirstName` falling back to the invite nickname.
Sort order is **pending-first** so the iOS home screen can render that section by traversing the array in order. Set `Status=pending` or `Status=granted` to filter to one state.
| Field | Notes |
|---|---|
| `ContactUID` | The connected contact's opaque UID, or `""` for a pending invite |
| `AccessUID` | The access record's UID — use to revoke a granted connection, and as the share/relationship handle |
| `Status` | `granted` or `pending` |
| `Handle` | Contact's public handle (null for pending) |
| `FirstName` | Contact's first name when granted; the invite nickname when pending |
| `InviteName` | The nickname you assigned when creating the invite (always present) |
| `LinkShared` | Optional `1`/`0` flag indicating whether the caller already shared a specific link to this contact — only returned when `LinkUID` param is provided |
**Params** (all optional)
| Param | Notes |
|---|---|
| `Status` | Filter to `granted` or `pending`. Omit for both. |
| `LinkUID` | When set, adds the `LinkShared` field to each row — useful for share-target UIs that want to mark recipients who already received the link |
**Returns**
```json
[{
"ContactUID": "b3c1d2e4f5a6...",
"AccessUID": "f76e0dbef1152e27d9874417e8d842d2",
"Status": "granted",
"Handle": "mayap",
"FirstName": "Maya",
"InviteName": "Maya P",
"LinkShared": null
}]
```
A pending row looks like:
```json
[{
"ContactUID": "",
"AccessUID": "603487f5264a11f1a7f00a7cdc11235d",
"Status": "pending",
"Handle": null,
"FirstName": "Dan",
"InviteName": "Dan",
"LinkShared": null
}]
```
---
### POST /access/add
Create an invite link. Caller becomes `FromContact` (the pusher — they will be able to push to the queue of whoever redeems).
**Params**
| Param | Required | Notes |
|---|---|---|
| `AccessName` | yes | Nickname for the invitee shown in pending list |
**Returns**
```json
[{ "AccessUID": "f76e0dbef1152e27d9874417e8d842d2" }]
```
Use `AccessUID` to construct the redeem link: `/access/redeem/{AccessUID}`
Idempotent — same `AccessName` returns the same `AccessUID` if a pending invite already exists.
---
### GET /access/redeem/[accessUID]
Recipient redeems an invite link. Stamps `ToContactID` (redeemer = queue owner), sets `Status = granted`.
Magic link — safe to send via SMS or email.
**Returns**
```json
[{ "AccessUID": "603487f5...", "Status": "granted", "FromContactUID": "9a1c...", "ToContactUID": "b3c1...", "FromContactName": "Maya", "IsNew": 1 }]
```
Idempotent — hitting the same link twice returns the same result.
Returns `[]` if already claimed by someone else.
---
### POST /access/revoke
Mutually sever a connection by `AccessUID`. Marks **both directions** of the access revoked in one call — caller loses ability to push to the contact AND the contact loses ability to push to the caller. Either party may call (the SQL is keyed only on UID, not on which side the caller sits).
**Params**
| Param | Required | Notes |
|---|---|---|
| `AccessUID` | yes | Use `AccessUID` from `/contact/list` |
**Returns**
```json
[{ "AccessUID": "f76e0dbef1152e27d9874417e8d842d2", "Status": "revoked" }]
```
---
## Queue
### GET /queue
Your inbox. Returns all items with link metadata, embedded sender/recipient names, and list membership. Fully self-contained — no secondary lookup required.
**Params** (all optional)
| Param | Notes |
|---|---|
| `Type` | `read` / `listen` / `watch` / `shop` / `experience` |
| `Status` | `unread` / `read` / `archived` |
| `ListUID` | Filter to items in a specific list |
| `FromContactUID` | Filter to items pushed by a specific contact (their `ContactUID`) |
| `Search` | Pipe-separated terms ANDed against Title + URL + Description + Type + sender names + recipient names + list names. Case-insensitive, substring match. Clients should tokenize free-form input: split on spaces, keep double-quoted phrases as one term, then join with `|`. Stray pipes inside a term must be replaced with spaces (the server splits on `|` and would otherwise fragment the term). Do **not** use commas as the term separator — DBO's `_filter@` binding truncates values at the first comma, dropping every term after the first. Example: `ABC "A B C" jojo` → `Search=ABC|A%20B%20C|jojo` returns items containing all three. |
**Returns**
```json
[{
"ItemUID": "ae2a8788...",
"Status": "unread",
"IsFavorite": 0,
"AddedOn": "2026-03-22T19:43:41",
"LinkUID": "11c8a0f3...",
"URL": "https://...",
"Title": "How to Do Great Work",
"Type": "read",
"Thumbnail": "https://...",
"Description": "...",
"FromContactNames": "Maya, Sam",
"ToContactNames": "Maya",
"ListNames": "Design & Culture",
"FromContactArray": "[{\"uid\":\"b3c1d2e4...\",\"name\":\"Maya\",\"shareUID\":\"a0ab9a36...\",\"reactionB64\":\"8J+RjwA=\"},{\"uid\":\"d4e5f6a7...\",\"name\":\"Sam\",\"shareUID\":\"f6a7b8c9...\",\"reactionB64\":\"\"}]",
"ToContactArray": "[{\"uid\":\"b3c1d2e4...\",\"name\":\"Maya\",\"shareUID\":\"c8d9e0f1...\",\"reactionB64\":\"8J+RjwA=\"}]",
"InListArray": "[{\"uid\":\"1a2b3c4d...\",\"name\":\"Design & Culture\"}]"
}]
```
| Field | Notes |
|---|---|
| `FromContactNames` | Flat CSV of sender names — retained for server-side `Search` only. Use `FromContactArray` for display. |
| `ToContactNames` | Flat CSV of recipient names — retained for `Search` only. Use `ToContactArray` for display. |
| `ListNames` | Flat CSV of list names — retained for `Search`. Use `InListArray` for display. |
| `FromContactArray` | JSON string — array of `{uid, name, shareUID, reactionB64}` for each contact who pushed this to you. `uid` is the sender's `ContactUID`; `shareUID` is used with `/share/react`. `reactionB64` is the current reaction (Base64 UTF-8, empty = no reaction). |
| `ToContactArray` | JSON string — array of `{uid, name, shareUID, reactionB64}` for each contact you pushed to. `reactionB64` is Base64-encoded UTF-8 of the reaction emoji (avoids MySQL JSON surrogate pair encoding for non-BMP characters). Decode Base64 → UTF-8 to get the emoji string. |
| `InListArray` | JSON string — array of `{uid, name}` (`uid` = `ListUID`) for each list this item belongs to. Decode as JSON array. |
**`*Array` fields are the canonical source for display.** The only remaining flat CSV fields are name lists used by the server-side `Search` param — no integer-ID CSVs are returned.
**Sort order:** newest first. When `ListID` is set, items are ordered by when they were added to that list (`list_item._CreatedOn DESC`). Otherwise, by when the item entered your queue (`item._CreatedOn DESC`).
---
### GET /queue/shared
Items you have pushed to others. Optional filter by recipient.
**Params**
| Param | Required | Notes |
|---|---|---|
| `ContactUID` | no | Filter to shares sent to a specific contact (their `ContactUID`). Omit for all outbound shares. |
**Returns**
```json
[{
"LinkUID": "6c3a1b2d...",
"URL": "https://...",
"Title": "Abstract: The Art of Design — Netflix",
"Type": "watch",
"Thumbnail": "https://...",
"Description": "...",
"ShareUID": "2b9f4e1a...",
"ToContactUID": "b3c1d2e4...",
"ToContactName": "Maya",
"ReactionB64": "8J+RjwA=",
"SharedOn": "2026-03-22T10:47:34"
}]
```
| Field | Notes |
|---|---|
| `Title` | May be null if link metadata was not scraped. |
| `ToContactName` | Recipient's first name, resolved server-side |
| `ReactionB64` | Base64-encoded UTF-8 of the recipient's reaction emoji. Empty string if no reaction. Decode Base64 → UTF-8 to get the emoji. |
---
## Items
### POST /item/add
Add a link to your queue. Client fetches OG metadata before calling.
Deduplicates on URL — reuses existing link record, never duplicates.
Idempotent — if item already in your queue, returns existing item.
If item was previously soft-deleted, it is restored to `unread` status rather than creating a new record.
If the link already exists and the caller is the original creator (`link.CreatorContactID`), the link metadata is enriched — non-null/non-empty values for `Title`, `Description`, `Thumbnail`, and `Type` overwrite the stored values. Null or empty params are ignored, preserving richer data already stored. This allows the main app to enrich links originally added by the extension (which may have sparse metadata).
**Params**
| Param | Required | Notes |
|---|---|---|
| `URL` | yes | |
| `Type` | yes | `read` / `listen` / `watch` / `shop` / `experience` |
| `Title` | no | From OG metadata |
| `Description` | no | From OG metadata |
| `Thumbnail` | no | From OG metadata |
**Returns**
```json
[{
"ItemUID": "ae2a8788...",
"Status": "unread",
"IsFavorite": 0,
"LinkUID": "11c8a0f3...",
"URL": "https://...",
"Title": "How to Do Great Work",
"Type": "read",
"ToContactArray": "[]"
}]
```
---
### GET /item/view
Fetch a single item by `ItemUID`. The response shape depends on the caller's relationship to the item:
- **Caller owns the item** (it's in their queue): receiver shape — `Status`, `IsFavorite`, `FromContactArray` (who pushed it to them), `ToContactArray` (who they re-shared it to), `InListArray`.
- **Caller has previously shared this link to the item's owner** (sender perspective — e.g. they were notified about a reaction to their share): `ToContactArray` lists every recipient of this link from the caller along with each recipient's reaction. Receiver-only fields (`Status`, lists) come back as defaults or null.
- **Neither**: empty response (403-equivalent).
Bypasses `IsDeleted` for single-record lookup, so the endpoint still resolves for an item the caller removed from their queue after receiving a notification about it.
**Params**
| Param | Required | Notes |
|---|---|---|
| `ItemUID` | yes | |
**Returns** same Item shape as `/queue` rows. Field availability varies by perspective as described above.
```json
[{
"ItemUID": "040d22f5a796...",
"Status": "unread",
"IsFavorite": 0,
"LinkUID": "57a3b1c2...",
"URL": "https://...",
"Title": "...",
"FromContactArray": "[{\"uid\":\"9a1cf0d2...\",\"name\":\"Alex\",\"shareUID\":\"178bc4e9...\",\"reactionB64\":\"\"}]",
"ToContactArray": "[...]"
}]
```
---
### POST /item/edit
Update status or favorite flag. All params optional — only provided fields are updated.
**Params**
| Param | Required | Notes |
|---|---|---|
| `ItemUID` | yes | |
| `Status` | no | `unread` / `read` / `archived` |
| `IsFavorite` | no | `0` / `1` |
**Returns**
```json
[{ "ItemUID": "ae2a8788...", "Status": "read", "IsFavorite": 1 }]
```
---
### POST /item/delete
Soft delete. Sets `IsDeleted = 1`, stamps `DeletedOn`.
Item stays in DB — preserves share and reaction visibility on sender side.
**Params**
| Param | Required | Notes |
|---|---|---|
| `ItemUID` | yes | |
**Returns**
```json
[{ "ItemUID": "ae2a8788...", "IsDeleted": 1, "DeletedOn": "2026-03-22T19:50:51" }]
```
---
## Share
### POST /share/add
Push a link to someone's queue. Requires a granted access record where caller is `FromContact`.
Idempotent — pushing the same link to the same person twice returns the existing share.
**Params**
| Param | Required | Notes |
|---|---|---|
| `LinkUID` | yes | |
| `ToContactUID` | yes | Recipient's ContactUID (queue owner) |
**Returns**
```json
[{
"ShareUID": "a0ab9a36...",
"ToContactUID": "b3c1d2e4...",
"ItemUID": "c133f21a...",
"LinkUID": "2d4f6a8b...",
"FromContactName": "Alex",
"LinkTitle": "Thinking Fast and Slow — NYT Review",
"LinkURL": "https://..."
}]
```
`ItemUID` — the item created (or existing) in the recipient's queue. `ShareUID` / `ToContactUID` / `LinkUID` identify the share, recipient, and link.
`FromContactName` / `LinkTitle` — surfaced for the push notification embed (see Push Notifications below).
**Side effect:** on a successful share, a push notification is dispatched to the recipient's registered devices if their `contact_notifications` settings allow (gated through `notify_recipient` → `notify_share`). Suppression is silent (caller can't tell whether a push fired).
---
### POST /share/react
React to a specific share. Reaction is per share (per sender) — two people can push the same link and receive different reactions.
`ShareUID` comes from the `/queue` response `FromContactArray[].shareUID` field.
**Params**
| Param | Required | Notes |
|---|---|---|
| `ShareUID` | yes | |
| `Reaction` | yes | Any string or emoji |
**Returns**
```json
[{
"ShareUID": "a0ab9a36...",
"FromContactUID": "9a1cf0d2...",
"Reaction": "❤️",
"ReactedOn": "2026-03-22T22:45:45",
"FromContactName": "Alex",
"LinkTitle": "Thinking Fast and Slow — NYT Review",
"ItemUID": "c133f21a..."
}]
```
---
## Push Notifications
Three Queue actions can produce a push notification to another user, gated by the recipient's preferences:
| Action endpoint | Recipient gets notified | Body shape | Custom data payload |
|---|---|---|---|
| `POST /share/add` | The person you shared with | `{Sender} added "{title}" to your Queue` | `{"itemUID": "..."}` |
| `POST /share/react` | The original sharer (when someone reacts to their share) | `{Reactor} reacted {emoji} to "{title}"` | `{"itemUID": "..."}` |
| `POST /access/redeem/[accessUID]` | The inviter (when someone accepts their invite) | `{Joiner} accepted your invite — you're connected` | `{"contactUID": "..."}` |
The actor of an action is never notified about their own action. Recipients with no registered device, or whose preferences suppress that notification type, receive nothing. The caller of the action endpoint receives the normal response either way — there's no signal in the response indicating whether a push was actually sent.
The custom data payload follows the platform convention — for APNs it sits next to `aps`, for FCM it's the `data` dictionary. Use it to deep-link from a tap: both share and reaction notifications carry `itemUID` which can be passed straight to `GET /item/view?ItemUID=...` to fetch the appropriate perspective (receiver's queue item or sender's view of who-they-shared-with). Connection notifications carry `contactUID` for navigation to that contact's profile.
### Device registration
Push delivery to a specific device requires that device to be registered once after sign-in and unregistered on sign-out. Two root-level endpoints handle that (not under `/app/queue/api` — they're site-level).
The `MailServerUID` parameter identifies which push channel a device should be bound to. Queue maintains one channel per platform + environment combination (e.g., APNs sandbox, APNs production, FCM, Web Push), and the UID selects the right one. Use the value that matches your build:
| Build / platform | `MailServerUID` |
|---|---|
| iOS Debug build (APNs sandbox) | `6wxkoab6qk6hqrblchpn8g` |
| iOS Release / TestFlight / App Store (APNs production) | `2g2hkcwi50s4dm2nqwww2a` |
| Android (FCM) | _(not yet provisioned)_ |
| Web Push | _(not yet provisioned)_ |
These UIDs are not secrets — they're routing identifiers, equivalent to a public push key. Knowing one only lets an authenticated Queue user bind their own device under it, which is the normal usage. Production environments will have different UIDs from sandbox/alpha; this table will be updated as channels are provisioned.
#### POST /api/push/register
Register a device's push token. Idempotent for a given `(DeviceToken, UserID)` — re-registering the same token under the same user updates the platform endpoint rather than creating a duplicate.
**Params**
| Param | Required | Notes |
|---|---|---|
| `DeviceToken` | yes | The platform's device token: APNs device token (hex), FCM registration token, or Web Push subscription identifier |
| `Platform` | yes | `APNS_SANDBOX` (iOS Debug builds), `APNS` (iOS Release / TestFlight / App Store), `FCM` (Android), or `WEB` (browser Web Push) |
| `MailServerUID` | yes | Channel identifier from the table above — must match the build's platform |
| `DeviceName` | no | Friendly label, e.g. `iOS 26.0` — surfaced in account settings when per-device management is added |
**Returns** (envelope shape, `Successful`/`Payload`)
```json
{
"Successful": true,
"Payload": {
"EndpointArn": "",
"Platform": "APNS_SANDBOX",
"UserID": 10001
}
}
```
#### POST /api/push/unregister
Deactivate a previously-registered device. Call on sign-out so subsequent pushes for that user don't land on a device they no longer own. `MailServerUID` is not needed — the server looks up the registered device by token or id and tears down its endpoint.
**Params** (provide one)
| Param | Required | Notes |
|---|---|---|
| `DeviceToken` | one of | The token to deactivate |
| `UserDeviceID` | one of | …or the device id returned from a prior register call |
### User notification preferences
Each user has four boolean flags controlling which push notifications they receive. All flags default to on; flipping any flag off takes effect immediately for the next notification of that type targeting that user.
| Flag | Effect |
|---|---|
| `Enabled` | Master switch — when `0`, suppresses ALL push notifications regardless of category flags |
| `NotifyShare` | Gates notifications from `/share/add` |
| `NotifyReaction` | Gates notifications from `/share/react` |
| `NotifyConnection` | Gates notifications from `/access/redeem` |
#### GET /notification
Read the authenticated user's notification preferences.
```bash
curl -b /tmp/alex-cookie.txt 'https://beta-queue.dbo.io/app/queue/api/notification'
```
**Returns**
```json
[{
"Enabled": 1,
"NotifyShare": 1,
"NotifyReaction": 1,
"NotifyConnection": 1
}]
```
All flags are `0` or `1`.
#### POST /notification/edit
Update any subset of the 4 flags. Partial — flags you don't send keep their current value (or default to `1` on first call if they've never been set).
```bash
# Turn off reaction notifications, leave the rest unchanged
curl -b /tmp/alex-cookie.txt -X POST 'https://beta-queue.dbo.io/app/queue/api/notification/edit?NotifyReaction=0'
# Master off + new-share off in one call
curl -b /tmp/alex-cookie.txt -X POST 'https://beta-queue.dbo.io/app/queue/api/notification/edit?Enabled=0&NotifyShare=0'
```
**Params** (all optional, each `0` or `1`)
| Param | Effect |
|---|---|
| `Enabled` | Master switch |
| `NotifyShare` | New-share notifications |
| `NotifyReaction` | Reaction notifications |
| `NotifyConnection` | Invite-accepted notifications |
**Returns** the new full state, same shape as `GET /notification`:
```json
[{
"Enabled": 1,
"NotifyShare": 1,
"NotifyReaction": 0,
"NotifyConnection": 1
}]
```
---
## Lists
Lists are like albums — items can belong to many lists. Removing an item from a list never deletes the item itself.
### GET /list
Your lists with item counts.
**Returns**
```json
[{ "ListUID": "abc...", "Name": "Design & Culture", "Visibility": "public", "ItemCount": 3 }]
```
---
### POST /list/add
**Params**
| Param | Required | Notes |
|---|---|---|
| `Name` | yes | |
| `Visibility` | no | `private` (default) / `public` |
**Returns**
```json
[{ "ListUID": "abc...", "Name": "My Favs", "Visibility": "private" }]
```
---
### POST /list/edit
**Params**
| Param | Required | Notes |
|---|---|---|
| `ListUID` | yes | |
| `Name` | no | |
| `Visibility` | no | `private` / `public` |
**Returns** updated list record.
---
### POST /list/delete
Hard deletes the list and all its `list_item` associations. Items are untouched.
**Params**
| Param | Required | Notes |
|---|---|---|
| `ListUID` | yes | |
**Returns**
```json
[{ "DeletedListUID": "abc..." }]
```
---
### POST /list/item/add
Add an item to a list. Both must belong to the current user.
Idempotent — adding the same item twice returns the existing association.
**Params**
| Param | Required | Notes |
|---|---|---|
| `ListUID` | yes | |
| `ItemUID` | yes | |
**Returns**
```json
[{ "ListUID": "abc...", "ItemUID": "def..." }]
```
---
### POST /list/item/remove
Remove an item from a list. Deletes the `list_item` row only — item is untouched.
**Params**
| Param | Required | Notes |
|---|---|---|
| `ListUID` | yes | |
| `ItemUID` | yes | |
**Returns**
```json
[{ "ListUID": "abc...", "ItemUID": "def..." }]
```
---
### GET /list/view/[listUID]
Public list contents. No authentication required.
Returns `[]` for private or nonexistent lists — intentionally indistinguishable.
Omits sender info and reactions — public-safe fields only.
Sorted by date added to the list (most recent first).
**Returns**
```json
[{
"ItemUID": "abc...",
"URL": "https://...",
"Title": "Abstract: The Art of Design",
"Type": "watch",
"Thumbnail": "https://...",
"Description": "..."
}]
```
---
## Patterns
### Auth
All endpoints require a logged-in session. Pass `_em` + `_pw` inline or use a session cookie.
Security enforced via `CurrentUser` filter preset — not logged in returns `[]`.
### Self-contained responses
Each endpoint returns all display data inline. Name fields are resolved server-side — no secondary lookup required. Designed for cross-platform consumption (iOS, Android, web, MCP, 3rd parties).
### Structured arrays (`*Array` fields)
When an endpoint returns a collection of related rows alongside a parent row, it uses a JSON sub-array field named with the `*Array` suffix (e.g. `FromContactArray`, `ToContactArray`, `InListArray`). These fields contain a JSON-encoded string of an array of objects — decode as JSON to get a typed array. They are the canonical source for display; the only remaining flat CSV fields are name lists (e.g. `FromContactNames`) used by the server-side `Search` param.
### Emoji encoding (`reactionB64`)
MySQL's `JSON_OBJECT` / `JSON_ARRAYAGG` encodes non-BMP Unicode characters (including most emoji) as surrogate pairs (`\uD83D\uDC4F`), which some JSON parsers reject or mangle. Reaction fields are Base64-encoded before embedding in JSON (`TO_BASE64(Reaction)`). Clients must decode Base64 → UTF-8 bytes → string to get the emoji. Empty Base64 string (`""`) means no reaction.
### Idempotency
Write endpoints are safe to call multiple times. Duplicate inserts are detected and the existing record returned.
### Empty response
`[]` means not authenticated, record not found, or caller does not own the record.
All cases return the same response — no information leakage.
### UID generation
All UIDs use `LOWER(HEX(RANDOM_BYTES(16)))` — 128-bit cryptographically random, not time-based.