BIO.RE
Content

Get Active Announcements

Public list of currently-active announcements. Filtered by active flag + time window. Cap 50. Cache 60s. HTML body server-side sanitized.

GET /api/v1/public/announcements โ€” ๐ŸŒ Public ยท Rate limit: 60 req / minute

Returns active announcements โ€” site-wide banners / notices admin-managed. Filter is active: true AND startsAt <= now (or null) AND endsAt >= now (or null). Cap is 50 (server-side take: 50). Each item's content is sanitized via the same helper as CMS pages (<script>, <style>, <iframe>, inline on* event handlers stripped). Cache window is shorter than other content endpoints (60s s-maxage, no SWR) โ€” announcements need fresher reads (e.g. urgent maintenance notices).

Time window is open-ended. startsAt: null means "active from forever ago", endsAt: null means "active forever". Both sides nullable, both sides matched. A row with both null is "active until admin disables it".

Sanitization is defensive, not exhaustive. Same caveat as GET /public/pages/:slug โ€” strips obvious XSS but admin authors are still ultimately trusted. Use client-side sanitization for additional defense.

Request

No body, no params, no headers required.

Response headers

HeaderValue
Cache-Controlpublic, s-maxage=60

Note: shorter cache than other content endpoints (60s, no stale-while-revalidate). Urgent announcements should propagate quickly.

Response

200 OK โ€” ArrayApiResponseOf<PublicAnnouncementDto>

{
  "success": true,
  "data": [
    {
      "title": "Scheduled Maintenance",
      "content": "<p>We will be down for maintenance...</p>",
      "type": "MAINTENANCE",
      "locale": "en",
      "startsAt": "2026-05-01T00:00:00.000Z",
      "endsAt": "2026-05-01T02:00:00.000Z"
    },
    {
      "title": "Welcome to BIO.RE",
      "content": "<p>Thanks for joining...</p>",
      "type": "INFO",
      "locale": null,
      "startsAt": null,
      "endsAt": null
    }
  ]
}

Item fields

FieldTypeNotes
titlestringAnnouncement title
contentstringServer-side sanitized HTML body
typestringINFO / WARNING / CRITICAL / MAINTENANCE (admin-set; clients branch on this for color/icon)
localestring | nullLocale code; null means the announcement is locale-agnostic (shown to all locales)
startsAtstring (ISO 8601) | nullWhen the announcement becomes active. null = active from beginning.
endsAtstring (ISO 8601) | nullWhen it stops being active. null = no end date.

Stripped fields

id, active (always true here, filter excludes false), publishedBy, createdAt, updatedAt, internal metadata โ€” all excluded.

Ordering

Server returns up to 50 rows ordered by createdAt DESC (newest first). There is no priority field exposed; admins control display order via timing.

Errors

HTTPcode / i18nKeyReason
429(throttle)Rate limit exceeded (60 req/min)

Side effects

  1. prisma.announcement.findMany({ where: { active: true, OR: [startsAt null OR <= now], AND: [endsAt null OR >= now] }, orderBy: createdAt desc, take: 50 }).
  2. Map each row โ†’ strip internal fields, sanitize content via sanitizeHtml().
  3. Return the array. No mutations.

Code samples

curl https://api.bio.re/api/v1/public/announcements
type AnnouncementType = 'INFO' | 'WARNING' | 'CRITICAL' | 'MAINTENANCE';

type PublicAnnouncement = {
  title: string;
  content: string;
  type: AnnouncementType | string;
  locale: string | null;
  startsAt: string | null;
  endsAt: string | null;
};

async function getActiveAnnouncements(): Promise<PublicAnnouncement[]> {
  const res = await fetch('https://api.bio.re/api/v1/public/announcements');
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Announcements fetch failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}
import { useQuery } from '@tanstack/react-query';

export const contentKeys = {
  announcements: () => ['content', 'announcements'] as const,
};

export function useActiveAnnouncements() {
  return useQuery({
    queryKey: contentKeys.announcements(),
    queryFn: async () => {
      const res = await fetch('/api/v1/public/announcements');
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Announcements fetch failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as PublicAnnouncement[];
    },
    // Match the shorter cache window โ€” announcements need fresh reads
    staleTime: 60_000,
    refetchOnWindowFocus: true,
  });
}

Try it

GET
/api/v1/public/announcements

Response Body

application/json

curl -X GET "https://loading/api/v1/public/announcements"
{
  "success": true,
  "data": [
    {
      "title": "Scheduled Maintenance",
      "content": "<p>We will be down for maintenance...</p>",
      "type": "INFO",
      "locale": "en",
      "startsAt": "2019-08-24T14:15:22Z",
      "endsAt": "2019-08-24T14:15:22Z"
    }
  ]
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/content/public-content.controller.ts269โ€“285 (getActiveAnnouncements), 29โ€“36 (sanitizeHtml)
DTO (response item)apps/api-core/src/modules/content/dto/content-public-response.dto.ts146โ€“164 (PublicAnnouncementDto)
Serviceapps/api-core/src/modules/content/content.service.ts389โ€“400 (getActiveAnnouncements)
Prisma modelpackages/prisma/prisma/schema.prismaAnnouncement (filter active = true + time window)

On this page