---
title: "Create a system announcement (admin only)"
method: POST
path: "/api/v1/announcements"
tags: ["Announcements"]
---

# Create a system announcement (admin only)

`POST /api/v1/announcements`

## Request

- Base URL: `https://api.uptimemonitoring.com`
- URL: `https://api.uptimemonitoring.com/api/v1/announcements`
- Auth: HTTP bearer

Admin-gated. Creates a new banner row.

- `title` and `body` are required; both are TrimSpace'd and
  length-checked (1..200 / 1..2000 chars).
- `published_at` is optional; when omitted the server defaults
  to `now()`.
- `expires_at` is optional; when set must be strictly greater
  than the effective `published_at`.

Decode is permissive — unknown top-level fields are silently
ignored to match the rest of the API's POST/PUT convention.
`PATCH /api/v1/account` is the documented strict-decode
exception, NOT the rule.

## Request body

- AnnouncementInput
  - `title` string, required: Short banner title. Server TrimSpace's leading and trailing whitespace before length-checking.
  - `body` string, required: Banner body / paragraph. Server TrimSpace's leading and trailing whitespace before length-checking.
  - `published_at` string, date-time: RFC 3339 timestamp at which the banner becomes active. When omitted the server defaults to `now()`.
  - `expires_at` string, date-time, nullable: RFC 3339 timestamp after which the banner is hidden. When set, must be strictly greater than the effective `published_at`. Omit (or send `null`) for no expiry.

## Response `201`

Announcement created.

- Announcement
  - `id` integer
  - `title` string
  - `body` string
  - `published_at` string, date-time
  - `expires_at` string, date-time, nullable
  - `updated_at` string, date-time: Last time this row was mutated. For rows created after migration 025 (2026-08-01), this is exact creation time if never edited. For rows that already existed when that migration ran, no creation timestamp had ever been recorded, so the backfilled starting value is `LEAST(published_at, migration time)` instead — not the row's actual original creation time. Bumped by any PUT that changes at least one field; an empty-body no-op PUT does not bump it. Strictly monotonic per row — never ties or regresses across concurrent writes.

## Other responses

- `400`: Invalid request.
- `401`: Missing or invalid credentials.
- `403`: Authenticated but the caller is not permitted on this route. Body is the opaque `{"error":"forbidden"}` envelope used for every admin-gated 403 across the API; the same shape is used whether the caller is missing the admin role or the route otherwise denies them, so clients cannot distinguish the two.
- `413`: Request body exceeds the 64 KB hard limit. Per docs/SPEC.md §Hard Limits the dispatcher rejects any payload larger than 65,536 bytes before it is decoded.
- `415`: The request `Content-Type` is not `application/json`. Write operations (POST, PUT, PATCH) require that header; a missing or non-JSON media type is rejected before the body is read. The `charset` parameter is accepted and ignored, so `application/json; charset=utf-8` is valid.
- `429`: Per-account dispatcher rate limit exceeded (200 requests/minute). The `Retry-After` header gives the integer seconds until at least one token has refilled. The `X-RateLimit-*` trio reflects the bucket whose rejection produced this response — usually the global 200/min cap; on `POST /api/v1/monitors` a 429 may instead reflect the per-route 20/min creation limit.

---

[API](https://skmtc.dev/uptimemonitoring/apis/uptimemonitoring-com-api.md) · [All operations](https://skmtc.dev/uptimemonitoring/apis/uptimemonitoring-com-api/llms.txt) · [OpenAPI document](https://skmtc.dev/uptimemonitoring/apis/uptimemonitoring-com-api/revisions/ddcb2a8bc70f?raw)
