BWire Developer Platform

Embed feeds, fetch public data, and integrate BWire into your own projects.

BWire exposes a set of public read APIs that require no authentication. You can use them to display a user's posts on your website, power an RSS reader integration, or build dashboards. Write access (posting from scripts, bots, or third-party tools) is available via API keys — see the Developer API section below.

Base URL

HTTP
https://bwire.app

All API endpoints are prefixed with /api. The RSS endpoint and widget embed page live at /api/users/:handle/feed.rss and /widget/embed/:handle respectively.

Response Format

Every JSON API response follows a consistent envelope.

Success

JSON
{
  "success": true,
  "data": { /* payload */ },
  "meta": {
    "total":      248,
    "page":       1,
    "limit":      20,
    "totalPages": 13,
    "hasNext":    true,
    "hasPrev":    false
  }
}

Paginated endpoints include a meta block. Single-resource endpoints omit it.

Error

JSON
{
  "success": false,
  "message": "User not found"
}
200 OK
400 Bad request / validation
404 Not found
429 Rate limit exceeded
Rate Limits

Limits apply per IP address.

TierLimitWindow
Public API (all endpoints on this page)500 requests15 minutes
Search100 requests15 minutes
Developer API — post creation (per key)100 posts (default, configurable up to 1 000)Per calendar day, resets midnight UTC
Developer API — per-minute burst30 postsRolling 60 seconds

When the limit is exceeded the server returns 429 Too Many Requests. Back off and retry after the window resets. The Developer API daily quota resets automatically at midnight UTC regardless of when the key was created.


RSS Feeds

Every public BWire profile has a machine-readable RSS 2.0 feed — no API key required.

Use it to syndicate posts to your blog, newsletter, or any tool that can consume RSS (WordPress, Ghost, Feedly, Zapier, Make, n8n, etc.).

ℹ️

Protected accounts return 403 Forbidden. The feed is empty while the account has no posts.

Endpoint

GET /api/users/:handle/feed.rss
Returns an RSS 2.0 XML document for the specified user's public posts.

Query Parameters

ParamTypeDescription
count integer optional Number of posts to include (1–50, default 20).

Example Request

HTTP
GET https://bwire.app/api/users/johndoe/feed.rss?count=10

Example Response

XML
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>John Doe (@johndoe) on BWire</title>
    <link>https://bwire.app/@johndoe</link>
    <description>Building cool stuff in public.</description>
    <language>en</language>
    <ttl>10</ttl>
    <item>
      <title>Just shipped the new notifications panel...</title>
      <link>https://bwire.app/@johndoe/post/42</link>
      <description>Just shipped the new notifications panel — took 3 days but worth it.</description>
      <pubDate>Thu, 15 May 2026 09:00:00 GMT</pubDate>
      <guid isPermaLink="true">https://bwire.app/@johndoe/post/42</guid>
    </item>
  </channel>
</rss>

CMS & Tool Usage

WordPress (RSS block)

In any WordPress page or post, add an RSS block and paste your feed URL:

URL
https://bwire.app/api/users/YOUR_HANDLE/feed.rss

JavaScript (fetch + parse)

JavaScript
const url  = 'https://bwire.app/api/users/johndoe/feed.rss';
const res  = await fetch(url);
const text = await res.text();
const doc  = new DOMParser().parseFromString(text, 'application/xml');
const items = [...doc.querySelectorAll('item')].map(el => ({
  title:   el.querySelector('title')?.textContent,
  link:    el.querySelector('link')?.textContent,
  pubDate: el.querySelector('pubDate')?.textContent,
  content: el.querySelector('description')?.textContent,
}));
console.log(items);

Zapier / Make / n8n

Use the RSS trigger in any automation platform and paste the feed URL. New posts will trigger your workflow automatically.


Embeddable Widget

Render a live feed of any public profile on your website with a single <script> tag.

The widget renders inside a sandboxed <iframe>, so it never conflicts with your site's styles. It auto-resizes to fit its content, supports dark/light/auto themes, and links directly back to BWire.

Quick Start

Paste this where you want the feed to appear:

HTML
<script
  src="https://bwire.app/widget/feed.js"
  data-user="johndoe"
  data-theme="auto"
  data-count="5"
></script>

The script tag is replaced in-place by the iframe — no wrapper <div> needed.

Attributes

