Skip to content
LogoLogo

HTTP API

This documentation outlines the HTTP API for the Kiwistand P2P node.

Overview

The API provides methods for interacting with the Kiwistand P2P node, including adding messages and listing messages.

Ports

Kiwistand used to ship with running both the frontend and the node REST API on the same port (HTTP_PORT). To more easily code-split the node code and frontend code, we're now, however, running the node's REST API on API_PORT. For news.kiwistand.com it is at 8443.

JSON Schemas

Pagination

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "from": {
      "type": "integer",
      "minimum": 0
    },
    "amount": {
      "type": "integer",
      "minimum": 0,
      "maximum": HTTP_MESSAGES_MAX_PAGE_SIZE
    }
  },
  "required": ["from", "amount"]
}

Message

A message is one of four objects, each with "additionalProperties": false. The schemas are upvote, comment, community and membership in src/constants.mjs.

// amplify: a submission or an upvote
{
  "timestamp": 1700000000,
  "type": "amplify",
  "title": "...",            // up to 80 characters
  "href": "https://...",     // https?://, data:text/plain, or kiwi:0x; up to 2048 characters
  "community": "0x.../ai",   // optional: posts to a community (protocol 13.0.0)
  "signature": "0x..."
}
 
// comment
{
  "timestamp": 1700000000,
  "type": "comment",
  "title": "...",            // the comment, up to 10,000 characters
  "href": "kiwi:0x...",      // the story's index (72 hex characters)
  "signature": "0x..."
}
 
// community (protocol 13.0.0)
{
  "timestamp": 1700000000,
  "type": "community",
  "name": "Agent Builders",  // up to 24 characters: words of letters and digits
  "about": "...",            // up to 100 characters, one line
  "rules": ["..."],          // up to 5, each 1 to 140 characters, one line
  "icon": "robot",           // a Phosphor Icons 2.1.1 name
  "color": "#6E56CF",        // any hex colour, #RRGGBB
  "signature": "0x..."
}
 
// join or leave a community (protocol 14.0.0)
{
  "timestamp": 1700000000,
  "type": "join",            // or "leave"
  "community": "0x.../ai",   // the community's ID
  "signature": "0x..."
}

See Communities and Membership for what they are and which EIP-712 type each message is signed with.

API Versioning

As of the current version, all API endpoints are prefixed with /api/v1. This is part of our move towards better API versioning.

Endpoints

GET /api/v1/delegations

Retrieves an object of valid delegations in the form of to:from, where from is the user's "identity" (the address messages are attributed to), whereas to can be any delegated Ethereum address.

What makes a delegation valid, how to create one and how nodes index them is on the Delegation page.

Please note that it may take some time for the indexer to pick up a new delegation (usually around 1-2 minutes).

POST /api/v1/list

Lists messages with the given pagination parameters.

Request body:

  • from (integer): The number of entries the request should be offset by. It is inclusive.
  • amount (integer): The number of entries that the request should contain. Must be between 0 and HTTP_MESSAGES_MAX_PAGE_SIZE, as defined in the .env file.
  • type (enum: ["amplify", "comment", "community", "join", "leave"], defaults to: "amplify" if the parameter is omitted): The type of the message.

Response body

  • status (string): The status of the request. Possible values are "success" or "error".
  • code (integer): The HTTP status code.
  • message (string): A brief description of the response.
  • details (string): A more detailed description of the response.
  • data (array): An array of objects, each representing a post. Each post object has the following properties:
    • href (string): The URL of the post.
    • signature (string): The signature of the post.
    • timestamp (integer): The timestamp of the post.
    • title (string): The title of the post.
    • type (string): The type of the post.
    • signer (string): The Ethereum address that signed the message. Can be a delegated address or the address that represents the user's identity.
    • identity (string): The Ethereum address the message is attributed to: the signer itself, or the address that delegated to the signer.

POST /api/v1/messages

Adds a message to the Kiwistand P2P node.

Request body:

  • timestamp (integer): Unix timestamp. Must be bigger than MIN_TIMESTAMP_SECS (1672527600), as defined in the .env file.
  • type (string): "amplify", "comment", "community", "join" or "leave".
  • The other fields of that type, see Message.
  • signature (string): Message signature. Must match the pattern "0x[a-fA-F0-9]+".

