ZyrableDocs

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 activityPosts · profiles · sessions
ZyrableNormalize · enrich · cache
Your applicationDispatch · 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.

1Install a WebSocket client

Terminal
npm install ws

2Connect and handle events

server.js
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.

3Add event handlers

Use the literal type field to dispatch events and retain processed id values for the deduplication window appropriate to your application.

NextBrowse 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.

BASEhttps://api.zyrable.com/v1

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
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.
  1. Check for HTTP 429.
  2. Parse x-rate-limit-reset-after-ms as a number.
  3. Wait that many milliseconds before retrying.

Error responses

Errors use a stable JSON shape containing a short code and human-readable message.

Error shape
{
  "code": "NOT_FOUND",
  "message": "The resource you requested was not found."
}
StatusCodeMeaning
401UNAUTHORIZEDAuthorization is missing or invalid.
400BAD_CREDENTIALSProvided custom credentials are 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.

WSSwss://api.zyrable.com/v1/events

Authenticate

Pass the Zyrable API key in the required authorization query parameter.

authorizationstringrequired

Your Zyrable API key.

JavaScript
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 clientPONG…

Reconnect safely

  1. Stop reconnecting during an intentional shutdown.
  2. Retry transient closures with bounded exponential backoff.
  3. Reset backoff after a successful connection.
  4. Keep event deduplication state across reconnects.
  5. 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.

01tweet.update

First available representation.

02tweet.expanded.update

Referenced and rich content.

03tweet.complete.update

Most 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;
  credentials: string;
  reason: "invalid.credentials"
    | "locked.credentials"
    | "not_following.user";
}>;

Never log, transmit, or expose watched.*.credentials. Treat these values as sensitive.

Monitoring API

Watched profiles

Manage the profiles Zyrable monitors, inspect plan usage, retrieve active live sessions, and identify monitoring problems.

!
Protect custom credentials

The optional credentials value enables account-specific monitoring contexts. Treat it as sensitive and never place it in logs, analytics, or client-side code.

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 credential metadata.

Rate limit10 requests / 5 secondsQueryexpanded?: numberReturns{ plan, watched }
200 response
{
  "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 limit20 requests / 5 secondsBodyid?: string · handle?: string · priority?: number · credentials?: stringReturns{ 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 limit10 requests / 1 secondPathid: stringReturns{ plan }
GET/v1/watched/spacesList active live sessions+

Returns live sessions involving any currently watched profile, keyed by profile ID.

Rate limit10 requests / 5 secondsReturnsRecord<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 limit10 requests / 5 secondsPathid: stringReturnsRecord<string, WatchedProblem>
invalid-credentialsCustom credential cookies are invalid.
locked.credentialsThe credential account is restricted.
not_following.userThe credential account does not follow the watched profile.

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 limit7 requests / 2 secondsPathid: stringReturnsTwitterTweet
GET/v1/data/tweet/:id/repliesRetrieve replies+

Returns replies using likes (default), latest, or the unstable relevant ranking.

Rate limit5 requests / 2 secondsQueryranking?: "likes" | "latest" | "relevant"Paginationx-next-cursorReturnsTwitterTweet[]

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 limit7 requests / 2 secondsQuerytype?: "id" | "handle"ReturnsTwitterUser
GET/v1/data/user/:handle/aboutRetrieve account context+

Returns the account context stored at TwitterUser["profile"]["about"].

Rate limit3 requests / 2 secondsPathhandle: stringReturnsTwitterUser["profile"]["about"]
GET/v1/data/user/:id/tweetsRetrieve profile posts+

Returns posts authored by the profile.

Rate limit5 requests / 2 secondsPaginationx-cursorx-next-cursorReturnsTwitterTweet[]
GET/v1/data/user/:id/affiliatesRetrieve affiliates+

Returns affiliate profiles for a business profile.

Rate limit5 requests / 2 secondsPaginationx-cursorx-next-cursorReturnsTwitterUser[]
GET/v1/data/user/:id/followersRetrieve followers+

Returns profiles following the requested profile.

Rate limit5 requests / 4 secondsPaginationx-cursorx-next-cursorReturnsTwitterUser[]
GET/v1/data/user/:id/followers/notableRetrieve notable followers+

Returns notable followers based on recently recorded Zyrable data and analytics.

Rate limit10 requests / 2 secondsReturnsTwitterUser[]
GET/v1/data/user/:id/followers/verifiedRetrieve verified followers+

Returns verified profiles following the requested profile.

Rate limit5 requests / 4 secondsPaginationx-cursorx-next-cursorReturnsTwitterUser[]
GET/v1/data/user/:id/followingRetrieve following+

Returns profiles followed by the requested profile.

Rate limit5 requests / 4 secondsPaginationx-cursorx-next-cursorReturnsTwitterUser[]

Communities 4 endpoints

GET/v1/data/community/:idRetrieve a community+

Retrieves a public community in hydrated form.

Rate limit5 requests / 4 secondsReturnsTwitterHydratedCommunity
GET/v1/data/community/:id/tweetsRetrieve community posts+

Returns community posts ranked by top (default) or latest.

Rate limit5 requests / 4 secondsQuerytype?: "top" | "latest"Paginationx-cursorx-next-cursorReturnsTwitterTweet[]
GET/v1/data/community/:id/membersRetrieve community members+

Returns all members or only moderators, including each member's role and account flags.

Rate limit5 requests / 4 secondsQuerytype?: "all" | "moderators"Paginationx-cursorx-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 limit5 requests / 4 secondsQueryquery: stringReturnsTwitterMiniUser[]

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.

TransportWebSocket / WSS
URLwss://api.zyrable.com/v1/events
AuthenticationRequired authorization query parameter
HeartbeatReply to every server PING… with the corresponding PONG…
Client latencyClients may send pings and measure the matching response
Event identityEvery JSON event includes a unique id
DispatchEvery 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

01Keep keys server-side

Never construct authenticated connections in public browser code.

02Redact query strings

Connection URLs can surface in application and proxy logs.

03Protect credentials fields

Never log sensitive values carried by watched.problems.