AttributeDefaultDescription
data-user Required. BWire handle (without @) whose feed to display.
data-theme "auto" auto follows the visitor's OS preference, dark or light force a specific theme.
data-count "5" Number of posts to show (1–20).
data-width "100%" Width of the embed — a plain number like 400 is treated as pixels; any CSS value (e.g. 100%) is used as-is. Height is automatic.

Examples

Fixed width, dark theme, 3 posts

HTML
<script
  src="https://bwire.app/widget/feed.js"
  data-user="johndoe"
  data-theme="dark"
  data-count="3"
  data-width="380"
></script>

Full-width sidebar embed

HTML
<aside style="width:320px">
  <script
    src="https://bwire.app/widget/feed.js"
    data-user="johndoe"
    data-theme="auto"
    data-count="8"
  ></script>
</aside>

Framework Usage

React

JSX
import { useEffect } from 'react';

export function BwireFeed({ user, theme = 'auto', count = 5, width = '100%' }) {
  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://bwire.app/widget/feed.js';
    script.dataset.user  = user;
    script.dataset.theme = theme;
    script.dataset.count = String(count);
    script.dataset.width = String(width);
    document.getElementById('bwire-mount').appendChild(script);
    return () => document.getElementById('bwire-mount')?.replaceChildren();
  }, [user, theme, count, width]);

  return <div id="bwire-mount" />;
}

Next.js (App Router)

TSX
// components/BwireFeed.tsx
'use client';
import { useEffect, useRef } from 'react';

export default function BwireFeed({
  user, theme = 'auto', count = 5,
}: { user: string; theme?: string; count?: number }) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!ref.current) return;
    ref.current.innerHTML = '';
    const s = document.createElement('script');
    s.src = 'https://bwire.app/widget/feed.js';
    s.dataset.user = user; s.dataset.theme = theme; s.dataset.count = String(count);
    ref.current.appendChild(s);
  }, [user, theme, count]);

  return <div ref={ref} />;
}

REST API — Users

Fetch public profile data and post lists. No API key needed.

GET /api/users/:handle
Returns a user's public profile.

Example

JavaScript
const res  = await fetch('https://bwire.app/api/users/johndoe');
const { data } = await res.json();
// data.username, data.handle, data.bio, data.avatar_url
// data.follower_count, data.following_count, data.post_count

Response Fields

FieldTypeDescription
idintegerNumeric user ID
usernamestringDisplay name
handlestringUnique handle (no @)
biostring | nullProfile bio (up to 500 chars)
avatar_urlstring | nullAvatar image URL
banner_urlstring | nullProfile banner image URL
locationstring | nullUser-set location
websitestring | nullUser-set website URL
is_verifiedbooleanVerified badge
is_protectedbooleanWhether posts are private
follower_countintegerNumber of followers
following_countintegerNumber of accounts followed
post_countintegerTotal posts
created_atdatetimeAccount creation timestamp
GET /api/users/:handle/posts
Returns a paginated list of posts by the user.

Query Parameters

ParamTypeDescription
pageintegeroptionalPage number (default 1)
limitintegeroptionalResults per page (1–100, default 20)
typestringoptionalFilter by post type: post, repost, quote, reply

Example

JavaScript
const res = await fetch(
  'https://bwire.app/api/users/johndoe/posts?page=1&limit=10&type=post'
);
const { data, meta } = await res.json();
// data.posts — array of post objects
// meta.total, meta.hasNext, meta.page
GET /api/users/:handle/followers
Paginated list of the user's followers.

Query Parameters

ParamTypeDescription
pageintegeroptionalPage number (default 1)
limitintegeroptionalResults per page (1–100, default 20)
GET /api/users/:handle/following
Paginated list of accounts the user follows.

Query Parameters

ParamTypeDescription
pageintegeroptionalPage number (default 1)
limitintegeroptionalResults per page (1–100, default 20)
GET /api/users/:handle/likes
Posts the user has liked, paginated.

Query Parameters

ParamTypeDescription
pageintegeroptionalPage number (default 1)
limitintegeroptionalResults per page (1–100, default 20)
REST API — Posts

Fetch individual posts and threads.

GET /api/posts/:id
Returns a single post by numeric ID.

Example

JavaScript
const { data } = await fetch('https://bwire.app/api/posts/42').then(r => r.json());
// data.id, data.content, data.type, data.like_count, data.created_at
GET /api/posts/:id/thread
Returns the full thread — ancestor posts above and direct replies below.
GET /api/posts/:id/replies
Paginated direct replies to a post.

