> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wristler.eu/llms.txt
> Use this file to discover all available pages before exploring further.

# Devices

> Registering an install so it can be pushed to.

Scope `account:write`. A device belongs to whoever is signed into it, which is why
it lives under `/v1/me` with the rest of what an account owns.

```json theme={null}
POST /v1/me/devices
{ "data": { "type": "devices", "attributes": {
  "platform": "ios",
  "pushToken": "…",
  "appVersion": "2.4.0",
  "name": "iPhone 15 Pro",
  "locale": "nl"
}}}
```

Register after signing in, and again whenever the platform hands you a new token —
which Apple and Google both do on reinstall, on restore, and occasionally on their
own.

## Just send it every time

**Registering is an upsert, not a create.** The app has no durable handle on a
registration: it is reinstalled, restored onto a new phone, or simply handed a
fresh token, and in none of those cases does it know the id we gave it last time.

So the push token decides — the same token updates the row it already owns (`200`),
a new one creates a row (`201`). You do not have to remember an id, check whether
you registered before, or clean up after a reinstall.

A token that already belongs to **another user** is taken over rather than refused.
The token addresses a handset, and whoever signed in on it last is who should
receive its notifications; leaving it where it was would push the previous owner's
alerts to a phone that is no longer theirs.

## The rest

`pushToken` is write-only and never comes back in a response: the app already has
it, and anything else reading it could push to somebody else's device. It is set only
by `POST`: a `PATCH` changes `appId`, `appVersion`, `name`, `locale`, `platform` or
`apnsEnvironment`, and one carrying `pushToken` is refused with `422` — a new token is
a new registration.

`GET`, `PATCH` and `DELETE` reach only your own devices — somebody else's answers
`404`, not `403`, so the ids cannot be used to count installs. On sign-out, `DELETE`
the registration if you kept the id; otherwise simply stop sending, and it moves
along with the token the next time someone signs in on that handset.

> [!NOTE]
>
> **Sending is built.** Notifications go direct to APNs and FCM — no third-party push
> service — and every one about the person themselves also lands in the bell at
> `/v1/me/notifications`, because a push arrives or it does not and somebody who missed one
> has no way back to it. A shop's news does not: see below.
>
> The bell, as the website's inbox has it: `filter[unread]=true` (or `false`) narrows the list,
> `PATCH /v1/me/notifications/{id}` marks one read, `POST /v1/me/notifications/read` all of
> them, `DELETE /v1/me/notifications/{id}` clears one and `DELETE /v1/me/notifications` the
> whole bell — the website's "delete all".
>
> A registration the platform tells us is dead is **deleted on the spot**, so a row
> disappearing during testing is the cleanup working rather than a bug.
>
> **Send `apnsEnvironment` with an iOS registration if you can read it** (`development` or
> `production`, from the provisioning profile). A simulator build is on Apple's sandbox and an
> internal build is on production — Ad Hoc provisioning signs with
> `aps-environment=production` however much the build is called "development" — so there is no
> one setting for us to get right. Without it, the first notification probes: the default is
> tried, the other host is tried once if Apple refuses the token, and the answer is stored.
>
> **Send `appId`** with every registration: the bundle id / package of this build
> (`eu.wristler.app`, `eu.wristler.app.staging` or `eu.wristler.app.dev`). The three apps share
> one Firebase project, so the token does not say which app it is. A server that is not
> production pushes only to devices of its own app and logs the rest — a device without `appId`
> included — so staging can never reach somebody's store app. Production pushes to every device.
>
> What to do with a push: route on `data.type` and the ids beside it, never on the words —
> they are translated, and they come in the language of `locale` on this registration
> rather than the language of the account. `data.notificationId` is the bell entry it
> belongs to, so opening a push can mark the right item read.

## What arrives

Every push carries, in `data`:

* `type`: the **category**, the switch in the notification preferences that governs it;
* `event`: **what happened**, from the table below — the bell calls the same word `event`;
* the ids its screen needs, all strings, and only the ones that event has;
* `notificationId`: the bell entry, on everything about the person themselves.

Route on `event` and the ids; fall back on `type` for an `event` you do not know yet, and
on the ids for one with no screen of its own. Never on the words.

### About the person themselves

These land in the bell as well, and carry `notificationId`.

