:::endpoint POST /v1/me/devices

Stores the push token Apple handed the app, so the server can send it
notifications. One registration per app per signed-in token: calling this again,
for instance when iOS hands out a new push token, replaces the one before.

Needs no scope, the same as [signing out](/docs/api/apps/sign-out). What
actually reaches the phone is still decided by the scopes of the token when there
is something to send: a token without `mail:read` gets no mail notifications,
registered or not.

Signing out ends the registration with it. So does a password change, which
signs out every other device.

## Request

```bash
curl -X POST https://api.proppertrading.com/v1/me/devices \
  -H "Authorization: Bearer ptat_9f2c1a44_3b8e7d2f5c9a1b4e6d8f0a2c4e6b8d1f" \
  -H "Idempotency-Key: 5b0e8c1c-9f0e-4d3e-a3b1-2f7c8d9e0a1b" \
  -H "Content-Type: application/json" \
  -d '{
    "app": "mail",
    "environment": "production",
    "token": "8f3b2c1d9e0a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c",
    "locale": "nl",
    "timezone": "Europe/Amsterdam",
    "app_version": "0.1.28",
    "os_version": "27.0"
  }'
```

| Field         | Description                                                                                                                                                  |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `app`         | `mail`, `calendar` or `admin`. Mail and the calendar share a token on a phone and register separately, because each has its own bundle id                    |
| `environment` | `sandbox` for a development build, `production` for TestFlight and the App Store. A token sent to the other one is refused by Apple as if it did not exist |
| `token`       | The device token as hex                                                                                                                                      |
| `locale`      | Optional. Not used for the words yet: those follow the account's language                                                                                   |
| `timezone`    | Optional, an IANA zone. A calendar reminder says the time on this clock                                                                                      |

## Response

`201` for a new registration, `200` for one that replaced the last.

```json
{
    "data": {
        "object": "push_device",
        "app": "mail",
        "environment": "production",
        "created_at": "2026-09-26T09:12:44.000000Z",
        "last_seen_at": "2026-09-26T09:12:44.000000Z"
    },
    "meta": {
        "request_id": "req_01kz9d2m3n4p5q6r7s8t9v0w1x"
    }
}
```

## What arrives

Every notification carries, beside the `aps` dictionary, a `type` and the
account it is for, so an app with several accounts opens the right one:
`session` is the id of the signed-in device it was sent to, the same id as in
[the sessions list](/docs/api/apps/sessions).

| `type`               | Extra fields                                                             |
| -------------------- | ------------------------------------------------------------------------ |
| `mail.message`       | `user_id`, `workspace`, `message_id`, `thread_id`, `mailbox_id`          |
| `calendar.reminder`  | `user_id`, `workspace`, `event_id`, `occurrence_start`, `conference_url` |
| `admin.notification` | `user_id`, `workspace`, `kind`, `notification_id`, `action_url`          |
| `test`               | none                                                                     |

Every id in these fields is a string. Compare it with the ids the API hands
out as it is, and never parse it as a number.

`user_id` is the `id` from [your profile](/docs/api/apps/profile), such as
`usr_hraj4wtnqgrkeu9b`. `workspace` is the id from
[the environments list](/docs/api/apps/workspaces): a propfirm's slug, or
`platform`.

`message_id`, `thread_id` and `mailbox_id` are the `id`, `thread_id` and
`address_id` of the message as [a list row](/docs/api/mail/messages) carries
them, such as `msg_h3k8q2w7n4r9t5vx`, so `message_id` is what
[opening it](/docs/api/mail/message) takes. `event_id` is the `id` of the
appointment as [the calendar](/docs/api/calendar/events) hands it out.

Mail notifications are stacked per mailbox: the `thread-id` in `aps` is
`mailbox-` followed by the mailbox's id.

`admin` is Propper, which carries Mail and the calendar inside it. A device
registered as `admin` gets the mail and calendar notifications too, as long as
its token holds the scope to read them.

`admin.notification` goes to `admin` devices only, and is a notice from
[the notification centre](/docs/api/apps/notifications) on its way to the
phone as well, in the same words: `notification_id` is its `id` there, `kind`
its `type` and `action_url` the same path. `workspace` is the environment the
reader runs, a propfirm's slug for its admins and `platform` for Propper's own
staff. These kinds arrive this way:

| `kind`                           | When                                                         |
| -------------------------------- | ------------------------------------------------------------ |
| `payout_requested`               | A payout waits to be reviewed, asked for or a cash prize     |
| `payout_awaiting_payment`        | A payout was approved and waits to be paid                   |
| `affiliate_application_received` | A trader applied to become an affiliate                      |
| `affiliate_code_requested`       | An affiliate asked for a code that has to be reviewed        |
| `affiliate_payout_requested`     | An affiliate asked for their commission                      |
| `risk_flag_raised`               | A fraud detector raised a flag                               |
| `ticket_awaiting_admin`          | A support ticket waits for a person                          |
| `storage_filling_up`             | The environment is running out of storage                    |
| `mail_deliverability`            | The environment's mail bounces or draws complaints too often |
| `new_device_signed_in`           | Somebody signed in to your account on a new device           |
| `deal_awaiting_approval`         | Staff: a new propfirm paid and waits for approval            |
| `billing_payment_failed`         | Staff: a propfirm's invoice failed to collect                |
| `deal_link_expired`              | Staff: a propfirm's onboarding link expired unused           |
| `analytics_health`               | Staff: an environment's traffic looks wrong                  |

The first five go to the admins of the environment who may see that kind on
the dashboard, apart from whoever caused it. The rest go to whoever the bell
gives them to. An app should open `action_url` when it knows the screen and
fall back to the web version when it does not, as for the notification centre.

Mail notifications go for mail landing in an inbox, to everybody who may read
that mailbox and has not [switched it off](/docs/api/mail/mailbox-update), apart
from its sender. The archive, spam and the rest stay quiet. They show sender and
subject, unless the propfirm chose notifications without content. The badge is
the unread count across the mailboxes this account can read in that environment.

## Errors

| Status | Code                | Meaning                               |
| ------ | ------------------- | ------------------------------------- |
| 401    | `invalid_token`     | Not a token, or the secret is wrong   |
| 422    | `validation_failed` | A field is missing or has a bad shape |