Notifications

What has happened on the boards an account can see — a comment, a card created or moved, an invitation, a due date coming up — and marking those as read.

Notifications belong to the account the key authenticates as. There is no parameter for whose notifications to read: it is always the key's own.

GET /api/data/notifications

The account's notifications, newest first.

Without limit the whole history comes back, which is what a client wants when it means to look at all of it. Pass limit to read it a page at a time, and before to carry on from where the last page ended.

Parameters

NameTypeRequiredDescription
limitintegernoPage size, at most 100. Omit for the whole history.
beforeintegernoThe id of the last notification you already have; the answer starts just after it. Ignored without limit.
unreadCardsanynoAnswer with card ids instead of notifications — see below.
boardIdintegernoWith unreadCards, limits the answer to one board.

What comes back

FieldTypeDescription
notificationsarrayThe page, newest first. Each carries id, type, boardId, cardId, message, isRead, createdAt, and the actor's actorName, actorImage and actorType where one is recorded.
hasMorebooleanWhether anything older exists past this page. Always false when no limit was given.
unreadCountintegerUnread notifications on the whole account, not on this page — so a badge stays right while a page is held.

Paging is anchored to a notification rather than to a count of rows. A notification arriving while somebody reads therefore cannot shift the window and make a page repeat itself or skip an entry, which an offset would.

curl -X GET "https://boards.example.com/api/data/notifications?limit=25" \
  -H "X-API-Key: $LOKALBOARDS_KEY"

# the next page, carrying on from the last id of the previous one
curl -X GET "https://boards.example.com/api/data/notifications?limit=25&before=418" \
  -H "X-API-Key: $LOKALBOARDS_KEY"
async function page(before) {
  const url = new URL("https://boards.example.com/api/data/notifications");
  url.searchParams.set("limit", "25");
  if (before) url.searchParams.set("before", before);

  const response = await fetch(url, { headers: { "X-API-Key": apiKey } });
  if (!response.ok) throw new Error(await response.text());
  return response.json();
}

// Walk the whole history a page at a time.
const all = [];
let cursor;
let more = true;

while (more) {
  const data = await page(cursor);
  all.push(...data.notifications);
  more = data.hasMore;
  cursor = data.notifications.at(-1)?.id;
}
<script setup>
const config = useRuntimeConfig();

const notifications = ref([]);
const hasMore = ref(false);
const unreadCount = ref(0);

async function load(before) {
  const data = await $fetch("https://boards.example.com/api/data/notifications", {
    headers: { "X-API-Key": config.lokalBoardsKey },
    query: { limit: 25, before },
  });

  notifications.value = before
    ? [...notifications.value, ...data.notifications]
    : data.notifications;
  hasMore.value = data.hasMore;
  unreadCount.value = data.unreadCount;
}

await load();
</script>

<template>
  <p>{{ unreadCount }} unread</p>
  <ul>
    <li v-for="item in notifications" :key="item.id">{{ item.message }}</li>
  </ul>
  <button v-if="hasMore" @click="load(notifications.at(-1).id)">Show older</button>
</template>
import { useCallback, useEffect, useState } from "react";

export function Example({ apiKey }) {
  const [notifications, setNotifications] = useState([]);
  const [hasMore, setHasMore] = useState(false);

  const load = useCallback(
    async (before) => {
      const url = new URL("https://boards.example.com/api/data/notifications");
      url.searchParams.set("limit", "25");
      if (before) url.searchParams.set("before", before);

      const data = await fetch(url, { headers: { "X-API-Key": apiKey } }).then((r) =>
        r.json(),
      );

      setNotifications((current) =>
        before ? [...current, ...data.notifications] : data.notifications,
      );
      setHasMore(data.hasMore);
    },
    [apiKey],
  );

  useEffect(() => {
    load();
  }, [load]);

  return (
    <>
      <ul>
        {notifications.map((item) => (
          <li key={item.id}>{item.message}</li>
        ))}
      </ul>
      {hasMore && (
        <button onClick={() => load(notifications.at(-1).id)}>Show older</button>
      )}
    </>
  );
}
<?php

$ch = curl_init('https://boards.example.com/api/data/notifications?limit=25');

curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'X-API-Key: ' . $apiKey,
]);