| `event` | `type` | When | Ids |
| - | - | - | - |
| `message` | `messages` | The shop wrote in a thread | `conversationId`, `messageId`, `watchId` |
| `proposal_received` | `offers` | The shop sent a price — also what accepting the buyer's offer sends | `conversationId`, `messageId`, `watchId` |
| `proposal_expiring` | `offers` | That price runs out within 6 hours and has not been paid, replaced, or made unbuyable. Once per proposal | `conversationId`, `messageId`, `watchId` |
| `saved_search` | `saved_searches` | A saved search found new watches, at the search's own interval | `searchId` |
| `price_drop` | `price_drops` | A saved watch got cheaper | `watchId` |
| `favorite_reserved` | `price_drops` | Another buyer reserved a saved watch while they pay. Not sent to that buyer | `watchId` |
| `favorite_sold` | `price_drops` | A saved watch sold. Not sent to its buyer | `watchId` |
| `payment_instructions` | `orders` | A bank-transfer order: how to pay. Its mail is always sent; this push follows the switch | `orderId`, `watchId` |
| `payment_reminder` | `orders` | That transfer has not arrived yet | `orderId`, `watchId` |
| `payment_failed` | `orders` | A card (or other non-transfer) payment was declined; the order is cancelled | `orderId`, `watchId` |
| `payment_received` | `orders` | The payment came through | `orderId`, `watchId` |
| `order_shipped` | `orders` | The shop shipped it, or changed the tracking | `orderId`, `watchId` |
| `order_delivered` | `orders` | It was delivered | `orderId`, `watchId` |
| `order_cancelled` | `orders` | A bank transfer failed and the order was cancelled | `orderId`, `watchId` |
| `order_expired` | `orders` | Nobody paid within 48 hours; the order was cancelled and the watch is back on sale | `orderId`, `watchId` |
| `on_request_reminder` | `orders` | A watch "on request" is still being got ready, some days after payment | `orderId`, `watchId` |
| `password_changed` | `security` | The password was changed | — |
| `email_changed` | `security` | The email address was changed | — |
| `two_factor_changed` | `security` | Two-factor authentication went on or off | — |
| `sign_in_method_changed` | `security` | Apple or Google was linked to the account, or unlinked | — |
| `new_sign_in` | `security` | A sign-in on a device the account was not used on before. Its sign-in is in `/v1/me/sign-ins` | — |

`security` and `orders` pushes cannot be turned off in `/v1/me/notification-preferences`
(`labels.{category}.lockedNote` says why); `saved_searches` is set per search
(`interval`, `push` in `/v1/me/saved-searches`, and `labels.saved_searches.managedPerSearch`
is `true`); every other category can, per channel. Locked
on is our side only: a phone that refuses notifications still gets no push. Everything still lands in the bell.

On the phone, the order ones collapse per order, the saved-watch ones per watch, a saved
search's per search and a thread's messages per thread (`apns-collapse-id` / FCM `tag`): a
newer one replaces the older, so the lock screen shows where it ended up rather than every
step. The bell keeps every entry.

`offer_accepted`, `offer_received` and `offer_refused` may still be on older bell entries;
none of them is sent any more.

### About a shop somebody works at

Push only, never the bell, so no `notificationId`. They carry `side: "seller"` and
`sellerId`: switch into that shop before opening anything.

| `event` | `type` | When | Ids |
| - | - | - | - |
| `shop_message` | `shop` | A buyer wrote to the shop | `conversationId`, `messageId`, `watchId` |
| `shop_offer` | `shop` | A buyer made the shop an offer | `conversationId`, `messageId`, `watchId` |
| `shop_order` | `shop` | A buyer paid for one of its watches | `orderId`, `watchId` |
| `shop_unanswered` | `shop` | Buyers have waited more than 2 hours for an answer (see below) | `conversationId` when it is one thread; none when it is several — open the inbox |

`shop_unanswered` follows the app plan's rules: the last message in the thread is the
buyer's, read or not; not an archived thread, not a watch that is sold, offline or
deleted, not a thread that led to an order, not a thread the shop marked `noReplyNeeded`
until the buyer writes again, not Wristler's own aftercare thread, not a thread closed by a
block or with a buyer Wristler blocked; one reminder per waiting message, everything
waiting in one push, and at most one per shop per 2 hours. Only messages from the last
14 days count. Checked every 15 minutes.

## A shop's news

Somebody who runs a shop can be told when a buyer writes to it, makes it an offer, or
pays for one of its watches. That is the `shop` category in the notification
preferences, and it is **on unless turned off**, like every other category: only
people who work at a shop ever receive it. `PATCH` `categories.shop.push` to `false`
for somebody who does not want it. Its `mail` is the digest of what buyers wrote and
the "your watch was sold" mail, and is switchable the same way.

These pushes carry `data.type: "shop"`, `data.side: "seller"` and `data.sellerId`,
and either `conversationId` (a message or an offer, in the shop's inbox) or `orderId`
(a paid order, in the shop's orders) — see the table above. Switch into that shop before you open anything:
the thread or order belongs to the shop's side, not to the person's own. They do not
land in the bell, so there is no `notificationId`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.