# Send a bulk notification

`POST https://api.coach-platform.com/api/public/notifications/bulk`

Send the same in-app notification to up to 100 trainees, immediately or on a schedule. Requires the notifications:send scope, which is separate from messaging:send (WhatsApp). Trainees this key may not reach are returned in skipped, and trainees over the 10-per-day cap in rateLimited; both are excluded from the send, and a 429 is returned only when no recipient remains. Trainees who opted out of broadcast notifications are returned in optedOut and excluded too; a 400 is returned when every reachable trainee opted out. High-risk — send an Idempotency-Key so a retry cannot double-notify.

- Resource: Notifications
- Authentication: API key in the `Authorization: Bearer cp_live_...` header
- HTML version: https://www.coach-platform.com/docs/api/reference/post-notifications-bulk
- OpenAPI spec: https://www.coach-platform.com/openapi.json

## Header parameters

| Name | Type | Required | Description |
|---|---|---|---|
| `Idempotency-Key` | string | No | Optional. Send a unique key (e.g. a UUID) to make this POST safe to retry. The same key within 24h returns the original result instead of creating a duplicate. |

## Request body

Content type: `application/json`. Required.

| Field | Type | Required | Description |
|---|---|---|---|
| `traineeIds` | string[] | Yes | Between 1 and 100 trainee ids Minimum items: `1`. Maximum items: `100`. |
| `title` | string | Yes | Short notification title |
| `message` | string | Yes | Body text |
| `type` | string | No | Delivery style. "push" (default) is a standard push notification, "popup" shows an in-app popup card, "both" sends a push and shows the popup. Never sends WhatsApp. Allowed values: `push`, `popup`, `both`. |
| `popupData` | object | No | Content of the in-app popup card. Only used when type is "popup" or "both". |
| `popupData.title` | string | No |   |
| `popupData.description` | string | No |   |
| `popupData.videoLink` | string | No |   |
| `popupData.actionLink` | string | No | URL opened when the call-to-action button is pressed |
| `popupData.buttonText` | string | No | Call-to-action button label |
| `popupData.imageUrl` | string | No |   |
| `popupData.dismissible` | boolean | No | Whether the trainee can dismiss the popup. Defaults to true. |
| `scheduledAt` | string | No | Shorthand for a single future delivery: an ISO date-time. Use schedule instead for recurring sends. Cannot be combined with schedule. |
| `schedule` | object | No | Deliver later, optionally repeating. Omit both schedule and scheduledAt to send immediately. Cannot be combined with scheduledAt. |
| `schedule.startDate` | string | Yes | First send date, ISO-8601. Either a date ("2026-06-15") or a full date-time. |
| `schedule.timeOfDay` | string | Yes | Local time of day in 24h "HH:mm" format, e.g. "09:30" Pattern: `^([01]?[0-9]\|2[0-3]):[0-5][0-9]$`. |
| `schedule.frequency` | string | No | Defaults to "once" (a single future send). Any other value repeats until endDate. Allowed values: `once`, `daily`, `weekly`, `biweekly`, `triweekly`, `monthly`. |
| `schedule.weeklyDays` | number[] | No | 0=Sunday..6=Saturday. Required for weekly, biweekly and triweekly. |
| `schedule.monthlyMode` | string | No | For monthly: byDate uses monthlyDays, byWeekday uses monthlyWeekday plus monthlyWeekOfMonth. Defaults to byDate. Allowed values: `byDate`, `byWeekday`. |
| `schedule.monthlyDays` | number[] | No | Days of the month for monthly byDate |
| `schedule.monthlyWeekday` | number | No | 0=Sunday..6=Saturday for monthly byWeekday Minimum: `0`. Maximum: `6`. |
| `schedule.monthlyWeekOfMonth` | number | No | 1-4, or -1 for the last week of the month Allowed values: `1`, `2`, `3`, `4`, `-1`. |
| `schedule.endDate` | string | No | Optional ISO date that stops a recurring schedule |
| `schedule.timezone` | string | No | IANA timezone. Defaults to Asia/Jerusalem. |

