SDK Initialization
Connect your add-on to SyncBooks by wrapping it with the SDK provider. This handles authentication, context, and API setup automatically.
The SyncBooksProvider
Every SyncBooks add-on must be wrapped with SyncBooksProvider. This component:
- Establishes the postMessage bridge with the SyncBooks host
- Receives the session context (organization, user, token, scopes)
- Configures the API client with the session token
- Makes all hooks and API calls work automatically
import React from 'react'
import ReactDOM from 'react-dom/client'
import { SyncBooksProvider } from '@syncbooks/addon-sdk'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<SyncBooksProvider>
<App />
</SyncBooksProvider>
</React.StrictMode>
)Don't skip the provider
Without SyncBooksProvider, none of the SDK hooks work. Your add-on will show "Loading..." forever and API calls will fail with "No session token."
Using the Context Hook
Access the SyncBooks context from any component using useSyncBooks():
import { useSyncBooks } from '@syncbooks/addon-sdk'
export default function App() {
const {
ready, // boolean — true when bridge is connected
organizationId, // string — the org that installed your addon
userId, // string — the user currently viewing
userRole, // string — "admin", "accountant", "member", etc.
appId, // string — your addon's unique ID
scopes, // string[] — permissions granted to you
token, // string — session JWT (used by API client automatically)
resource, // { type, id } | null — if opened from a specific record
} = useSyncBooks()
// ALWAYS check ready before rendering content
if (!ready) {
return (
<div className="flex items-center justify-center min-h-screen">
<p>Connecting to SyncBooks...</p>
</div>
)
}
return (
<div className="p-6">
<h1>Connected!</h1>
<p>Organization: {organizationId}</p>
<p>User: {userId} ({userRole})</p>
<p>Scopes: {scopes.join(', ')}</p>
</div>
)
}Context Properties
| Property | Type | Description |
|---|---|---|
readyrequired | boolean | True when the bridge is connected and context is received. Always check this before rendering. |
organizationId | string | null | The ID of the organization that installed your addon. |
userId | string | null | The ID of the user currently viewing your addon. |
userRole | string | null | The user's role: admin, accountant, sales, hr, cashier, member, etc. |
appId | string | null | Your addon's unique app ID (com.publisher.appname format). |
scopes | string[] | Array of permission scopes granted to your addon by this org. |
token | string | null | The session JWT. You don't need to use this directly — the API client uses it automatically. |
resource | { type: string, id: string } | null | If the user opened your addon from a specific record (e.g., clicked your button on an invoice), this tells you which record. |
Resource Context
When your add-on is opened from a specific record (like an invoice action button), the resource object tells you which record triggered it:
import { useSyncBooks, invoices } from '@syncbooks/addon-sdk'
function App() {
const { ready, resource } = useSyncBooks()
if (!ready) return <p>Loading...</p>
// If opened from an invoice action button
if (resource?.type === 'invoice') {
return <InvoiceDetail invoiceId={resource.id} />
}
// If opened from sidebar (no specific resource)
return <Dashboard />
}
function InvoiceDetail({ invoiceId }: { invoiceId: string }) {
const [invoice, setInvoice] = useState(null)
useEffect(() => {
invoices.get(invoiceId).then(res => setInvoice(res.data))
}, [invoiceId])
if (!invoice) return <p>Loading invoice...</p>
return <div>Invoice: {invoice.invoiceNumber}</div>
}Scope Checking
Always verify you have the required scopes before making API calls or showing write UI:
import { useSyncBooks, hasScope } from '@syncbooks/addon-sdk'
function App() {
const { ready, scopes } = useSyncBooks()
if (!ready) return null
// Check single scope
const canReadInvoices = scopes.includes('invoices.read') ||
scopes.includes('invoices.*') ||
scopes.includes('*')
// Or use the helper function
const canWrite = hasScope('customers.write')
const canDelete = hasScope('customers.delete')
return (
<div>
{canReadInvoices && <InvoiceList />}
{canWrite && <button>Create Customer</button>}
{canDelete && <button>Delete</button>}
</div>
)
}Use the PermissionGate component
Instead of manual scope checks, use the SDK's PermissionGate component for cleaner code. See UI Components.
Local Development
During development, your add-on runs on localhost but can't receive a real session from SyncBooks. Configure the SDK with test credentials:
import { SyncBooksProvider, configureClient } from '@syncbooks/addon-sdk'
// For local development only — remove before deploying!
if (import.meta.env.DEV) {
configureClient({
baseUrl: 'http://localhost:5000/api/addon/v1', // Your local backend
token: 'paste-your-test-token-here', // Get from browser DevTools
})
}
// Provider still needed for hooks to work
ReactDOM.createRoot(document.getElementById('root')!).render(
<SyncBooksProvider>
<App />
</SyncBooksProvider>
)How to get a test token:
- Register your add-on in the marketplace (draft mode)
- Install it on your org
- Open browser DevTools → Network tab
- Click your add-on in SyncBooks
- Find the iframe request — the URL contains
syncbooks_token=eyJ... - Copy that token value
Test tokens expire
Tokens expire after 1 hour. You'll need to get a new one each development session. For extended testing, see the Testing guide.
Provider Props
| Property | Type | Description |
|---|---|---|
childrenrequired | ReactNode | Your addon's component tree |
apiBaseUrl | string | Override the API base URL (for development) |
token | string | Manually provide a session token (for development/testing) |
Next Steps
- Fetching Data — read and write data from SyncBooks
- UI Components — build native-looking interfaces