Zyrable developer documentation
Social signals,ready to build with.
Connect server-side tools to authenticated real-time events, manage watched sources and queries, or request structured social data over REST.
Source activity Posts · profiles · sessions
····→
Zyrable Normalize · enrich · cache
····→
Your application Dispatch · deduplicate · act
Availability and data freshness
i
Upstream availability Some source data is obtained through the X API. Availability and freshness for affected events can reflect that service's stability. Check the official X API status page ↗ when investigating a source-specific interruption.
◴
Caching Zyrable may cache source results or derived data to control upstream usage and operating cost. A payload can reflect a recently cached state rather than the latest upstream change at the exact moment it is delivered.
Get started
Quickstart
Open a Zyrable WebSocket connection, answer server heartbeats, parse events, and reconnect with bounded backoff.
! Keep your API key server-side Do not expose the key in browser code or allow complete WebSocket URLs to enter logs, telemetry, or error reports.
1 Install a WebSocket client
Terminal Copy
npm install ws
2 Connect and handle events
server.js Copy
import WebSocket from "ws" ;
const apiKey = process.env.ZYRABLE_API_KEY;
const seenEvents = new Set();
let reconnectAttempt = 0 ;
let stopped = false ;
function connect () {
const url = new URL("wss://api.zyrable.com/v1/events" );
url.searchParams.set("authorization" , apiKey);
const socket = new WebSocket(url);
socket.on("open" , () => { reconnectAttempt = 0 ; });
socket.on("message" , (buffer) => {
const message = buffer.toString();
if (message.startsWith("PING" )) {
socket.send(message.replace(/^PING/, "PONG" ));
return ;
}
const event = JSON.parse(message);
if (seenEvents.has(event.id)) return ;
seenEvents.add(event.id);
routeEvent (event.type, event);
});
socket.on("close" , () => {
if (stopped) return ;
const delay = Math.min(1_000 * 2 ** reconnectAttempt, 30_000 );
reconnectAttempt += 1 ;
setTimeout(connect, delay);
});
return socket;
}
function routeEvent (type, event) {
switch (type) {
case "tweet.update" :
case "tweet.expanded.update" :
case "tweet.complete.update" :
console.log("Post update" , event.tweet.id);
break ;
default :
console.log("Zyrable event" , type);
}
}
const socket = connect ();
function shutdown () { stopped = true ; socket.close(); }
process.on("SIGINT" , shutdown);
process.on("SIGTERM" , shutdown);
The reconnect schedule shown here is client behavior, not a server timing guarantee. The protocol does not define close-code-specific retry rules.
3 Add event handlers
Use the literal type field to dispatch events and retain processed id values for the deduplication window appropriate to your application.
Next Browse all 17 events →
API basics
HTTP requests
All REST and monitoring endpoints use the Zyrable API host, JSON responses, and an API key supplied through the Authorization header.
BASE https://api.zyrable.com/v1Copy
Authentication
Send the API key directly in the required Authorization header. Keep it on a trusted server and never expose it in public client code.
HTTP request Copy
curl "https://api.zyrable.com/v1/watched" \
-H "Authorization: $ZYRABLE_API_KEY" \
-H "Accept: application/json"
Rate limits
Limits vary by endpoint. Every response includes the values needed to pace requests and back off after a 429.
x-rate-limit-limitRequests allowed in the current interval.
x-rate-limit-remainingRequests remaining in the current interval.
x-rate-limit-reset-after-msMilliseconds until the interval resets.
Check for HTTP 429. Parse x-rate-limit-reset-after-ms as a number. Wait that many milliseconds before retrying.
Error responses
Errors use a stable JSON shape containing a short code and human-readable message.
Error shape Copy
{
"code" : "NOT_FOUND" ,
"message" : "The resource you requested was not found."
}
Status Code Meaning
401UNAUTHORIZEDAuthorization is missing or invalid.
400NOT_WATCHEDThe requested profile is not watched.
404NOT_FOUNDThe requested resource was not found.
400LIMIT_REACHEDThe current plan limit has been reached.
403ACCOUNT_LOCKEDService access is locked for the account.
400RESOURCE_PRIVATEThe requested resource is private.
400USER_SUSPENDEDThe requested profile is suspended.
400INVALID_QUERYThe supplied query is invalid.
500SERVER_ERRORThe request failed inside Zyrable.
429RATE_LIMITEDThe endpoint rate limit was exceeded.
Real-time API
Connect to the event stream
Authenticate one WebSocket connection and implement the Zyrable heartbeat protocol.
WSS wss://api.zyrable.com/v1/eventsCopy
Authenticate
Pass the Zyrable API key in the required authorization query parameter.
authorizationstring required Your Zyrable API key.
JavaScript Copy
const url = new URL("wss://api.zyrable.com/v1/events" );
url.searchParams.set("authorization" , process.env.ZYRABLE_API_KEY);
const socket = new WebSocket(url);
! Query strings can be logged Redact connection URLs in application, proxy, and error logs. Never commit an API key to source control.
Respond to heartbeats
Server string messages beginning with PING must receive the corresponding PONG message. You may also send pings and use the response to estimate connection latency.
PING…→ Your client → PONG…
Reconnect safely
Stop reconnecting during an intentional shutdown. Retry transient closures with bounded exponential backoff. Reset backoff after a successful connection. Keep event deduplication state across reconnects. Surface repeated authentication failures instead of retrying indefinitely.
Real-time API
Event lifecycle and reference
Every JSON event has a unique id and a literal type. Use the ID for deduplication and the type for dispatch.
◴ Observed state may be cached Some source results and derived data may be cached. Treat payloads as observed state rather than a guarantee that every field matches the upstream platform at the exact instant of delivery. For source incidents, check the X API status page ↗ .
Progressive post lifecycle
New posts can be delivered in three progressively enriched stages. No exact timing is guaranteed.
01 tweet.updateFirst available representation.
→ 02 tweet.expanded.updateReferenced and rich content.
→ 03 tweet.complete.updateMost complete hydrated form.
Post pipeline 5 events
tweet.updateInitial post update + type: "tweet.update";
tweet: TwitterTweet;
tweet.expanded.updateExpanded post update + type: "tweet.expanded.update";
tweet: TwitterTweet;
tweet.complete.updateComplete post update + type: "tweet.complete.update";
tweet: TwitterTweet;
tweet.deleted.updateRecent post deleted + type: "tweet.deleted.update";
tweet: TwitterTweet;
search.tweets.updateWatched search results + type: "search.tweets.update";
tweets: TwitterTweet[];
query: { id: string; value: string };
Relationships 4 events
following.updateProfile followed + type: "following.update";
following: TwitterUser;
user: TwitterUser;
others: TwitterUser[];
unfollowing.updateProfile unfollowed + type: "unfollowing.update";
unfollowing: TwitterUser;
user: TwitterUser;
affiliated.updateAffiliate added + type: "affiliated.update";
user: TwitterUser;
affiliate: TwitterUser;
unaffiliated.updateAffiliate removed + type: "unaffiliated.update";
user: TwitterUser;
affiliate: TwitterUser;
Profiles 6 events
profile.updateProfile fields changed + type: "profile.update";
user: TwitterUser;
before: TwitterUser;
modifications: string[];
profile.affiliation.updateAffiliation changed + type: "profile.affiliation.update";
user: TwitterUser;
affiliation: {
from: null | TwitterUser;
to: null | TwitterUser;
};
profile.pinned.updatePosts pinned + type: "profile.pinned.update";
user: TwitterUser;
pinned: TwitterTweet[];
profile.unpinned.updatePosts unpinned + type: "profile.unpinned.update";
user: TwitterUser;
unpinned: TwitterTweet[];
profile.suspended.updateProfile suspended + type: "profile.suspended.update";
user: TwitterUser;Fatal monitoring event: the watched profile is removed automatically.
profile.deactivated.updateProfile deactivated + type: "profile.deactivated.update";
user: TwitterUser;Fatal monitoring event: the watched profile is removed automatically.
Live sessions 1 event
profile.spaces.updateLive audio session changed + type: "profile.spaces.update";
user: TwitterUser;
session: null | TwitterSpacesSession;
before: null | TwitterSpacesSession;
Monitoring health 1 event
watched.problemsWatched profile problem detected + type: "watched.problems";
watched: Record<string, {
user: TwitterUser;
reason: string;
}>;
Monitoring API
Watched profiles
Manage the profiles Zyrable monitors, inspect plan usage, retrieve active live sessions, and identify monitoring problems.
Endpoints 5 endpoints
GET /v1/watchedList watched profiles −
Returns plan usage and a map of watched profile IDs. Set expanded to a number to include handle, priority, timestamps, and monitoring metadata.
Rate limit 10 requests / 5 secondsQuery expanded?: numberReturns { plan, watched }
200 response Copy
{
"plan" : { "limit" : 15 , "usage" : 2 , "remaining" : 13 },
"watched" : { "44196397" : "example_handle" }
}
Use immutable profile IDs when reconciling records; handles can change.
POST /v1/watchedAdd or update a profile +
Begins monitoring a profile or updates its monitoring preferences. Supply either id or handle; both are not required.
Rate limit 20 requests / 5 secondsBody id?: string · handle?: string · priority?: numberReturns { plan, user: TwitterUser }
i Priority uses plan capacity Priority defaults to 1. Each additional point consumes the same plan usage as another priority-1 profile.
DELETE /v1/watched/:idRemove a profile +
Stops monitoring the profile identified by its immutable profile ID and frees the associated plan usage.
Rate limit 10 requests / 1 secondPath id: stringReturns { plan }
GET /v1/watched/spacesList active live sessions +
Returns live sessions involving any currently watched profile, keyed by profile ID.
Rate limit 10 requests / 5 secondsReturns Record<string, TwitterSpacesSession>
GET /v1/watched/:id/problemsInspect monitoring problems +
Returns monitoring problems for a watched profile. The detailed request contract uses the profile id path parameter.
Rate limit 10 requests / 5 secondsPath id: stringReturns Record<string, WatchedProblem>
Monitoring API
Watched searches
Manage persistent social search queries and receive matching posts through search.tweets.update events.
◴ Query results may be cached Zyrable may cache source results or derived data to control upstream usage and operating cost. Validate query behavior before relying on it for long-running monitoring.
Endpoints 3 endpoints
GET /v1/search/watchedList watched queries −
Returns search plan usage and a map of watched query IDs to their query text and recent volume.
Rate limit 10 requests / 5 secondsReturns { plan, watched }
200 response Copy
{
"plan" : { "usage" : 1 , "limit" : 5 , "remaining" : 4 },
"watched" : {
"e3fb5eb4-527d-40cd-8e9c-19864dac3f77" : {
"query" : "(product OR launch)" ,
"tweets_in_last_minute" : 1
}
}
}
POST /v1/search/watchedAdd a watched query +
Begins monitoring a raw search query.
Rate limit 10 requests / 5 secondsBody query: stringReturns { plan, query }
JSON body Copy
{ "query" : "(product OR launch)" }
DELETE /v1/search/watched/:idRemove a watched query +
Stops monitoring the query identified by its Zyrable query ID.
Rate limit 10 requests / 5 secondsPath id: stringReturns { plan, watched }
REST API
Data endpoints
Retrieve public posts, profiles, relationships, and communities without first adding the source to watched monitoring.
i Freshness and availability Responses can reflect cached source results or derived data, and affected requests rely on X API stability. Check the official X API status page ↗ during source-specific incidents.
→ Forward pagination Paginated endpoints accept the prior x-next-cursor value in the x-cursor request header. Retain earlier cursors yourself if your application needs backward navigation.
Posts 2 endpoints
GET /v1/data/tweet/:idRetrieve a post − Retrieves any public post in fully expanded form.
Rate limit 7 requests / 2 secondsPath id: stringReturns TwitterTweet
GET /v1/data/tweet/:id/repliesRetrieve replies + Returns replies using likes (default), latest, or the unstable relevant ranking.
Rate limit 5 requests / 2 secondsQuery ranking?: "likes" | "latest" | "relevant"Pagination x-next-cursorReturns TwitterTweet[]
Profiles 8 endpoints
GET /v1/data/user/:id_or_handleRetrieve a profile + Retrieves a complete profile by ID or handle. Use type to resolve an ambiguous identifier explicitly.
Rate limit 7 requests / 2 secondsQuery type?: "id" | "handle"Returns TwitterUser
GET /v1/data/user/:handle/aboutRetrieve account context + Returns the account context stored at TwitterUser["profile"]["about"].
Rate limit 3 requests / 2 secondsPath handle: stringReturns TwitterUser["profile"]["about"]
GET /v1/data/user/:id/tweetsRetrieve profile posts + Returns posts authored by the profile.
Rate limit 5 requests / 2 secondsPagination x-cursor → x-next-cursorReturns TwitterTweet[]
GET /v1/data/user/:id/affiliatesRetrieve affiliates + Returns affiliate profiles for a business profile.
Rate limit 5 requests / 2 secondsPagination x-cursor → x-next-cursorReturns TwitterUser[]
GET /v1/data/user/:id/followersRetrieve followers + Returns profiles following the requested profile.
Rate limit 5 requests / 4 secondsPagination x-cursor → x-next-cursorReturns TwitterUser[]
GET /v1/data/user/:id/followers/notableRetrieve notable followers + Returns notable followers based on recently recorded Zyrable data and analytics.
Rate limit 10 requests / 2 secondsReturns TwitterUser[]
GET /v1/data/user/:id/followers/verifiedRetrieve verified followers + Returns verified profiles following the requested profile.
Rate limit 5 requests / 4 secondsPagination x-cursor → x-next-cursorReturns TwitterUser[]
GET /v1/data/user/:id/followingRetrieve following + Returns profiles followed by the requested profile.
Rate limit 5 requests / 4 secondsPagination x-cursor → x-next-cursorReturns TwitterUser[]
Communities 4 endpoints
GET /v1/data/community/:idRetrieve a community + Retrieves a public community in hydrated form.
Rate limit 5 requests / 4 secondsReturns TwitterHydratedCommunity
GET /v1/data/community/:id/tweetsRetrieve community posts + Returns community posts ranked by top (default) or latest.
Rate limit 5 requests / 4 secondsQuery type?: "top" | "latest"Pagination x-cursor → x-next-cursorReturns TwitterTweet[]
GET /v1/data/community/:id/membersRetrieve community members + Returns all members or only moderators, including each member's role and account flags.
Rate limit 5 requests / 4 secondsQuery type?: "all" | "moderators"Pagination x-cursor → x-next-cursorReturns { user: TwitterMiniUser; attributes: MemberAttributes }[]
GET /v1/data/community/:id/members/searchSearch community members + Searches members by name, handle, or related profile text.
Rate limit 5 requests / 4 secondsQuery query: stringReturns TwitterMiniUser[]
Real-time API
Data models
These contract names remain platform-specific where renaming them would misrepresent the current API payload.
i Nullability and enrichment Timestamps are JavaScript numbers in milliseconds. Nullable rich fields can be unavailable in an initial event and populated by later expanded or complete events.
WebsocketWorkerEventBase event envelope
− interface WebsocketWorkerEvent {
id: string;
type: string;
}
TwitterMiniUserCompact profile
+ interface TwitterMiniUser {
id: string;
handle: string;
profile: {
name: string;
avatar: null | string;
};
}
TwitterUserDetailed profile
+ interface TwitterUser {
id: string;
handle: string;
private: boolean;
verified: boolean;
sensitive: boolean;
restricted: boolean;
joined_at: number;
profile: {
name: string;
location: null | string;
avatar: null | string;
banner: null | string;
pinned: string[];
parody: null | boolean;
affiliates: null | boolean;
url: null | { name: string; url: string; tco: string };
badge: null | {
type: null | "BLUE" | "BUSINESS" | "GOVERNMENT";
affiliation: null | { name: string; handle: string; icon: string };
automated_by: null | { id: string; handle: string };
};
description: {
text: string;
urls: { name: string; url: string; tco: string }[];
};
about: null | {
location: null | { country: string; accurate: boolean };
verification: { verified: boolean; timestamp: null | number };
usernames: { changes: number; last_changed_at: null | number };
connection: { source: null | string };
};
};
metrics: {
likes: number;
media: number;
tweets: number;
friends: number;
followers: number;
following: number;
highlights: null | number;
affiliates: null | number;
};
}
TwitterMiniTweetCompact post
+ interface TwitterMiniTweet {
id: string;
type: "TWEET" | "RETWEET" | "QUOTE" | "REPLY";
created_at: number;
author: TwitterMiniUser;
subtweet: null | TwitterMiniTweet;
reply: null | { id: string; handle: string };
quoted: null | { id: string; handle: string };
retweet: null | { id: string; handle: string };
body: {
text: string;
urls: { name: string; url: string; tco: string }[];
mentions: { id: string; name: string; handle: string }[];
};
media: { images: string[]; videos: string[]; thumbnails: string[] };
}
TwitterTweetDetailed progressively enriched post
+ interface TwitterTweet {
id: string;
type: "TWEET" | "RETWEET" | "QUOTE" | "REPLY";
source: null | string;
created_at: number;
author: TwitterUser;
subtweet: null | TwitterTweet;
community: null | TwitterCommunity;
reply: null | { id: string; handle: string };
quoted: null | { id: string; handle: string };
retweet: null | { id: string; handle: string };
body: {
text: string;
translation: {
available: boolean;
result: null | {
text: string;
languages: { from: string; to: string };
};
};
urls: { name: string; url: string; tco: string }[];
mentions: { id: string; name: string; handle: string }[];
components: (
| { type: "text"; text: string; bold: boolean; italics: boolean }
| { type: "image"; url: string }
| { type: "video"; url: string }
)[];
};
media: { images: string[]; videos: string[]; thumbnails: string[] };
grok: null | {
id: string;
conversation: {
from: "USER" | "AGENT";
message: string;
images: string[];
}[];
};
card: null | {
url: string;
image: string;
title: string;
description: string;
};
poll: null | {
ends_at: number;
updated_at: number;
choices: { label: string; count: number; image: null | string }[];
};
editable: null | {
latest: string;
until_ms: number;
remaining: number;
history: string[];
};
article: null | {
id: string;
url: string;
title: string;
author: TwitterUser;
thumbnail: null | string;
created_at: number;
updated_at: number;
body: {
text: string;
components: (
| { type: "divider" }
| {
type: "text";
variant:
| "header-one" | "header-two" | "paragraph"
| "blockquote" | "ordered-list" | "unordered-list"
| "latex-box" | "markdown-box";
lines: {
text: string;
styles: {
from: number;
to: number;
text: "bold" | "italics" | "strikethrough";
}[];
urls: { from: number; to: number; url: string }[];
mentions: { from: number; to: number; handle: string }[];
}[];
}
| {
type: "media";
variant: "image" | "gif" | "video";
url: string;
thumbnail: string;
caption: null | string;
}
| {
type: "tweet";
tweet: { id: string; url: string; object: null | TwitterTweet };
}
)[];
};
};
metrics: {
likes: number;
quotes: number;
replies: number;
retweets: number;
advanced: null | { views: number };
};
}Rich fields retain their documented nullability and are progressively populated as source data is enriched.
TwitterCommunityCommunity attached to a post
+ interface TwitterCommunity {
id: string;
name: string;
category: null | string;
description: string;
banner: null | string;
created_at: number;
nsfw: boolean;
joinable: boolean;
creator: null | { id: string; handle: string };
facepiles: null | { id: string; avatar: string }[];
rules: { id: string; name: string }[];
metrics: { members: number };
}
TwitterHydratedCommunityExpanded community information
+ interface TwitterHydratedCommunity extends TwitterCommunity {
creator: null | TwitterUser;
}
TwitterSpacesSessionLive audio session
+ interface TwitterSpacesSession {
id: string;
url: string;
title: string;
creator: TwitterUser;
live: boolean;
joinable: boolean;
replayable: boolean;
created_at: number;
started_at: number;
updated_at: number;
scheduled_start_at: null | number;
administrators: TwitterMiniUser[];
speakers: TwitterMiniUser[];
listeners: TwitterMiniUser[];
metrics: { participants: number; participated: number };
}
Reference
Protocol contract
The concise, public connection requirements for the Zyrable real-time channel.
Transport WebSocket / WSS
URL wss://api.zyrable.com/v1/events
Authentication Required authorization query parameter
Heartbeat Reply to every server PING… with the corresponding PONG…
Client latency Clients may send pings and measure the matching response
Event identity Every JSON event includes a unique id
Dispatch Every JSON event includes a literal type
What the contract does not promise
No undocumented delivery guarantee is implied. No exact progressive-enrichment timing is promised. No close-code-specific retry schedule is defined. Source availability and freshness can reflect upstream service health and caching.
Security checklist
01 Keep keys server-side Never construct authenticated connections in public browser code.
02 Redact query strings Connection URLs can surface in application and proxy logs.
03 Handle auth failures Surface repeated API key failures instead of retrying indefinitely.