Apache 2.0 · Open Source

Build on the unsuppressed feed.

Candor Reach is the open-source infrastructure layer for the Open Feed Network — a chronological, zero-suppression data pipeline that any developer can read from, write to, and run a node on.

https://reach.candor.network/v1
📡
Chronological feed
Every post delivered in strict timestamp order. No algorithmic ranking, no shadowbanning, no suppression. Ever.
🌐
Federated nodes
Run your own node. Sync with the network. Sovereignty distributed across independent servers worldwide.
Real-time WebSocket
Subscribe to live feed events over WebSocket. Posts arrive within milliseconds of publication.
🔧
Open SDK
Node.js, Python, and Go SDKs. Drop-in integration for any platform that wants to speak the Reach protocol.

Authentication

Candor Reach uses two auth models depending on the operation. Read operations (subscribing to feeds, querying posts) require a public app key. Write operations (publishing posts, managing accounts) require a signed user token.

App key — read access

http
GET /v1/feed HTTP/1.1
Host: reach.candor.network
Authorization: App your_app_key_here
Accept: application/json

User token — write access

http
POST /v1/posts HTTP/1.1
Host: reach.candor.network
Authorization: Bearer user_jwt_token_here
Content-Type: application/json
Getting keys: Register your app at reach.candor.network/developers to receive an app key. User tokens are issued via the OAuth 2.0 flow — see the Authentication guide for the full flow.

Quickstart

Subscribe to the live feed and publish your first post in under two minutes.

javascript
import { CandorReach } from '@candor/reach';

const reach = new CandorReach({
  appKey: process.env.CANDOR_APP_KEY,
});

// Subscribe to the live chronological feed
const feed = reach.subscribe({ limit: 20 });

feed.on('post', (post) => {
  console.log(`[${post.timestamp}] ${post.author.handle}: ${post.content}`);
});

// Publish a post (requires user token)
const result = await reach.publish({
  content:  'Hello from the open feed.',
  userToken: process.env.CANDOR_USER_TOKEN,
});

console.log(`Published: ${result.postId}`);
python
from candor_reach import CandorReach

reach = CandorReach(app_key="your_app_key")

# Fetch the latest 20 posts
posts = reach.feed.get(limit=20)
for post in posts:
    print(f"[{post.timestamp}] {post.author.handle}: {post.content}")

# Publish a post
result = reach.publish(
    content="Hello from the open feed.",
    user_token="your_user_token"
)
print(f"Published: {result.post_id}")
bash
# Fetch the live feed
curl https://reach.candor.network/v1/feed \
  -H "Authorization: App your_app_key"

# Publish a post
curl -X POST https://reach.candor.network/v1/posts \
  -H "Authorization: Bearer your_user_token" \
  -H "Content-Type: application/json" \
  -d '{"content":"Hello from the open feed."}'
Install the SDK: npm install @candor/reach · pip install candor-reach · go get github.com/candor-network/reach-go

Subscribe to feed

The primary read endpoint. Returns posts in strict chronological order — newest first. No ranking algorithm. No suppression. What was published is what you receive.

GET /v1/feed

Fetch a paginated chronological slice of the feed. Use before for pagination.

Query parameters

ParameterTypeDescription
limit optionalintegerNumber of posts to return. Default 20, max 100.
before optionalstringPost ID cursor — returns posts older than this ID. Use for pagination.
after optionalstringPost ID cursor — returns posts newer than this ID.
node optionalstringFilter to posts originating from a specific node ID.
author optionalstringFilter to posts from a specific account handle or ID.

Response

postsPost[]Array of Post objects in descending chronological order.
cursorstringPagination cursor — pass as before to fetch the next page.
hasMorebooleanWhether additional posts exist beyond this page.
nodeCountintegerNumber of nodes that contributed to this feed slice.
json — example response
{
  "posts": [
    {
      "id":        "post_9f8e7d6c5b4a",
      "content":  "The feed is live.",
      "timestamp": "2026-05-23T14:22:01.000Z",
      "author": {
        "id":     "acc_a1b2c3",
        "handle": "rolando",
        "displayName": "Rolando Cruz"
      },
      "nodeId":   "node_iad_01",
      "signature": "ed25519:a3f9c12d..."
    }
  ],
  "cursor":    "post_9f8e7d6c5b4a",
  "hasMore":   true,
  "nodeCount": 5
}

Publish to feed

POST /v1/posts