Acceptance Criteria for Messages

  1. Every message must comply with the message JSON schema.
  2. The timestamp must be bigger than MIN_TIMESTAMP_SECS (1672527600), as defined in the .env file.
  3. The timestamp must be less than MAX_TIMESTAMP_DELTA_SECS (60) seconds in the future, as defined in the .env file.
  4. The message signer's address must be a valid Ethereum address.
  5. When running ecrecover on the signature, it must reproduce a valid Ethereum address. EIP712 is used as the signing method (https://eips.ethereum.org/EIPS/eip-712).
  6. For every link, a combination of address, link, and "amplify" type can only exist once. This means that every user can only upvote a link once.
  7. A comment's story must exist and be older than the comment.
  8. An amplify with community must name a community the node has, in a version older than the post (see Communities).
  9. A join or leave must name a community the node has, in a version older than the message (see Membership). The signer's earlier joins and leaves don't matter.

When the node takes a join, it notifies the community's owner (a push notification to the iOS app and a Farcaster mini app notification), unless the owner joined their own community. Like for upvotes, the person joining needs a name and a Neynar score of at least MIN_NEYNAR_SCORE, and someone who leaves and joins again within a day notifies only once. Leaves don't notify.

Reading communities

Since protocol 13.0.0 (see Communities). The website serves these endpoints (HTTP_PORT, e.g. https://news.kiwistand.com), not the node's API_PORT. They answer in the envelope described above: status, code, message, details and data.

GET /api/v1/communities and GET /api/v1/communities/:owner/:key are cached for 30 seconds (Cache-Control: public, s-maxage=30, max-age=0, stale-while-revalidate=300), so a new community, story or member shows up within about half a minute. With member, the list isn't cached (no-cache): it changes when that person joins or leaves, and they read it right after.

GET /api/v1/communities

Lists communities, most active first: by their newest story, then the communities without stories, the newest first. The communities of a profile the moderation bans aren't listed.

Query parameters, all optional:

  • q (string): only communities whose name or about contains it, ignoring case.
  • owner (address, any case): only that owner's communities. 400 if it isn't an address.
  • member (address, any case; since protocol 14.0.0): only the communities that identity is a member of: the ones it joined and hasn't left since, and the ones it owns. Joins signed with a key it delegated to count. 400 if it isn't an address.
  • limit (integer): 50 by default, at most 200.

data is an array of Community objects:

{
  "id": "0x0f6a…/agent-builders", // as posts name it (submissions' community)
  "owner": "0x0f6A…",             // checksummed
  "ownerName": "timdaub.eth",     // ENS, Farcaster, Lens or Kiwi name, or null
  "name": "Agent Builders",
  "about": "…",
  "rules": ["…"],
  "icon": "robot",                // a Phosphor Icons 2.1.1 name
  "color": "#6E56CF",
  "created": 1700000000,          // the oldest version's timestamp
  "timestamp": 1700000500,        // the newest version's timestamp
  "stories": 12,                  // stories submitted to it
  "lastStory": 1700009999,        // the newest story's timestamp, or null
  "members": 1204                 // since 14.0.0: its members, the owner included
}

GET /api/v1/communities/:owner/:key

One Community object as data, or 404 when the node doesn't know the community. :owner may be in any case, :key is the name key.

GET /api/v1/communities/:owner/:key/feeds/:name

:name is hot, new or best. A community's feeds, namespaced by the community like its page. They take the same parameters and answer like /api/v1/feeds/:name, with only that community's stories, ranked with the same rules as Kiwi News' feeds:

  • Hot: Kiwi News' Hot ranking and rules, except that a story missing its engagement rules (enough upvotes, the submitter's karma, a known name) isn't hidden: it comes after the stories that meet them, so a small community's Hot isn't empty. Kiwi News' own picks (the app curation, predictions and the pinned story) don't apply. /api/v1/communities/:owner/:key/feed/rows pages on like /api/v1/feed/rows, with the same exception.
  • New: the community's newest 25 stories, as /new shows them. /api/v1/communities/:owner/:key/new/rows pages on from there.
  • Best: per period and page, like Kiwi News' Best.

An :owner/:key that isn't a community ID (COMMUNITY_ID_PATTERN in src/constants.mjs) is a 400.

Kiwi News' feeds (/api/v1/feeds/:name, and the website's /, /new and /best) have every story, a community's ones included, ranked as always. /api/v1/feed/rows and /api/v1/new/rows page through the same stories.

Every story of these feeds, of their rows (format=json) and of GET /api/v1/stories?index= has community: null for a Kiwi News story, else {"id", "name", "icon", "color"}.

GET /c/:owner/:key

Also /c/:owner/:key/new and /c/:owner/:key/best. The community's web pages: its Hot, New and Best, like /, /new and /best with only its stories, under its header card (icon, name, about, who created it) and with its house rules. With ?via=<address>, the page an invite link opens: who invited you, the community and its rules, to join it. 404 when the community isn't known. The app's universal links include /c/*.

GET /communities

Also /communities?tab=joined and /communities/new. The website's list of communities, most active first (q searches them like GET /api/v1/communities), the ones you joined, and the form to start a community or to edit yours (?edit=<name key>). The website still keeps the communities you joined in the browser; since protocol 14.0.0 clients sign a join or leave instead and read GET /api/v1/communities?member=<address>.

Who joined: GET /api/v1/activity

Since protocol 14.0.0, the notifications of GET /api/v1/activity?address=<address> have joins next to notifications: who joined that person's communities in the last three weeks and hasn't left since, newest first, at most 20. They're separate so that apps that don't know them yet don't show them as likes. The website's notifications page (/activity) shows them grouped per community. A join or leave purges the owner's cached notifications, like a like or a comment does.

{
  "verb": "joined",
  "timestamp": 1700000000,
  "message": {
    "index": "…",               // the join's message ID, without 0x
    "identity": "0xA1…",        // who joined
    "timestamp": 1700000000,
    "community": { "id": "0x0f6a…/agent-builders", "name": "Agent Builders", "icon": "robot", "color": "#6E56CF" }
  },
  "identities": [{ "address": "0xA1…", "displayName": "alice.eth", … }]
}

Karma

Karma counts what an identity contributed: one point for every story it submitted and one for every upvote its stories received. Upvotes it gives and comments don't count. Karma belongs to the identity, so what an account's delegated keys did counts for the account. The website serves these endpoints (HTTP_PORT, e.g. https://news.kiwistand.com) and answers in the envelope described above.

GET /api/v1/karma/:address

:address may be in any case; 400 if it isn't an address. data is {"address", "karma"}, the address checksummed:

{
  "address": "0x0f6A…",
  "karma": 1234
}

GET /api/v1/karma/top

The identities with the most karma, most first. Query parameters, both optional:

  • limit (integer): 10 by default, at most 100.
  • offset (integer): how many to skip, 0 by default.

data:

{
  "total": 2048,        // identities with karma
  "limit": 10,
  "offset": 0,
  "holders": [
    {
      "rank": 1,
      "identity": "0x0f6A…",
      "displayName": "alice.eth",   // ENS name, else the address
      "karma": 1234,
      "ensData": { "name": "alice.eth", "avatar": "https://…" } // or null
    }
  ]
}