The Bifrost API
One REST API to connect creators across LinkedIn, YouTube, Instagram, TikTok, Facebook and X — and pull their profiles, posts and analytics through a single, unified schema.
Introduction#
Bifrost is unified creator infrastructure: connect a social account once, then read it through the same endpoints and the same response shape no matter which platform it lives on.
Instead of integrating six OAuth flows and six wildly different data models, you integrate Bifrost once. We handle each platform's OAuth, token refresh, and API quirks, and normalize everything into a consistent set of resources:
- Accounts — a creator's connected account on one platform.
- Profile — username, avatar, follower count, verification.
- Posts — recent content with engagement metrics.
- Analytics — a point-in-time snapshot of account-level metrics.
The API is organized into two surfaces. The Data API (authenticated with a project API key) is what your application calls to connect accounts and read creator data. The Management API under /v1 (authenticated with your login token) is what you use to manage organizations, projects, keys, billing and webhooks — the same operations the dashboard performs.
Quickstart#
From zero to reading a creator's data in three steps.
1 — Create a project API key
Sign in to the dashboard, open API Keys, and create a key. It's shown once and looks like bif_live_…. Keys are scoped to a single project.
2 — Start an OAuth connection
Ask Bifrost for an authorize URL, then redirect the creator to it.
curl -X POST https://usebifrost.org/api/connect \ -H "X-API-Key: bif_live_your_key" \ -H "Content-Type: application/json" \ -d '{"platform":"youtube","redirect_to":"https://yourapp.com/done"}' # → { "authorize_url": "https://accounts.google.com/o/oauth2/...", "state": "..." } # Redirect the creator to authorize_url. Bifrost handles the callback and # sends them back to redirect_to?status=connected&platform=youtube&account_id=...
3 — Read their data
Once connected, use the returned account_id to fetch profile, posts, or analytics.
curl https://usebifrost.org/api/analytics?account_id=ACCOUNT_ID \ -H "X-API-Key: bif_live_your_key" # → { "platform":"youtube", "followers":10450, "likes":..., "views":..., "captured_at":"..." }
Authentication#
Every request is authenticated. Which credential you use depends on the surface.
Data API — project API keys
Calls to /connect, /accounts, /profile, /posts and /analytics use a project API key (prefix bif_). Pass it either way:
X-API-Key: bif_live_your_key # — or — Authorization: Bearer bif_live_your_key
Keys are project-scoped: a key can only ever see accounts and data belonging to its own project. The full secret is shown once at creation — store it securely. A leaked key can be revoked instantly from the dashboard or the Management API.
Management API — login token
Calls under /v1 and /auth/me use the JWT returned by /auth/login or /auth/signup:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsIn...Base URL & versioning#
All requests are made over HTTPS to the base URL above; plain-HTTP requests are redirected to HTTPS. Request and response bodies are JSON (Content-Type: application/json).
Data-API endpoints are unversioned and stable. Management endpoints live under the /v1 prefix. New fields may be added to responses over time — treat unknown fields as forward-compatible and don't break on them.
Rate limits & quotas#
Two limits apply to Data-API calls, both determined by your plan:
- Per-minute rate limit — a fixed request-per-minute ceiling.
- Monthly included calls — total API calls per calendar month.
Every authenticated response carries your remaining budget:
X-RateLimit-Remaining: 118 # requests left in the current minute X-Monthly-Remaining: 99872 # included calls left this month
Exceeding either limit returns 429 Too Many Requests. Slow down and retry after the window resets, or upgrade the plan for higher limits — see Plans & limits.
Errors#
Bifrost uses conventional HTTP status codes. Errors return a JSON body with a human-readable detail:
{ "detail": "Connected-account limit reached for the Free plan. Upgrade to add more." }| Status | Meaning | When it happens |
|---|---|---|
| 200 | OK | Successful GET / POST. |
| 201 | Created | Resource created (key, project, ticket…). |
| 204 | No Content | Successful DELETE (e.g. disconnect). |
| 400 | Bad Request | Malformed input, or a redirect_to origin that isn't allow-listed. |
| 401 | Unauthorized | Missing, invalid, or revoked API key / token. |
| 402 | Payment Required | Plan limit hit (accounts, projects) or a paid-only feature on Free. |
| 403 | Forbidden | Authenticated but lacking the required role. |
| 404 | Not Found | Resource doesn't exist or isn't in your project. |
| 429 | Too Many Requests | Rate limit or monthly quota exceeded. |
| 501 | Not Implemented | Operation not yet supported for that platform. |
| 502 | Bad Gateway | Upstream platform API returned an error. |
Errors from a platform
When a live fetch (refresh=true) fails at the upstream platform, Bifrost passes through the platform's status so you can tell a permission/scope problem from a real outage. The body includes platform and provider_status:
{
"detail": "[linkedin] GET /rest/posts -> 403: ...",
"platform": "linkedin",
"provider_status": 403
}A 403 means the platform hasn't granted the required API/scope for that operation (see Platform capabilities) — not a Bifrost fault. Only genuine upstream failures return 502.
Pagination#
List endpoints accept limit and offset query parameters and return a plain JSON array. Page by advancing offset in multiples of limit.
# second page of 50 accounts curl "https://usebifrost.org/api/accounts?limit=50&offset=50" \ -H "X-API-Key: bif_live_your_key"
| Endpoint | limit default | max | offset |
|---|---|---|---|
GET /accounts | 50 | 200 | ≥ 0 |
GET /posts | 20 | 100 | ≥ 0 |
Cache vs. live
Read endpoints (/profile, /posts, /analytics) return cached data by default and fetch live from the platform when you pass refresh=true. Every read tells you which you got via a response header:
X-Bifrost-Source: live # fetched from the platform just now (refresh=true) X-Bifrost-Source: cache # served from stored data X-Bifrost-Source: empty # nothing cached yet — call ?refresh=true to populate
An empty cache is always a 200 with empty/null fields ([] for posts, null metrics for analytics) and source: empty — never a 404.
Connect an account#
Starts an OAuth flow and returns the platform's authorize URL. Redirect the creator there; after they consent, Bifrost exchanges the code, stores the encrypted tokens, and creates a connected account.
Body parameters
| Field | Type | Description | |
|---|---|---|---|
platform | string | required | One of linkedin, youtube, instagram, tiktok, facebook, x. |
redirect_to | string | optional | Where to send the creator after the callback. Its origin must be registered under your project's Redirect origins (Accounts page in the dashboard). Omit to receive a JSON account object instead of a redirect. |
reference | string | optional | Your own id for this connection (tenant / user). Stored on the account, returned in GET /accounts, echoed on the callback as &reference=, and filterable via ?reference=. Bind it here so you never have to map by account_id after the fact. |
creator_id | uuid | optional | Attach to an existing Bifrost creator (a Bifrost UUID from a prior connect). To bind your own id, use reference instead. |
{
"authorize_url": "https://www.linkedin.com/oauth/v2/authorization?...",
"state": "b1f0…"
}The callback
Bifrost hosts the OAuth redirect target at GET /connect/callback — you never call it directly. When you supplied redirect_to, the creator is 302-redirected back to it with a result:
# success (reference included when you supplied one) https://yourapp.com/done?status=connected&platform=youtube&account_id=8f3c…&reference=tenant-42 # failure https://yourapp.com/done?status=error&platform=youtube&message=...
POST /connect. Connecting the same platform account again updates it in place (refreshes tokens, keeps the same account_id and reference) — it never creates a duplicate. This is how a creator restores an expired account. Don't trust the callback query params alone for critical flows — confirm server-to-server with GET /accounts/{id}.List connected accounts#
Returns the connected accounts in the calling key's project.
Query parameters
| Param | Type | Description | |
|---|---|---|---|
reference | string | optional | Filter to accounts bound to your external id (see reference on connect). |
creator_id | uuid | optional | Filter to a single creator's accounts. |
limit | int | optional | Default 50, max 200. |
offset | int | optional | Default 0. |
[
{
"id": "8f3c1a2e-…",
"creator_id": "3d90…",
"platform": "youtube",
"username": "@creatorhandle",
"profile_image_url": "https://…",
"follower_count": 10450,
"is_verified": true,
"status": "active"
}
]status is one of active, expired (token lapsed and can't refresh — the creator should reconnect), or revoked.
Get one account#
Fetch a single connected account by id — the server-to-server way to confirm a connection result (instead of trusting the callback query params).
200 # the account object (same shape as the list) 404 # no such account in this project
Disconnect an account#
Permanently removes a connected account along with its cached posts and analytics, and frees a slot against your plan's connected-account limit. Fires an account.disconnected webhook.
204 No Content # deleted 404 # no such account in this project
Get profile#
Returns the stored profile snapshot for an account. Pass refresh=true to fetch live from the platform and update the snapshot.
| Param | Type | Description | |
|---|---|---|---|
account_id | uuid | required | The connected account. |
refresh | bool | optional | Re-fetch live from the platform (default false). |
{
"platform": "instagram",
"username": "@creatorhandle",
"profile_image_url": "https://…",
"follower_count": 10450,
"is_verified": false
}List posts#
Returns recent posts for an account, newest first, with normalized engagement metrics.
| Param | Type | Description | |
|---|---|---|---|
account_id | uuid | required | The connected account. |
limit | int | optional | Default 20, max 100. |
offset | int | optional | Default 0. |
refresh | bool | optional | Re-fetch live from the platform. |
[
{
"id": "a1…", "platform": "tiktok",
"external_id": "73920…",
"type": "video",
"caption": "behind the scenes",
"url": "https://…", "media_url": "https://…",
"published_at": "2026-07-01T12:00:00Z",
"likes": 2100, "comments": 88,
"shares": 14, "views": 54200
}
]type is one of post, video, reel, short. Metrics not exposed by a platform are returned as null.
Get analytics#
Returns the most recent account-level metrics snapshot. Pass refresh=true to capture a fresh snapshot from the platform. If none exists yet, call once with refresh=true.
| Param | Type | Description | |
|---|---|---|---|
account_id | uuid | required | The connected account. |
refresh | bool | optional | Capture a fresh snapshot (default false). |
{
"platform": "youtube",
"followers": 10450,
"likes": 50120,
"comments": 3310,
"shares": 420,
"views": 1200500,
"captured_at": "2026-07-10T09:00:00Z"
}Events & delivery#
Webhooks push account lifecycle events to your server as they happen, so you don't have to poll. They're available on Starter and above.
Create an endpoint from the dashboard's Webhooks page or the Management API. Each endpoint gets a signing secret (whsec_…), shown once. Bifrost delivers a JSON POST and retries on failure.
Event types
| Event | Fires when |
|---|---|
account.connected | A creator finishes connecting an account. |
account.disconnected | An account is disconnected / deleted. |
ping | A test delivery you trigger from the dashboard. |
Delivery format
X-Bifrost-Event: account.connected X-Bifrost-Signature: sha256=9f86d0818... { "event": "account.connected", "data": { "account_id": "8f3c…", "platform": "youtube", "username": "@creatorhandle", "platform_user_id": "UC…" } }
Verifying signatures#
Every delivery is signed. Compute an HMAC-SHA256 of the raw request body using your endpoint secret and compare it (constant-time) to the X-Bifrost-Signature header. Reject the request if they don't match.
const crypto = require("crypto"); function verify(rawBody, header, secret) { const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected)); } app.post("/webhooks/bifrost", express.raw({type:"*/*"}), (req, res) => { if (!verify(req.body, req.get("X-Bifrost-Signature"), process.env.WHSEC)) return res.sendStatus(401); const evt = JSON.parse(req.body); // handle evt.event / evt.data … res.sendStatus(200); });
Accounts & auth#
The Management API mirrors the dashboard. Authenticate with the login token (see Authentication). Signing up bootstraps an organization you own.
| Endpoint | Description |
|---|---|
POST /auth/signup | Create an account + owner org. Returns a token, user, and memberships. |
POST /auth/login | Exchange email + password for a token. |
GET /auth/me | Current user and org memberships. |
POST /auth/verify-email | Confirm an email with the token from the verification link. |
POST /auth/forgot-password | Send a reset link (always returns success; never reveals whether an email exists). |
POST /auth/reset-password | Set a new password with a reset token. |
POST /auth/accept-invite | Join an org you were invited to. |
Organizations, projects & keys#
Bifrost is multi-tenant: an organization contains projects, and each project has its own API keys and connected accounts. This is how you isolate environments or customers.
| Endpoint | Description |
|---|---|
POST /v1/orgs | Create an organization. |
GET /v1/orgs | List your organizations. |
POST /v1/orgs/{id}/projects | Create a project. |
GET /v1/orgs/{id}/projects | List projects in an org. |
POST /v1/projects/{id}/keys | Create an API key — the secret is returned once. |
GET /v1/projects/{id}/keys | List a project's keys (prefixes only). |
DELETE /v1/keys/{id} | Revoke a key immediately. |
Team, usage, plan & webhooks#
| Endpoint | Description |
|---|---|
POST /v1/orgs/{id}/invite | Email an invitation to join the org. |
GET /v1/orgs/{id}/members | List members and roles (owner / admin / member). |
GET /v1/projects/{id}/usage | API-call counts by day (?days=7). |
GET /v1/orgs/{id}/plan | Current plan, limits, and live usage. |
PATCH /v1/orgs/{id}/plan | Change plan (owners). |
POST /v1/projects/{id}/webhooks | Register a webhook endpoint. |
POST /v1/webhooks/{id}/test | Send a ping to verify your endpoint. |
DELETE /v1/webhooks/{id} | Delete a webhook endpoint. |
Plans & limits#
Billing is per connected account. Limits are enforced live on the Data API.
| Plan | Price / mo | Accounts | Projects | API calls / mo | Rate (req/min) | Webhooks |
|---|---|---|---|---|---|---|
| Free | $0 | 10 | 2 | 5,000 | 60 | — |
| Starter | $9 | 50 | 5 | 100,000 | 120 | ✓ |
| Growth | $49 | 500 | 20 | 1,000,000 | 300 | ✓ |
| Scale | $99 | 2,000 | 1,000 | 5,000,000 | 600 | ✓ |
| Enterprise | Custom | Custom volume, rate limits, SSO & audit logs, white-label, dedicated support. | ✓ | |||
See live pricing at usebifrost.org/pricing.
Data models#
Field-level reference for the core resources.
Account
| Field | Type | Notes |
|---|---|---|
id | uuid | Bifrost account id — use this to read data. |
creator_id | uuid | The creator this account belongs to. |
reference | string? | Your external id, if you bound one at connect. |
platform | enum | linkedin·youtube·instagram·tiktok·facebook·x |
username | string? | Handle / display name. |
profile_image_url | string? | Avatar URL. |
follower_count | int? | Followers / subscribers. |
is_verified | bool | Platform verification badge. |
status | enum | active·expired·revoked |
Post
| Field | Type | Notes |
|---|---|---|
id | uuid | Bifrost post id. |
external_id | string | Platform's native post id. |
type | enum | post·video·reel·short |
caption | string? | Text / caption. |
url / media_url | string? | Permalink and media. |
published_at | datetime? | ISO-8601, UTC. |
likes·comments·shares·views | int? | null where a platform doesn't expose the metric. |
Analytics snapshot
| Field | Type | Notes |
|---|---|---|
followers·likes·comments·shares·views | int? | Account-level totals at capture time. |
captured_at | datetime? | When the snapshot was taken. |
Supported platforms#
All six platforms share the same endpoints and response shapes, but what each can actually return is bounded by that platform's API and your app's review status with them. This matrix is the source of truth — read it before you rely on a field.
| Platform | platform | Profile | Posts | Analytics | Notes |
|---|---|---|---|---|---|
| YouTube | youtube | ✅ | ✅ | ✅ | Fully live. |
| X (Twitter) | x | ✅ | ⚠️ | ✅ | Profile & analytics live. Reading a user's timeline needs paid X API v2 access — the free tier can't, so live posts return 402 (credits-depleted) until the X app is on a paid tier. |
linkedin | ✅ | ⚠️ | ❌ | Posts need LinkedIn's Community Management API (in review) — until granted, live post fetch returns 403. Member analytics don't exist in LinkedIn's member APIs (only Organization pages expose share stats) → 501. follower_count is always null for members. | |
| TikTok | tiktok | ⚠️ | ✅ | ⚠️ | Follower/like counts and analytics need the user.info.stats scope + approved app (in review). Without it, profile stats are null and analytics returns 403. Posts work with video.list. |
facebook | ⏳ | ⏳ | ⏳ | Pending Meta App Review + Business Verification. Login shows "Feature Unavailable" until approved. | |
instagram | ⏳ | ⏳ | ⏳ | Same shared Meta app — pending the same review. |
✅ live · ⚠️ partial / needs platform grant · ❌ unsupported by the platform's API · ⏳ pending platform review
POST /connect with the platform value, redirect the creator, then read with the returned account_id. Where a capability is gated, the failure is explicit — a provider 403/501 with platform and provider_status (see Errors), not a silent empty.