---
title: "Update a system announcement (admin only)"
method: PUT
path: "/api/v1/announcements/{announcementID}"
tags: ["Announcements"]
---

# Update a system announcement (admin only)

`PUT /api/v1/announcements/{announcementID}`

## Request

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

Admin-gated. Body fields are all optional; only fields present
in the request are mutated. An empty body `{}` returns 200
with the row unchanged.

`expires_at` carries three-state semantics:
  - **absent**         → column is left unchanged.
  - **explicit `null`** → column is set to NULL (no expiry).
  - **concrete value**  → column is set to the supplied
    timestamp; must be strictly greater than the effective
    `published_at` if both are supplied in the same request.

`title` and `body`, when supplied, are TrimSpace'd and
length-checked (1..200 / 1..2000 chars).

Decode is permissive — unknown top-level fields are silently
ignored to match the rest of the API's POST/PUT convention.

## Path parameters

- `announcementID` integer, required

## Request body

- AnnouncementUpdate: All fields optional. Only fields present in the request body are mutated; any field that is absent is left untouched on the row. `expires_at` distinguishes three states: - **absent** → column unchanged. - **explicit `null`** → column set to NULL (no expiry). - **concrete value** → column set to the supplied value.
  - `title` string
  - `body` string
  - `published_at` string, date-time
  - `expires_at` string, date-time, nullable

## Response `200`

Announcement updated (or unchanged on empty body).

- 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.
- `404`: No announcement exists with the given ID.
- `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)