Publish a new post to the feed. The post is broadcast to all connected nodes within milliseconds and becomes immediately visible in the chronological feed. Requires a user token.

Request body

FieldTypeDescription
content requiredstringPost text content. Max 5,000 characters.
mediaUrls optionalstring[]Array of media attachment URLs. Max 4 items.
replyTo optionalstringPost ID this is a reply to.
visibility optionalstringpublic (default) or followers.
language optionalstringBCP-47 language code, e.g. en, es. Used for search indexing.

Response

postIdstringUnique ID of the created post.
timestampstringISO 8601 timestamp of publication.
nodeIdstringID of the node that accepted and broadcast the post.
signaturestringEd25519 signature of the post payload for verification.
DELETE /v1/posts/:postId

Delete a post you authored. The deletion is propagated to all nodes within the sync window. Only the original author can delete a post.

Query feed

GET /v1/posts/:postId

Fetch a single post by ID, including its full thread context.

Query parameters

ParameterTypeDescription
thread optionalbooleanIf true, includes the full reply thread. Default false.
GET /v1/accounts/:handle/posts

Fetch all posts by a specific account in reverse chronological order. Supports the same limit and before cursor parameters as /v1/feed.

Run a node

The Candor Reach network is federated. Any developer can run a node that syncs with the global network, serves local traffic, and contributes to network resilience. Nodes are the foundation of the decentralized architecture.

Node requirements: Any server with a public IP, 2GB RAM minimum, and 20GB disk. Node software is open source — github.com/candor-network/reach-node.
POST /v1/nodes/register

Register a new node with the network. Once registered, the node begins receiving the feed sync stream and is listed in the public node directory.

Request body

