# Mark posts in topic as "read'

**URL:** https://meta.discourse.org/t/mark-posts-in-topic-as-read/394852
**Category:** Development
**Tags:** rest-api
**Created:** [January 31, 2026, 3:32pm UTC](https://meta.discourse.org/t/mark-posts-in-topic-as-read/394852 "2026-01-31T15:32:42Z")
**Posts on this page:** 1
**Showing post:** 1

<div class="post-metadata">

### Author: ![kittenwater](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/kittenwater/32/541219_2.png) [@kittenwater](https://meta.discourse.org/u/kittenwater)
#### Post date: [January 31, 2026, 3:32pm UTC](https://meta.discourse.org/t/mark-posts-in-topic-as-read/394852/1 "2026-01-31T15:32:42Z")

</div>

I tried to reverse engineer the site by sending POST requests to “https://{hostUrl}/topics/timings” with content-type, csrf token, and user-agent.

Here is what the body (json) looks like:

```python
payload = {
  "topic_id": topic_id,
  "topic_time": post_count * 60000,
  "timings": timings
}

```

It returns a status code of 200 but the read history never changes at `https://{hostUrl}/u/USERNAME/activity/read`

I tried to look into this post but it wasn’t much help:

> [@Mark specific posts as "read" through the API?](https://meta.discourse.org/t/mark-specific-posts-as-read-through-the-api/198701?tl=en):
>
> Hello, I’m pulling posts through the discourse API. I’m using python for that. The request calls the following code (which makes a GET request essentially) and returns the .json similar to [this post](https://meta.discourse.org/t/198701.json) def topic\_by\_id(self, topic\_id, \*\*kwargs): return self.\_get("/t/{0}.json".format(topic\_id), \*\*kwargs) The posts returned have a 'read' flag. The posts I send are read=True, but for the posts I receive they are all marked as read=False, unless I actively log in to Discourse and read the…

Here is a good amount of the code:

```python
def get_csrf(session):
    r = session.get(f"https://{hostUrl}/session/csrf.json")

    if r.status_code != 200:
        raise RuntimeError("Failed to get CSRF")

    data = r.json()

    if "csrf" not in data:
        raise RuntimeError("No CSRF in response")

    return data["csrf"]

def load_topics(session, page):
    print(f"[Topics] Page {page}")

    r = session.get(
        f"https://{hostUrl}/latest.json?page={page}"
    )

    if r.status_code != 200:
        return []

    data = r.json()

    return [
        {
            "id": t["id"],
            "posts_count": t["posts_count"]
        }
        for t in data["topic_list"]["topics"]
    ]

def mark_post_as_read(session, topic_id, post_count):
    url = f"https://{hostUrl}/topics/timings"

    timings = {
        str(i): 60000
        for i in range(1, post_count + 1)
    }

    payload = {
        "topic_id": topic_id,
        "topic_time": post_count * 60000,
        "timings": timings
    }

    csrf = get_csrf(session)

    r = session.post(
        url,
        json=payload,
        headers={
            "X-CSRF-Token": csrf,
            "User-Agent": "Mozilla/5.0",
            "Content-Type": "application/json"
        }
    )

    print(f"[Read] {topic_id} → {r.status_code}")

    if r.status_code != 200:
        print(r.text[:300])

def tab_worker(session):
    page = 1

    while True:
        topics = load_topics(session, page)

        if not topics:
            break

        for t in topics:
            mark_post_as_read(
                session,
                t["id"],
                t["posts_count"]
            )

            time.sleep(0.4)

        page += 1

```

---

_[View the full topic](https://meta.discourse.org/t/mark-posts-in-topic-as-read/394852)._
