Today we're excited to announce Headless Mode for Facebook connections—a new feature that lets you build completely custom, white-labeled Facebook page selection experiences using the Late API.
If you're building a SaaS product, agency tool, or any application where maintaining your brand identity throughout the user journey is critical, this feature is for you. Your users will never see the "Late" branding during the Facebook connection flow.
The Problem We Solved
Until now, when your users connected their Facebook accounts through Late's API, they would:
- Start OAuth on your app
- Authorize on Facebook
- Get redirected to Late's hosted page selector (with our branding)
- Finally redirect back to your app
For many of our customers, especially agencies and white-label platforms, that third step broke the user experience. Users would see "Late" branding and get confused about where they were in the flow.
With Headless Mode, you now have complete control over that page selection UI.
How Headless Mode Works
The concept is simple: instead of redirecting users to our hosted page selector, we redirect them directly to YOUR domain with all the OAuth data they need. You build the UI, and Late handles all the OAuth complexity behind the scenes.
┌─────────────────┐
│ Your App │ 1. Start OAuth with &headless=true
│ (Backend) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Late API │ 2. Returns Facebook authUrl
└────────┬────────┘
│
▼
┌─────────────────┐
│ Facebook │ 3. User authorizes
└────────┬────────┘
│
│ 4. Redirects to YOUR domain
│ (not Late's domain!)
▼
┌─────────────────┐
│ Your App │ 5. Your custom page selector
│ (Frontend) │ with YOUR branding
└────────┬────────┘
│
│ 6. User selects page
▼
┌─────────────────┐
│ Late API │ 7. Saves connection
└─────────────────┘Getting Started: Add One Parameter
Enabling headless mode is incredibly simple. When you initiate the Facebook OAuth flow, just add &headless=true to your redirect URL:
// Standard mode (redirects to Late's hosted UI)
const response = await fetch(
'https://getlate.dev/api/v1/connect/facebook?' +
'profileId=YOUR_PROFILE_ID&' +
'redirect_url=https://yourapp.com/success',
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
// Headless mode (redirects to YOUR UI)
const response = await fetch(
'https://getlate.dev/api/v1/connect/facebook?' +
'profileId=YOUR_PROFILE_ID&' +
'redirect_url=https://yourapp.com/facebook/callback&' +
'headless=true', // ← Add this!
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
const { authUrl } = await response.json();
// Redirect user to authUrlThat's it! Now when the user completes Facebook authorization, they'll be redirected to YOUR callback URL with all the OAuth data.
Building Your Custom Page Selector
Step 1: Handle the OAuth Callback
After Facebook OAuth, your user lands on your callback URL with these parameters:
https://yourapp.com/facebook/callback?
profileId=507f1f77bcf86cd799439011&
tempToken=EAAxxxx...&
userProfile=%7B%22id%22%3A...&
connect_token=a1b2c3d4...&
platform=facebook&
step=select_pageParse these parameters in your frontend:
function FacebookCallback() {
const [pages, setPages] = React.useState([]);
React.useEffect(() => {
const params = new URLSearchParams(window.location.search);
// Extract OAuth data
const oauthData = {
profileId: params.get('profileId'),
tempToken: params.get('tempToken'),
connectToken: params.get('connect_token'),
userProfile: JSON.parse(
decodeURIComponent(params.get('userProfile'))
)
};
// Store for later use
sessionStorage.setItem('late_oauth_data', JSON.stringify(oauthData));
// Fetch available pages
fetchPages(oauthData);
}, []);
return <YourCustomPageSelector pages={pages} />;
}Step 2: Fetch Available Facebook Pages
Use Late's API to get the list of Facebook pages the user can manage:
async function fetchPages(oauthData) {
const response = await fetch(
`https://getlate.dev/api/v1/connect/facebook/select-page?` +
`profileId=${oauthData.profileId}&` +
`tempToken=${encodeURIComponent(oauthData.tempToken)}`,
{
headers: {
'X-Connect-Token': oauthData.connectToken
}
}
);
const data = await response.json();
return data.pages;
// Returns:
// {
// "pages": [
// {
// "id": "123456789",
// "name": "My Brand Page",
// "username": "mybrand",
// "category": "Brand",
// "access_token": "EAAyyyy..."
// }
// ]
// }
}Important: Use the X-Connect-Token header, not your regular API key. Late automatically generates short-lived connect tokens (valid for 15 minutes) for API users during OAuth flows.Step 3: Display Pages with YOUR Branding
Now comes the fun part—design a page selector that matches your brand:
function YourBrandedPageSelector({ pages }) {
return (
<div className="your-app-container">
{/* Your logo and branding */}
<header className="your-header">
<YourLogo />
<h1>Connect Your Facebook Page</h1>
<p>Choose which page to connect</p>
</header>
{/* Your custom design */}
<div className="your-page-grid">
{pages.map(page => (
<PageCard
key={page.id}
name={page.name}
username={page.username}
category={page.category}
onSelect={() => connectPage(page.id)}
/>
))}
</div>
{/* Your footer */}
<footer className="your-footer">
Need help? Contact support@yourapp.com
</footer>
</div>
);
}Step 4: Complete the Connection
When the user selects a page, send it back to Late's API:
async function connectPage(pageId) {
const oauthData = JSON.parse(
sessionStorage.getItem('late_oauth_data')
);
const response = await fetch(
'https://getlate.dev/api/v1/connect/facebook/select-page',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Connect-Token': oauthData.connectToken
},
body: JSON.stringify({
profileId: oauthData.profileId,
pageId: pageId,
tempToken: oauthData.tempToken,
userProfile: oauthData.userProfile,
redirect_url: 'https://yourapp.com/success'
})
}
);
const result = await response.json();
// Connection saved! Redirect to success page
window.location.href = result.redirect_url;
// → https://yourapp.com/success?connected=facebook&profileId=...
}Real Implementation Example
Here's a complete React component you can use as a starting point:
import { useState, useEffect } from 'react';
export default function FacebookPageSelector() {
const [pages, setPages] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [oauthData, setOauthData] = useState(null);
useEffect(() => {
// Parse OAuth callback
const params = new URLSearchParams(window.location.search);
const data = {
profileId: params.get('profileId'),
tempToken: params.get('tempToken'),
connectToken: params.get('connect_token'),
userProfile: JSON.parse(decodeURIComponent(params.get('userProfile')))
};
setOauthData(data);
// Fetch pages from Late API
fetch(
`https://getlate.dev/api/v1/connect/facebook/select-page?` +
`profileId=${data.profileId}&tempToken=${encodeURIComponent(data.tempToken)}`,
{ headers: { 'X-Connect-Token': data.connectToken } }
)
.then(r => r.json())
.then(result => {
setPages(result.pages);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
async function handleSelectPage(pageId) {
try {
const response = await fetch(
'https://getlate.dev/api/v1/connect/facebook/select-page',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Connect-Token': oauthData.connectToken
},
body: JSON.stringify({
profileId: oauthData.profileId,
pageId,
tempToken: oauthData.tempToken,
userProfile: oauthData.userProfile
})
}
);
const result = await response.json();
window.location.href = '/integrations/success?platform=facebook';
} catch (err) {
setError(err.message);
}
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full" />
</div>
);
}
if (error) {
return (
<div className="max-w-md mx-auto mt-12 p-6 bg-red-50 border border-red-200 rounded-lg">
<h3 className="text-red-800 font-bold mb-2">Connection Error</h3>
<p className="text-red-600">{error}</p>
</div>
);
}
return (
<div className="max-w-4xl mx-auto px-4 py-12">
<header className="text-center mb-12">
{/* Your logo here */}
<h1 className="text-3xl font-bold mb-2">Connect Facebook Page</h1>
<p className="text-gray-600">Choose which page to use for posting</p>
</header>
<div className="grid md:grid-cols-2 gap-4">
{pages.map(page => (
<button
key={page.id}
onClick={() => handleSelectPage(page.id)}
className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 hover:shadow-lg transition-all text-left"
>
<h3 className="font-bold text-lg">{page.name}</h3>
{page.username && (
<p className="text-blue-600 text-sm mt-1">@{page.username}</p>
)}
{page.category && (
<p className="text-gray-500 text-sm mt-2">{page.category}</p>
)}
</button>
))}
</div>
</div>
);
}Important: Security and Tokens
Late uses short-lived connect tokens specifically for headless OAuth flows:
- Automatically generated when you initiate OAuth via API key (not browser session)
- Valid for 15 minutes - enough time for users to select a page
- Single-use - don't try to reuse them
- Passed via X-Connect-Token header - not your regular API key
This design ensures that even if a connect token is intercepted, it expires quickly and can't be used to access your account.
Error Handling
Make sure to handle these common scenarios:
async function fetchPagesWithErrorHandling(oauthData) {
try {
const response = await fetch(
`https://getlate.dev/api/v1/connect/facebook/select-page?` +
`profileId=${oauthData.profileId}&tempToken=${encodeURIComponent(oauthData.tempToken)}`,
{ headers: { 'X-Connect-Token': oauthData.connectToken } }
);
if (!response.ok) {
const error = await response.json();
// Handle specific errors
if (error.error?.includes('Invalid access token')) {
throw new Error(
'Facebook token expired. Please try connecting again.'
);
}
if (error.error?.includes('no pages')) {
throw new Error(
'No Facebook pages found. You need to be an admin of at least one page.'
);
}
throw new Error(error.error || 'Failed to fetch pages');
}
const data = await response.json();
if (!data.pages || data.pages.length === 0) {
throw new Error(
'No pages available. Please create a Facebook page or get admin access to one.'
);
}
return data.pages;
} catch (err) {
console.error('Error fetching Facebook pages:', err);
throw err;
}
}Testing Your Implementation
Before going live, test these scenarios:
- Happy path: User with multiple pages selects one successfully
- No pages: User doesn't manage any Facebook pages
- OAuth cancellation: User clicks "Cancel" on Facebook
- Token expiry: User waits > 15 minutes before selecting
- Network errors: API calls fail temporarily
When to Use Headless Mode
Headless mode is perfect for:
- White-label products: Your customers never see "Powered by Late"
- Agency tools: Maintain your agency's brand throughout
- Custom workflows: Add validation or business logic before page selection
- Branded experiences: Match your app's exact design language
You might not need headless mode if:
- You're okay with users seeing "Late" branding briefly
- You want to minimize frontend development work
- You're building an internal tool (not customer-facing)
What's Next?
We're bringing headless mode to more platforms:
- ✅ Facebook - Available now!
- 🚧 LinkedIn - Organization selection (coming soon)
- 🚧 Pinterest - Board selection (coming soon)
- 🚧 YouTube - Channel selection for brand accounts (coming soon)
Resources
Ready to implement headless mode? Check out these resources:
- API Documentation - Complete API reference
- OpenAPI Specification - Machine-readable API spec
- Get Help - Email us if you need assistance
Pricing
Headless mode is included in all Late plans at no extra cost. Whether you're on our Free plan or Enterprise, you can use this feature.
Wrapping Up
Headless OAuth mode gives you the best of both worlds: Late's robust OAuth infrastructure combined with complete control over your user experience.
To get started:
- Add
&headless=trueto your OAuth initiation - Build your custom page selector UI
- Call our selection endpoint to complete the connection
Questions? Email us at miki@getlate.dev - we'd love to hear how you're using headless mode!
“We built headless mode because our customers told us they needed it. If you have feedback or feature requests, please reach out—we're always listening.”
— Miki, Founder of Late