Query Parameters

ParamTypeDescription
pageintegeroptionalDefault 1
limitintegeroptional1–100, default 20
REST API — Hashtags
GET /api/hashtags/:tag
Posts containing the given hashtag (without #), paginated.

Query Parameters

ParamTypeDescription
pageintegeroptionalDefault 1
limitintegeroptional1–100, default 20

Example

JavaScript
const { data } = await fetch('https://bwire.app/api/hashtags/javascript').then(r => r.json());
// data.posts — recent posts with #javascript
GET /api/hashtags/trending
Returns the top 20 trending hashtags with post counts.
REST API — Timeline

Public explore and hashtag timelines use cursor-based pagination for infinite scroll.

ℹ️

These endpoints use cursor-based pagination. Pass the cursor value from the previous response to fetch the next page.

GET /api/timeline/explore
A public stream of recent posts from across the platform.

Query Parameters

ParamTypeDescription
cursorstringoptionalOpaque cursor from previous response for next-page fetch
limitintegeroptionalDefault 20

Example

JavaScript
let cursor = null;

async function loadMore() {
  const url = 'https://bwire.app/api/timeline/explore'
    + (cursor ? `?cursor=${cursor}` : '');
  const { data } = await fetch(url).then(r => r.json());
  cursor = data.nextCursor; // null when no more results
  return data.posts;
}
GET /api/timeline/hashtag/:tag
Cursor-paginated timeline for a specific hashtag.

Query Parameters

ParamTypeDescription
cursorstringoptionalNext-page cursor
limitintegeroptionalDefault 20

Developer API

Create posts programmatically with long-lived API keys — no password exposure, no session management.

Each API key is scoped to your account, carries its own daily post quota, and can be revoked at any time without affecting your password or other keys. Keys begin with f9k_ and are valid indefinitely until revoked.

⚠️

The full key is shown once at creation time. Store it in a secret manager or environment variable immediately — it cannot be retrieved again.

ℹ️

Key management (create / list / revoke) requires a short-lived JWT Bearer token from POST /api/auth/login. Only using a key to post requires the API key itself.

Quick Start

Step 1 — Get a Bearer token

Shell
curl -s -X POST https://bwire.app/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"your-password"}' \
  | jq -r '.data.accessToken'

Step 2 — Create an API key

Shell
curl -s -X POST https://bwire.app/api/developer/keys \
  -H "Authorization: Bearer <ACCESS_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"name":"My Bot","daily_post_limit":50}'
JSON
{
  "success": true,
  "data": {
    "key":            "f9k_4a7b3c8d...",  // save this — shown once only
    "prefix":         "f9k_4a7b3c8d",
    "name":           "My Bot",
    "dailyPostLimit": 50
  }
}

Step 3 — Create a post

Shell
curl -s -X POST https://bwire.app/api/posts \
  -H "X-API-Key: f9k_4a7b3c8d..." \
  -H "Content-Type: application/json" \
  -d '{"content":"Hello from the API! #buildinpublic"}'
JSON
{
  "success": true,
  "data": {
    "post": {
      "id":         1042,
      "content":    "Hello from the API! #buildinpublic",
      "type":       "post",
      "created_at": "2026-05-16T14:23:00.000Z"
    }
  }
}
Managing Keys

All key management endpoints require Authorization: Bearer <token>. You can hold up to 5 active keys per account.

POST /api/developer/keys
Create a new API key. The full key value is returned exactly once — store it immediately.

Request Body

FieldTypeDescription
name string required Human-readable label (max 100 chars).
daily_post_limit integer optional Max posts per calendar day, 1–1 000 (default 100).

Response Fields — 201 Created

FieldTypeDescription
keystringFull API key (f9k_...). Returned once — cannot be retrieved again.
prefixstringFirst 12 characters, used for display in listings.
namestringThe label you provided.
dailyPostLimitintegerEffective daily post quota.
201 Created
401 Missing or invalid JWT
422 Validation error or key cap reached
GET /api/developer/keys
List all API keys on your account. Full key values are never returned — only the prefix.

Response Fields (per key)

FieldTypeDescription
idintegerKey ID — used when revoking.
namestringLabel.
key_prefixstringFirst 12 characters of the key.
daily_post_limitintegerDaily quota.
posts_todayintegerPosts created today (resets midnight UTC).
is_activebooleanWhether the key is usable.
created_atdatetimeCreation timestamp.
last_used_atdatetime | nullLast time a post was created with this key.

Example

JavaScript
const res  = await fetch('https://bwire.app/api/developer/keys', {
  headers: { 'Authorization': `Bearer ${accessToken}` },
});
const { data } = await res.json();
// data.keys — array of key objects (no full key values)
DELETE /api/developer/keys/:id
Revoke an API key permanently. Any in-flight request using the key will immediately receive 401 Unauthorized.

Example

Shell
curl -s -X DELETE https://bwire.app/api/developer/keys/3 \
  -H "Authorization: Bearer <ACCESS_TOKEN>"
200 Revoked
404 Key not found or belongs to another account
GET /api/developer/usage
Real-time daily usage for all active keys, including posts remaining today.

Example Response

JSON
{
  "success": true,
  "data": {
    "usage": [
      {
        "id":                  1,
        "name":                "My Bot",
        "key_prefix":          "f9k_4a7b3c8d",
        "daily_post_limit":    50,
        "posts_today":         12,
        "postsRemainingToday": 38,
        "last_used_at":        "2026-05-16T11:00:00.000Z"
      }
    ]
  }
}
Creating Posts

Send X-API-Key instead of a Bearer token on POST /api/posts.

POST /api/posts
Create a post. Accepts either Authorization: Bearer <jwt> or X-API-Key: <key>.

Headers

HeaderDescription
X-API-Key required* Your API key (f9k_...). Use this or a JWT Bearer token.
Content-Type required Must be application/json.

Body Fields

FieldTypeDescription
content string required Post text, up to 280 characters. Markdown, #hashtags, and @mentions are supported.
type string optional post (default), reply, or quote.
parentId integer optional ID of the post being replied to. Required when type is reply.
quoteOfId integer optional ID of the post being quoted. Required when type is quote.
replySettings string optional Who can reply: everyone (default), following, or mentioned.
201 Post created
401 Invalid or missing API key
429 Daily or per-minute limit reached
422 Validation error (e.g. content too long)

JavaScript (fetch)

JavaScript
const res = await fetch('https://bwire.app/api/posts', {
  method:  'POST',
  headers: {
    'X-API-Key':    'f9k_4a7b3c8d...',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    content: 'Shipped v2.0 today! #buildinpublic',
  }),
});

