Skip to content

Broadcasts Admin API

The broadcasts admin API allows platform superadmins to compose, preview, and publish platform-wide announcements. Broadcasts are targeted to an audience derived from audience_spec and delivered via BroadcastFanoutJob.

Admin-only

All /admin/broadcasts and /admin/notification_categories endpoints require a platform-admin account. Non-admin requests receive 403 Forbidden.


Broadcast lifecycle

draft → published
  POST /admin/broadcasts          → creates a draft (published_at = null)
  GET  /admin/broadcasts/:id/audience_preview  → check audience before publishing
  POST /admin/broadcasts/:id/publish           → sets published_at, enqueues BroadcastFanoutJob

Once published, a broadcast cannot be unpublished.


Audience targeting

Every broadcast requires an audience_spec object that controls who receives it. BroadcastFanoutJob resolves the account set at publish time using the same Broadcasts::AudienceResolver class used by the preview endpoints.

type Target Additional fields
all Every account
organization All accounts in one org organization_id (UUID)
feature_enabled Accounts in orgs with a feature enabled feature_key (string)
feature_provisioned Accounts in orgs with a feature provisioned feature_key (string)

Examples

{ "type": "all" }
{ "type": "organization",      "organization_id": "org-uuid" }
{ "type": "feature_enabled",   "feature_key": "kubernetes" }
{ "type": "feature_provisioned", "feature_key": "apps" }

Broadcast endpoints

List broadcasts

GET /admin/broadcasts
Authorization: Bearer <admin-token>

Returns the 50 most recently created broadcasts (drafts and published).

Response 200

{
  "metadata": { "organization_id": null, "timestamp": "2026-06-30T20:00:00Z" },
  "data": [
    {
      "id": "uuid",
      "title": "Maintenance tonight",
      "body": "Scheduled maintenance 00:00–02:00 UTC.",
      "severity": "notice",
      "audience_spec": { "type": "all" },
      "banner": true,
      "channels": ["in_app"],
      "created_by_id": "acct-uuid",
      "published_at": "2026-06-30T20:00:00Z",
      "audience_count": null,
      "created_at": "2026-06-30T19:00:00Z"
    }
  ]
}

published_at: null = draft. audience_count is populated after fanout when the job records it back; otherwise null.


Create a broadcast (draft)

POST /admin/broadcasts
Authorization: Bearer <admin-token>
Content-Type: application/json

Creates a draft. The broadcast is not delivered until POST /admin/broadcasts/:id/publish.

Request body

{
  "title": "Scheduled maintenance",
  "body": "We will perform maintenance on 2026-07-01 00:00–02:00 UTC.",
  "severity": "notice",
  "audience_spec": { "type": "all" },
  "banner": true,
  "channels": ["in_app"]
}
Field Required Description
title yes Short summary shown in the banner and inbox
body yes Full message text
severity yes info, notice, or critical
audience_spec yes Targeting spec — see table above
banner no true to show as a dismissible top-of-app banner (default false)
channels no Delivery channels — in_app and/or email (defaults to ["in_app"])

Response 201 — returns the created broadcast object (same shape as list item).

Response 422

{
  "code": "validation_error",
  "message": "Title can't be blank, Audience spec type must be one of: all, organization, feature_enabled, feature_provisioned"
}

Show a broadcast

GET /admin/broadcasts/{id}
Authorization: Bearer <admin-token>

Returns a single broadcast by ID.

Response 200 — broadcast object.

Response 404 — broadcast not found.


Update a broadcast (draft only)

PATCH /admin/broadcasts/{id}
Authorization: Bearer <admin-token>
Content-Type: application/json

Updates any writable field. All fields are optional.

Request body — any subset of the create body fields.

Response 200 — updated broadcast object.


Publish a broadcast

POST /admin/broadcasts/{id}/publish
Authorization: Bearer <admin-token>

Sets published_at to the current time and enqueues BroadcastFanoutJob to deliver to the resolved audience. Once published, published_at cannot be cleared.

Response 200

{
  "metadata": { "organization_id": null, "timestamp": "2026-06-30T20:00:00Z" },
  "data": {
    "id": "uuid",
    "published_at": "2026-06-30T20:05:00Z"
  }
}

Response 404 — broadcast not found.


Audience preview (persisted broadcast)

GET /admin/broadcasts/{id}/audience_preview
Authorization: Bearer <admin-token>

Returns the estimated recipient count for an existing broadcast without triggering fanout. Use this to verify scope before publishing.

Response 200

{
  "metadata": { "organization_id": null, "timestamp": "2026-06-30T20:00:00Z" },
  "data": { "audience_count": 47 }
}

Audience preview (spec-only, no persist)

POST /admin/broadcasts/audience_preview
Authorization: Bearer <admin-token>
Content-Type: application/json

Returns the estimated recipient count from a submitted audience_spec without persisting any broadcast. Useful when composing the draft and wanting to check audience size before saving.

Request body

{
  "audience_spec": { "type": "feature_enabled", "feature_key": "kubernetes" }
}

Response 200

{
  "metadata": { "organization_id": null, "timestamp": "2026-06-30T20:00:00Z" },
  "data": { "audience_count": 12 }
}

Response 422 — invalid audience_spec:

{
  "code": "validation_error",
  "message": "type must be one of: all, organization, feature_enabled, feature_provisioned"
}

Notification categories (admin)

Superadmins can toggle mandatory_for_admins and default_channels on any category. Changes take effect on the next delivery — existing preference rows are not back-filled.

List categories (admin view)

GET /admin/notification_categories
Authorization: Bearer <admin-token>

Returns all categories. Same shape as GET /notification_categories (see Notifications API).


Update a category

PATCH /admin/notification_categories/{id}
Authorization: Bearer <admin-token>
Content-Type: application/json

Writable fields

Field Type Description
mandatory_for_admins boolean Force in_app on for org admins
default_channels string[] Default channel set for accounts without a preference row

Request body

{
  "mandatory_for_admins": false,
  "default_channels": ["in_app"]
}

Response 200 — updated category object.

Response 404 — category not found.

Response 422 — validation error.