# Send a push message

`POST /v1/push/messages`

Requires an API key with the `push:send` scope. [About keys and scopes](/docs/api/authentication)

Takes an `Idempotency-Key` header: a retry with the same key returns the first result instead of acting twice. Derive the key from what you are sending, and reuse it on every retry. [About idempotency](/docs/api/idempotency)

Sends a marketing push notification to one contact's devices on every ready app of the push project (`pushProjectId`): iOS, Android and web. It is marketing-tier: the contact needs push consent, a push suppression refuses it, and it is recorded as a marketing message. Receipts, sign-in codes and alerts belong on `POST /v1/transactional/messages` with `push`. `content.urgency` defaults to `normal`; `content.overrides.ios` takes `interruptionLevel` and `threadId`. Read what each device answered at `GET /v1/push/messages/{id}/deliveries`. An `Idempotency-Key` header is required: a retry with the same key and request returns the first push, with `Idempotency-Replay: true`, and sends nothing again; the same key with a different request is 409 `IDEMPOTENCY_KEY_REUSED`. Agent keys are refused with 403.

## Header parameters

- `Idempotency-Key` (string, required, 8–255 characters)

## Request body

- `contactId` (string, required, uuid) — The contact to notify.
- `pushProjectId` (string, required, uuid) — Reaches the contact's devices on every app of the project.
- `content` (object, required)
  - `title` (string, required, 1–100 characters) — The notification title (up to 100 characters).
  - `body` (string, required, 1–500 characters) — The notification text (up to 500 characters).
  - `actionUrl` (string, optional, uri, up to 2048 characters) — The HTTPS page opened when the notification is tapped.
  - `imageUrl` (string, optional, uri, up to 2048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
  - `iconUrl` (string, optional, uri, up to 2048 characters) — Web only; iOS and Android show the app's own notification icon.
  - `data` (object, optional) — Up to 10 string key-value pairs passed to the app untouched, for deep links and in-app routing.
  - `buttons` (object[], optional, at least 1 item, at most 2 items) — Up to 2 action buttons.
    - `title` (string, required, 1–30 characters) — The button label.
    - `url` (string, required, uri, up to 2048 characters) — The HTTPS page the button opens.
  - `collapseKey` (string, optional) — A newer notification with the same key replaces the older one on the device.
  - `sound` (string, optional, 1–64 characters) — The name of a sound the app ships; the device's default if unset.
  - `badge` (integer, optional, min 0, max 99999) — The number shown on the iOS app icon.
  - `ttlSeconds` (integer, optional, min 0, max 2419200) — Seconds a provider keeps trying an offline device (up to 28 days); defaults to 86400 (one day).
  - `urgency` (string, optional, one of `high`, `normal`) — `high` wakes the device now; `normal` lets it wait for a good moment to save battery. Unset, a transactional push is `high` and any other `normal`.
  - `overrides` (object, optional) — Fields that differ on one platform, each replacing the shared one there (a whole `data` map, not merged key by key). On iOS and Android `actionUrl` may be an app link such as `myapp://orders`; the shared one and the web one must be HTTPS.
    - `ios` (object, optional) — Replaces shared fields on iOS.
      - `title` (string, optional, 1–100 characters) — The notification title (up to 100 characters).
      - `body` (string, optional, 1–500 characters) — The notification text (up to 500 characters).
      - `actionUrl` (string, optional, up to 2048 characters)
      - `imageUrl` (string, optional, uri, up to 2048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
      - `iconUrl` (string, optional, uri, up to 2048 characters) — Web only; iOS and Android show the app's own notification icon.
      - `data` (object, optional) — Up to 10 string key-value pairs passed to the app untouched, for deep links and in-app routing.
      - `buttons` (object[], optional, at least 1 item, at most 2 items) — Up to 2 action buttons.
        - `title` (string, required, 1–30 characters) — The button label.
        - `url` (string, required, uri, up to 2048 characters) — The HTTPS page the button opens.
      - `collapseKey` (string, optional) — A newer notification with the same key replaces the older one on the device.
      - `sound` (string, optional, 1–64 characters) — The name of a sound the app ships; the device's default if unset.
      - `badge` (integer, optional, min 0, max 99999) — The number shown on the iOS app icon.
      - `ttlSeconds` (integer, optional, min 0, max 2419200) — Seconds a provider keeps trying an offline device (up to 28 days); defaults to 86400 (one day).
      - `urgency` (string, optional, one of `high`, `normal`) — `high` wakes the device now; `normal` lets it wait for a good moment to save battery. Unset, a transactional push is `high` and any other `normal`.
      - `interruptionLevel` (string, optional, one of `passive`, `active`, `time-sensitive`) — How iOS presents it: `passive` arrives silently, `time-sensitive` breaks through Focus and needs the app's Time Sensitive Notifications capability.
      - `threadId` (string, optional) — Notifications with the same thread are grouped together in Notification Center.
    - `android` (object, optional) — Replaces shared fields on Android.
      - `title` (string, optional, 1–100 characters) — The notification title (up to 100 characters).
      - `body` (string, optional, 1–500 characters) — The notification text (up to 500 characters).
      - `actionUrl` (string, optional, up to 2048 characters)
      - `imageUrl` (string, optional, uri, up to 2048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
      - `iconUrl` (string, optional, uri, up to 2048 characters) — Web only; iOS and Android show the app's own notification icon.
      - `data` (object, optional) — Up to 10 string key-value pairs passed to the app untouched, for deep links and in-app routing.
      - `buttons` (object[], optional, at least 1 item, at most 2 items) — Up to 2 action buttons.
        - `title` (string, required, 1–30 characters) — The button label.
        - `url` (string, required, uri, up to 2048 characters) — The HTTPS page the button opens.
      - `collapseKey` (string, optional) — A newer notification with the same key replaces the older one on the device.
      - `sound` (string, optional, 1–64 characters) — The name of a sound the app ships; the device's default if unset.
      - `badge` (integer, optional, min 0, max 99999) — The number shown on the iOS app icon.
      - `ttlSeconds` (integer, optional, min 0, max 2419200) — Seconds a provider keeps trying an offline device (up to 28 days); defaults to 86400 (one day).
      - `urgency` (string, optional, one of `high`, `normal`) — `high` wakes the device now; `normal` lets it wait for a good moment to save battery. Unset, a transactional push is `high` and any other `normal`.
      - `channelId` (string, optional) — The Android notification channel to post in, which the app creates; it sets the sound and importance a person can tune, e.g. one for offers and one for orders.
    - `web` (object, optional) — Replaces shared fields on web.
      - `title` (string, optional, 1–100 characters) — The notification title (up to 100 characters).
      - `body` (string, optional, 1–500 characters) — The notification text (up to 500 characters).
      - `actionUrl` (string, optional, uri, up to 2048 characters) — The HTTPS page opened when the notification is tapped.
      - `imageUrl` (string, optional, uri, up to 2048 characters) — A large picture: web and Android show it; iOS needs the app's service extension.
      - `iconUrl` (string, optional, uri, up to 2048 characters) — Web only; iOS and Android show the app's own notification icon.
      - `data` (object, optional) — Up to 10 string key-value pairs passed to the app untouched, for deep links and in-app routing.
      - `buttons` (object[], optional, at least 1 item, at most 2 items) — Up to 2 action buttons.
        - `title` (string, required, 1–30 characters) — The button label.
        - `url` (string, required, uri, up to 2048 characters) — The HTTPS page the button opens.
      - `collapseKey` (string, optional) — A newer notification with the same key replaces the older one on the device.
      - `sound` (string, optional, 1–64 characters) — The name of a sound the app ships; the device's default if unset.
      - `badge` (integer, optional, min 0, max 99999) — The number shown on the iOS app icon.
      - `ttlSeconds` (integer, optional, min 0, max 2419200) — Seconds a provider keeps trying an offline device (up to 28 days); defaults to 86400 (one day).
      - `urgency` (string, optional, one of `high`, `normal`) — `high` wakes the device now; `normal` lets it wait for a good moment to save battery. Unset, a transactional push is `high` and any other `normal`.

## Returns

Returns `202 Accepted` with a `PushMessage` in `data`. Otherwise returns an error; [Errors](/docs/api/errors) lists every code and which to retry.

- `id` (string, required, uuid)
- `channel` ("push", required)
- `status` (string, required, one of `queued`, `sending`, `skipped`, `sent`, `failed`, `delivered`, `bounced`, `complained`)
- `createdAt` (string, required, date-time)

## Example: A marketing push to one contact

```bash
curl -X POST https://api.kirora.io/v1/push/messages \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Idempotency-Key: weekend-sale-0192f3a1" \
  -H "Content-Type: application/json" \
  -d '{
    "contactId": "0192f3a1-7c2e-7d4b-9a10-3e5f6a7b8c9d",
    "pushProjectId": "0192f3a1-91d4-7b2a-8c3e-4d5e6f7a8b9c",
    "content": {
      "title": "Weekend sale starts now",
      "body": "Free delivery on every order until Sunday."
    }
  }'
```

Response `202`:

```json
{
  "data": {
    "id": "0192f3b0-2b3c-7d4e-9f50-6b7c8d9e0f1a",
    "channel": "push",
    "status": "queued",
    "createdAt": "2030-03-14T09:30:00.000Z"
  }
}
```

## Example: High urgency, grouped on iOS

```bash
curl -X POST https://api.kirora.io/v1/push/messages \
  -H "Authorization: Bearer $KIRORA_API_KEY" \
  -H "Idempotency-Key: cart-hold-7731" \
  -H "Content-Type: application/json" \
  -d '{
    "contactId": "0192f3a1-7c2e-7d4b-9a10-3e5f6a7b8c9d",
    "pushProjectId": "0192f3a1-91d4-7b2a-8c3e-4d5e6f7a8b9c",
    "content": {
      "title": "Last hour: your cart is held",
      "body": "Check out before 18:00 to keep your items.",
      "urgency": "high",
      "overrides": {
        "ios": {
          "threadId": "cart-reminders"
        }
      }
    }
  }'
```

Response `202`:

```json
{
  "data": {
    "id": "0192f3b0-4d5e-7f60-8b72-8d9e0f1a2b3c",
    "channel": "push",
    "status": "queued",
    "createdAt": "2030-03-14T09:30:00.000Z"
  }
}
```