## Responses

| Status | Description |
|---|---|
| `201` Created |  |
| `400` Bad Request |  |
| `401` Unauthorized |  |
| `403` Forbidden |  |
| `404` Not Found |  |
| `409` Conflict |  |
| `429` Too Many Requests |  |
| `500` Internal Server Error |  |

### 201 Created

Content type: `application/json`

| Field | Type | Description |
|---|---|---|
| `data` | object |   |
| `data.id` | string \| null |   |
| `data.type` | string | Allowed values: `push`, `popup`, `both`. |
| `data.status` | string | Allowed values: `sent`, `scheduled`. |
| `data.recipients` | number | How many trainees the notification was delivered to |
| `data.scheduledFor` | string \| null | ISO date-time of the first delivery when scheduled |
| `data.skipped` | string[] | Trainee ids the key may not reach. They were left out of the send. |
| `data.skippedCount` | number |   |
| `data.optedOut` | string[] | Trainee ids that opted out of broadcast notifications. They were left out of the send. |
| `data.optedOutCount` | number |   |
| `data.rateLimited` | string[] | Trainee ids that already hit their daily notification cap. They were left out of the send. |
| `data.rateLimitedCount` | number |   |
| `warnings` | object[] | Non-fatal problems with follow-up writes. The resource was created, but each listed field was not applied. |
| `warnings[].field` | string |   |
| `warnings[].message` | string |   |

Example:

```json
{
  "data": {
    "id": "<string>",
    "type": "push",
    "status": "sent",
    "recipients": 123,
    "scheduledFor": "<string>",
    "skipped": [
      "<string>"
    ],
    "skippedCount": 123,
    "optedOut": [
      "<string>"
    ],
    "optedOutCount": 123,
    "rateLimited": [
      "<string>"
    ],
    "rateLimitedCount": 123
  },
  "warnings": [
    {
      "field": "<string>",
      "message": "<string>"
    }
  ]
}
```

### Error responses 400, 401, 403, 404, 409, 429, 500

Content type: `application/json`

| Field | Type | Description |
|---|---|---|
| `error` | object |   |
| `error.code` | string |   |
| `error.message` | string |   |
| `error.fix` | string \| null |   |
| `error.details` | object \| null |   |
| `error.retryAfterMs` | number \| null |   |

Example:

```json
{
  "error": {
    "code": "<string>",
    "message": "<string>",
    "fix": "<string>",
    "details": {},
    "retryAfterMs": 123
  }
}
```

## Examples

### cURL

```bash
curl --request POST \
  --url 'https://api.coach-platform.com/api/public/notifications/bulk' \
  --header 'Authorization: Bearer cp_live_...' \
  --header 'Content-Type: application/json' \
  --data '{
  "traineeIds": [
    "<string>"
  ],
  "title": "<string>",
  "message": "<string>"
}'
```

### JavaScript (fetch)

```js
const response = await fetch('https://api.coach-platform.com/api/public/notifications/bulk', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer cp_live_...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "traineeIds": [
      "<string>"
    ],
    "title": "<string>",
    "message": "<string>"
  }),
});

const data = await response.json();
```

## Related endpoints

- [Send a notification](https://www.coach-platform.com/docs/api/reference/post-notifications/index.md): `POST /api/public/notifications`. Send an in-app notification to one trainee, immediately or on a schedule.
- Previous: [Send a notification](https://www.coach-platform.com/docs/api/reference/post-notifications/index.md): `POST /api/public/notifications`. Send an in-app notification to one trainee, immediately or on a schedule.
- Next: [Get the monitoring overview](https://www.coach-platform.com/docs/api/reference/get-monitoring-overview/index.md): `GET /api/public/monitoring/overview`. Monitoring overview: counts of trainees inactive in training, nutrition, water and weight, plus adherence graph data.
