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
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.
Every JSON API response follows a consistent envelope.
Success
{
"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
{
"success": false,
"message": "User not found"
}
Limits apply per IP address.
| Tier | Limit | Window |
|---|---|---|
| Public API (all endpoints on this page) | 500 requests | 15 minutes |
| Search | 100 requests | 15 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 burst | 30 posts | Rolling 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.
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
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| count | integer | optional | Number of posts to include (1–50, default 20). |
Example Request
GET https://bwire.app/api/users/johndoe/feed.rss?count=10
Example Response
<?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:
https://bwire.app/api/users/YOUR_HANDLE/feed.rss
JavaScript (fetch + parse)
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.
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:
<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
| Attribute | Default | Description |
|---|---|---|
| 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
<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
<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
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)
// 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} />; }
Fetch public profile data and post lists. No API key needed.
Example
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
| Field | Type | Description |
|---|---|---|
| id | integer | Numeric user ID |
| username | string | Display name |
| handle | string | Unique handle (no @) |
| bio | string | null | Profile bio (up to 500 chars) |
| avatar_url | string | null | Avatar image URL |
| banner_url | string | null | Profile banner image URL |
| location | string | null | User-set location |
| website | string | null | User-set website URL |
| is_verified | boolean | Verified badge |
| is_protected | boolean | Whether posts are private |
| follower_count | integer | Number of followers |
| following_count | integer | Number of accounts followed |
| post_count | integer | Total posts |
| created_at | datetime | Account creation timestamp |
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| page | integer | optional | Page number (default 1) |
| limit | integer | optional | Results per page (1–100, default 20) |
| type | string | optional | Filter by post type: post, repost, quote, reply |
Example
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
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| page | integer | optional | Page number (default 1) |
| limit | integer | optional | Results per page (1–100, default 20) |
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| page | integer | optional | Page number (default 1) |
| limit | integer | optional | Results per page (1–100, default 20) |
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| page | integer | optional | Page number (default 1) |
| limit | integer | optional | Results per page (1–100, default 20) |
Fetch individual posts and threads.
Example
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
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| page | integer | optional | Default 1 |
| limit | integer | optional | 1–100, default 20 |
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| q | string | required | Search query (min 1 char) |
| type | string | optional | Filter to post, user, or hashtag |
| page | integer | optional | Default 1 |
| limit | integer | optional | 1–100, default 20 |
Example
const res = await fetch('https://bwire.app/api/search?q=javascript&type=post&limit=5'); const { data } = await res.json(); // data.posts — array of matching posts
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| page | integer | optional | Default 1 |
| limit | integer | optional | 1–100, default 20 |
Example
const { data } = await fetch('https://bwire.app/api/hashtags/javascript').then(r => r.json()); // data.posts — recent posts with #javascript
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.
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| cursor | string | optional | Opaque cursor from previous response for next-page fetch |
| limit | integer | optional | Default 20 |
Example
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; }
Query Parameters
| Param | Type | Description | |
|---|---|---|---|
| cursor | string | optional | Next-page cursor |
| limit | integer | optional | Default 20 |
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
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
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}'
{
"success": true,
"data": {
"key": "f9k_4a7b3c8d...", // save this — shown once only
"prefix": "f9k_4a7b3c8d",
"name": "My Bot",
"dailyPostLimit": 50
}
}
Step 3 — Create a post
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"}'
{
"success": true,
"data": {
"post": {
"id": 1042,
"content": "Hello from the API! #buildinpublic",
"type": "post",
"created_at": "2026-05-16T14:23:00.000Z"
}
}
}
All key management endpoints require Authorization: Bearer <token>. You can hold up to 5 active keys per account.
Request Body
| Field | Type | Description | |
|---|---|---|---|
| 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
| Field | Type | Description |
|---|---|---|
| key | string | Full API key (f9k_...). Returned once — cannot be retrieved again. |
| prefix | string | First 12 characters, used for display in listings. |
| name | string | The label you provided. |
| dailyPostLimit | integer | Effective daily post quota. |
Response Fields (per key)
| Field | Type | Description |
|---|---|---|
| id | integer | Key ID — used when revoking. |
| name | string | Label. |
| key_prefix | string | First 12 characters of the key. |
| daily_post_limit | integer | Daily quota. |
| posts_today | integer | Posts created today (resets midnight UTC). |
| is_active | boolean | Whether the key is usable. |
| created_at | datetime | Creation timestamp. |
| last_used_at | datetime | null | Last time a post was created with this key. |
Example
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)
401 Unauthorized.Example
curl -s -X DELETE https://bwire.app/api/developer/keys/3 \ -H "Authorization: Bearer <ACCESS_TOKEN>"
Example Response
{
"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"
}
]
}
}
Send X-API-Key instead of a Bearer token on POST /api/posts.
Authorization: Bearer <jwt> or X-API-Key: <key>.Headers
| Header | Description | |
|---|---|---|
| X-API-Key | required* | Your API key (f9k_...). Use this or a JWT Bearer token. |
| Content-Type | required | Must be application/json. |
Body Fields
| Field | Type | Description | |
|---|---|---|---|
| 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. |
JavaScript (fetch)
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)
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
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
}'
| Limit | Value | Window | Scope |
|---|---|---|---|
| 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
// 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
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; }