Search the complete documentation

Try idempotency, proposal lifecycle, authentication, or ordering.

Navigate Open
繁體中文
Browse documentation

Activity · First request

Publish a post and read a timeline

This path creates one post for the signed actor, then reads the same actor’s materialized home timeline. Run it from a trusted host server that can access the gateway secret.

1. Serialize and sign the post

const path = "/api/v1/posts";
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({
  idempotency_key: "match:match_1842:published",
  verb: "matched",
  object_ref: { type: "match", id: "match_1842" },
  body: "Won the summer qualifier",
  metadata: { season: "2026-summer" },
  render_refs: {},
  visibility: "followers"
});

const signature = signActivityRequest({
  secret: process.env.ACTIVITY_GATEWAY_SECRET,
  timestamp,
  method: "POST",
  path,
  tenantId,
  actorId,
  body
});

verb is required. Set a stable idempotency_key for each logical publish. object_ref, metadata, and render_refs default to empty objects. visibility accepts public, followers, or private.

2. Publish the post

const response = await fetch(`${activityUrl}${path}`, {
  method: "POST",
  headers: {
    "content-type": "application/json",
    "x-tenant-id": tenantId,
    "x-actor-id": actorId,
    "x-feedaas-timestamp": timestamp,
    "x-feedaas-signature": signature
  },
  body
});

if (!response.ok) throw new Error(await response.text());
const { post } = await response.json();

A successful write returns 201 Created. Activity takes actor_id from the signed header, creates the source post, materializes the author’s self timeline entry, and schedules follower fan-out when the visibility permits it.

3. Read the home timeline

Sign the read separately. Its method, path, query string, body hash, and timestamp differ from the publish request.

const path = "/api/v1/timeline/home";
const query = "limit=20";
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = signActivityRequest({
  secret: process.env.ACTIVITY_GATEWAY_SECRET,
  timestamp,
  method: "GET",
  path,
  query,
  tenantId,
  actorId,
  body: ""
});

const response = await fetch(`${activityUrl}${path}?${query}`, {
  headers: {
    "x-tenant-id": tenantId,
    "x-actor-id": actorId,
    "x-feedaas-timestamp": timestamp,
    "x-feedaas-signature": signature
  }
});

if (!response.ok) throw new Error(await response.text());
const timeline = await response.json();

The response contains materialized entries and an opaque next cursor:

{
  "data": [
    {
      "id": "timeline-entry-uuid",
      "viewer_id": "player_42",
      "reason": "self",
      "ranked_at": "2026-07-14T04:10:00Z",
      "item": {
        "post": {
          "id": "post-uuid",
          "actor_id": "player_42",
          "verb": "matched",
          "visibility": "followers"
        },
        "stats": {
          "like_count": 0,
          "comment_count": 0,
          "reply_count": 0,
          "repost_count": 0,
          "share_count": 0
        },
        "viewer": {
          "has_liked": false,
          "follows_author": true,
          "can_delete": true
        }
      }
    }
  ],
  "page": { "next_cursor": null }
}

The actual post and viewer objects contain the complete fields defined by the OpenAPI contract. The example is shortened to show the response layers.

What is synchronous

The author’s self timeline entry is created with the post. Follower fan-out runs asynchronously and is designed to converge under retries. A successful post response does not mean every follower timeline has already been updated.

Do not use PostStats as the canonical record for engagements. Those counters are also derived asynchronously.

Retry boundary

Reuse the same idempotency_key when retrying one logical publish. Within the same tenant and actor, Activity returns the original post without creating another timeline entry, fan-out job, or outbox event. Use a new key for a new publish.

Timeline reads are safe to retry with a newly signed request. Keep page.next_cursor opaque and include its exact encoded value in both the request URL and signed query string.

Next: understand ordering, fan-out, cursors, and rebuild.