if (!res.ok) {
  const err = await res.json();
  throw new Error(err.message); // e.g. "Daily post limit of 50 reached."
}

const { data } = await res.json();
console.log(`Created post ID: ${data.post.id}`);

Python (requests)

Python
import requests

res = requests.post(
    "https://bwire.app/api/posts",
    headers={
        "X-API-Key":    "f9k_4a7b3c8d...",
        "Content-Type": "application/json",
    },
    json={"content": "Shipped v2.0 today! #buildinpublic"},
)
res.raise_for_status()  # raises on 4xx / 5xx
print(res.json()["data"]["post"]["id"])

Posting a reply

Shell
curl -s -X POST https://bwire.app/api/posts \
  -H "X-API-Key: f9k_4a7b3c8d..." \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Thanks for the feedback!",
    "type":    "reply",
    "parentId": 1042
  }'
Limits & Quotas
LimitValueWindowScope
Post creation (per key) 100 posts (default, configurable up to 1 000) Per calendar day — resets midnight UTC API key
Post creation (burst) 30 posts Rolling 60-second window User account
Active keys per account 5 Account
Key name length 100 characters Per key
ℹ️

The daily counter is per-key, not per-account. If you need higher throughput you can distribute posts across multiple keys (up to 5). The per-minute burst limit is shared across all keys on the same account.

Error Responses

JSON
// 429 — daily quota reached
{
  "success": false,
  "message": "Daily post limit of 50 reached. Resets at midnight UTC."
}

// 401 — key is invalid or has been revoked
{
  "success": false,
  "message": "Invalid API key"
}

// 403 — the account this key belongs to has been suspended
{
  "success": false,
  "message": "Account suspended"
}

// 422 — tried to create a 6th key
{
  "success": false,
  "message": "Maximum of 5 active API keys allowed"
}

Handling Rate Limits in Code

JavaScript
async function safePost(content) {
  const res = await fetch('https://bwire.app/api/posts', {
    method:  'POST',
    headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
    body:    JSON.stringify({ content }),
  });

  if (res.status === 429) {
    const body = await res.json();
    throw new Error(`Rate limited: ${body.message}`);
  }

  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()).data.post;
}