$response = json_decode(curl_exec($ch), true);
curl_close($ch);

// The next page starts after the last notification of this one.
$cursor = end($response['notifications'])['id'];

Only the cards with something unread

unreadCards answers a narrower question: which cards still have an unread notification on them. It is what a board view needs to mark its tiles, and it sends a handful of ids rather than every message and avatar in the history.

Add boardId to ask about one board. The answer is { "cardIds": [...] }; notifications, hasMore and unreadCount are not part of it, and limit and before do not apply.

curl -X GET "https://boards.example.com/api/data/notifications?unreadCards=1&boardId=123" \
  -H "X-API-Key: $LOKALBOARDS_KEY"
const response = await fetch(
  "https://boards.example.com/api/data/notifications?unreadCards=1&boardId=123",
  { headers: { "X-API-Key": apiKey } },
);

if (!response.ok) throw new Error(await response.text());

const { cardIds } = await response.json();
<script setup>
const config = useRuntimeConfig();

const { data } = await useAsyncData("unread-cards", () =>
  $fetch("https://boards.example.com/api/data/notifications", {
    headers: { "X-API-Key": config.lokalBoardsKey },
    query: { unreadCards: 1, boardId: 123 },
  }),
);

const unread = computed(() => new Set(data.value?.cardIds ?? []));
</script>

<template>
  <p>{{ unread.size }} card(s) with unread changes</p>
</template>
import { useEffect, useState } from "react";

export function Example({ apiKey }) {
  const [cardIds, setCardIds] = useState([]);

  useEffect(() => {
    const controller = new AbortController();

    fetch(
      "https://boards.example.com/api/data/notifications?unreadCards=1&boardId=123",
      { headers: { "X-API-Key": apiKey }, signal: controller.signal },
    )
      .then((response) => response.json())
      .then((data) => setCardIds(data.cardIds))
      .catch((error) => {
        if (error.name !== "AbortError") console.error(error);
      });

    return () => controller.abort();
  }, [apiKey]);

  return <p>{cardIds.length} card(s) with unread changes</p>;
}
<?php

$ch = curl_init('https://boards.example.com/api/data/notifications?unreadCards=1&boardId=123');

curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'X-API-Key: ' . $apiKey,
]);

$cardIds = json_decode(curl_exec($ch), true)['cardIds'];
curl_close($ch);

PATCH /api/data/notifications

Marks notifications read. Give exactly one of the three.

Parameters

NameTypeRequiredDescription
cardIdintegerone ofEverything unread about this card.
boardIdintegerone ofThe board's own notifications — an invitation, say. What belongs to a card on it stays unread until the card is read.
idintegerone ofA single notification.
curl -X PATCH "https://boards.example.com/api/data/notifications?cardId=501" \
  -H "X-API-Key: $LOKALBOARDS_KEY"
const response = await fetch(
  "https://boards.example.com/api/data/notifications?cardId=501",
  { method: "PATCH", headers: { "X-API-Key": apiKey } },
);

if (!response.ok) throw new Error(await response.text());

const data = await response.json();
<script setup>
const config = useRuntimeConfig();
const pending = ref(false);

async function markRead() {
  pending.value = true;
  try {
    return await $fetch("https://boards.example.com/api/data/notifications", {
      method: "PATCH",
      headers: { "X-API-Key": config.lokalBoardsKey },
      query: { cardId: 501 },
    });
  } finally {
    pending.value = false;
  }
}
</script>

<template>
  <button :disabled="pending" @click="markRead()">Mark read</button>
</template>
import { useState } from "react";

export function Example({ apiKey }) {
  const [pending, setPending] = useState(false);

  async function markRead() {
    setPending(true);

    const response = await fetch(
      "https://boards.example.com/api/data/notifications?cardId=501",
      { method: "PATCH", headers: { "X-API-Key": apiKey } },
    );

    setPending(false);
    return response.json();
  }

  return (
    <button disabled={pending} onClick={markRead}>
      Mark read
    </button>
  );
}
<?php

$ch = curl_init('https://boards.example.com/api/data/notifications?cardId=501');

curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'X-API-Key: ' . $apiKey,
]);

$response = json_decode(curl_exec($ch), true);
curl_close($ch);