FieldTypeDescription
nodeUrl requiredstringPublic HTTPS URL of your node (e.g. https://mynode.example.com).
region requiredstringISO 3166-1 alpha-2 country code of the node's primary location.
operatorHandle optionalstringYour Candor account handle — shown in the public node directory.
publicKey requiredstringEd25519 public key used to verify posts broadcast by this node.

Response

nodeIdstringAssigned node identifier.
syncTokenstringToken used to authenticate the node's sync stream connection.
syncUrlstringWebSocket URL to connect to for feed synchronization.
GET /v1/nodes

List all active nodes in the network. Includes region, uptime, and sync lag statistics.

Query parameters

ParameterTypeDescription
region optionalstringFilter by country code.
status optionalstringactive (default) · all · degraded
GET /v1/nodes/:nodeId/health

Returns health metrics for a specific node: sync lag, post throughput, uptime, and last heartbeat.

Accounts

POST /v1/accounts

Create a new account on the Candor Reach network. Accounts are portable across nodes.

Request body

FieldTypeDescription
handle requiredstringUnique username. 3–30 chars, alphanumeric and underscores. Immutable after creation.
displayName optionalstringDisplay name shown on posts. Max 64 chars.
email requiredstringUsed for account recovery only. Never shown publicly.
publicKey requiredstringEd25519 public key for post signing and identity verification.
GET /v1/accounts/:handle

Fetch a public account profile. Returns display name, bio, follower counts, and node affiliation.

POST /v1/accounts/:handle/follow

Follow an account. Following filters the /v1/feed endpoint when filter=following is passed. Requires user token.

DELETE /v1/accounts/:handle/follow

Unfollow an account. Requires user token.

GET /v1/notifications

Fetch notifications for the authenticated user: replies, mentions, follows, and reposts. Requires user token.

Query parameters

ParameterTypeDescription
type optionalstringFilter by type: reply · mention · follow · repost. Omit for all.
unread optionalbooleanIf true, returns only unread notifications.
limit optionalintegerDefault 20, max 50.

WebSocket feed

Connect to the live feed over WebSocket for real-time post delivery. Posts arrive within milliseconds of being published to any node in the network.

WS wss://reach.candor.network/v1/stream

Establish a persistent WebSocket connection to receive live feed events. Authentication via query parameter on connect.

javascript
const ws = new WebSocket(
  `wss://reach.candor.network/v1/stream?appKey=${APP_KEY}`
);

ws.onopen = () => {
  // Subscribe to the global feed
  ws.send(JSON.stringify({
    type: 'subscribe',
    channel: 'feed.global',
  }));

  // Or subscribe to a specific account
  ws.send(JSON.stringify({
    type: 'subscribe',
    channel: 'feed.account',
    handle: 'rolando',
  }));
};

ws.onmessage = ({ data }) => {
  const event = JSON.parse(data);

  switch (event.type) {
    case 'post.created':
      console.log('New post:', event.post);
      break;
    case 'post.deleted':
      console.log('Deleted:', event.postId);
      break;
    case 'node.joined':
      console.log('New node:', event.nodeId);
      break;
  }
};

WebSocket event types

post.createdfeedA new post was published to the network. Payload includes the full Post object.
post.deletedfeedA post was deleted. Payload includes postId and authorHandle.
notification.newaccountNew notification for the authenticated user (reply, mention, follow, repost).
node.joinednetworkA new node joined the federation.
node.leftnetworkA node went offline or de-registered.
heartbeatsystemSent every 30 seconds. Respond with {"type":"pong"} to keep the connection alive.

Post object

{ "id": string // Unique post ID, prefixed post_ "content": string // Post text, max 5,000 chars "timestamp": string // ISO 8601 UTC publication time "author": AccountRef // { id, handle, displayName } "nodeId": string // Node that accepted the post "replyTo": string|null // Parent post ID if reply "mediaUrls": string[] // Attachment URLs, max 4 "visibility": string // "public" | "followers" "language": string|null // BCP-47 language code "signature": string // Ed25519 signature for verification "repostCount": integer // Network-wide repost count "replyCount": integer // Direct reply count }

Account object

{ "id": string // Unique account ID, prefixed acc_ "handle": string // Unique username — immutable "displayName": string // Display name "bio": string // Profile bio, max 300 chars "avatarUrl": string // Profile image URL "nodeId": string // Home node for this account "publicKey": string // Ed25519 public key "followerCount":integer // Number of followers "followingCount":integer // Number of accounts followed "postCount": integer // Total posts published "createdAt": string // ISO 8601 account creation time }

Chronological guarantee

Candor Reach makes a hard protocol guarantee: posts are always delivered in timestamp order. There is no ranking algorithm, no engagement scoring, no promoted content, and no suppression mechanism in the protocol layer.

Every post is signed with the publishing node's Ed25519 key. Signatures include the post content, author ID, and timestamp — making it cryptographically verifiable that a post was not modified or reordered after publication.

Verification: Any client can verify a post's authenticity using the publishing node's public key from GET /v1/nodes/:nodeId. No trust required — verify yourself.

Sync lag

In a federated network, posts originating on one node propagate to all other nodes via the sync protocol. Typical sync lag is under 500ms across nodes in the same region, and under 2s globally. The nodeId field on each post indicates where it originated.

Error codes

All errors return a consistent JSON body: { error, code, message }.

400INVALID_REQUESTMalformed request body or missing required field.
401MISSING_AUTHNo app key or user token provided.
401INVALID_AUTHApp key or user token is invalid or expired.
403FORBIDDENAuthenticated user does not have permission for this operation.
404NOT_FOUNDPost, account, or node does not exist.
409HANDLE_TAKENAccount handle is already registered.
413CONTENT_TOO_LONGPost content exceeds 5,000 characters.
429RATE_LIMITEDRequest rate limit exceeded. See Rate Limits.
503NODE_UNAVAILABLEThe target node is temporarily offline. Retry after the Retry-After header value.

Rate limits

Limits are per app key for read operations and per user token for write operations.

OperationLimitWindow
GET /v1/feed300 requestsPer minute
GET /v1/search60 requestsPer minute
POST /v1/posts10 postsPer minute per user
POST /v1/accounts5 registrationsPer hour per IP
WebSocket messages120 messagesPer minute per connection
Node registration3 registrationsPer day per IP
Rate limit headers: Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. When limited, a 429 is returned with a Retry-After header.

Open source

Candor Reach is fully open source under the Apache 2.0 license. You can read the code, run your own node, submit issues, and contribute pull requests.

github.com/candor-network/reach
Core protocol, node software, and SDK source code.
github.com/candor-network/reach-node
Standalone node server. Docker-ready. One command to join the network.
github.com/candor-network/reach-sdk
Official SDKs for Node.js, Python, and Go.
reach.candor.network/developers
Get your app key, manage apps, and view usage metrics.
Contributing: Read CONTRIBUTING.md in the main repo. All contributors sign a standard CLA that assigns rights to Open Feed Network Corp while preserving the Apache 2.0 public license.
LIVEAPI Playground

Test the Candor Shield API in real time. No API key required for this demo.

LIVESystem Status

Real-time health of all Candor infrastructure. Updated every 30 seconds.