@foxscheduling/sdk
v0.2.7
Published
Official Fox Scheduling Partner API SDK
Readme
@foxscheduling/sdk
Official Node.js SDK for the Fox Scheduling Partner API.
Use this package to read and manage bookings, customers, availability, and more on behalf of a Fox Scheduling business — from your own server or integration.
Requirements: Node.js 18+
Full API reference: foxscheduling.com/developers
Install
npm install @foxscheduling/sdk @foxscheduling/sharedBoth packages are required. @foxscheduling/shared provides shared types and scope names.
Choose how to authenticate
| Method | Best for | Dashboard setup | | --- | --- | --- | | API key | Scripts, cron jobs, or automation for your own Fox business | Developers → API keys | | OAuth | SaaS apps where other people connect their Fox business to your product | Developers → OAuth apps |
If you only need to access a business you own, start with an API key. It is the fastest path.
Quick start: API key
1. Create a key
- Sign in to Fox Scheduling.
- Open Developers → API keys.
- Pick the business, choose scopes (e.g.
business:read,bookings:read), and create a key. - Copy the key immediately — it is shown only once. Keys look like
fx_live_....
2. Call the API
import { FoxScheduling } from "@foxscheduling/sdk";
const fox = new FoxScheduling({
auth: {
type: "apiKey",
apiKey: process.env.FOX_API_KEY!, // fx_live_...
},
});
// Example: read the connected business profile
const business = await fox.business.get();
console.log(business.name);Available resources
After creating a client, use:
fox.business— business profilefox.services— servicesfox.staff— staff membersfox.availability— availability rulesfox.bookings— bookingsfox.customers— customersfox.webhooks— webhook subscriptions
All methods are fully typed (no unknown inputs or outputs) and throw a
FoxSchedulingError on API errors.
Lists are paginated (max 100 per page)
Pagination is enforced by the API — every list() method sends limit/offset
to the server and returns a typed Page<T>, never a raw array:
interface Page<T> {
items: T[]; // at most 100
total: number; // total across all pages
limit: number; // effective page size
offset: number; // current offset
hasMore: boolean; // true if more items exist
}Pass limit (1–100, default 100) and offset to page through results:
const first = await fox.customers.list({ limit: 50, offset: 0 });
console.log(first.items.length, first.total, first.hasMore);
if (first.hasMore) {
const next = await fox.customers.list({ limit: 50, offset: 50 });
}limit is clamped to a maximum of 100.
Bookings require a month
fox.bookings.list() requires a month (format YYYY-MM) so responses stay
small. Other filters are optional and typed:
const page = await fox.bookings.list({
month: "2026-06", // required
status: "booked", // optional, typed union
serviceId: "…", // optional
limit: 100, // optional
});Omitting or malformatting month throws a FoxSchedulingError before any
request is sent.
Typed create & update
Create and update bodies are typed — for example:
// Create a customer
const customer = await fox.customers.create({
name: "Jane Doe",
email: "[email protected]",
});
// Create a booking
const result = await fox.bookings.create({
serviceId: "…",
name: "Jane Doe",
email: "[email protected]",
slots: [{ date: "2026-06-12", start: "09:00", end: "09:30" }],
});
console.log(result.bookingSummary, result.customerId);Get booking embed code
fox.business.getEmbedCode() returns ready-to-paste embed snippets for the
connected business: an inline iframe, a popup <script> widget, and the raw
bookingUrl. All options are optional and typed:
const embed = await fox.business.getEmbedCode({
theme: "dark", // "light" | "dark" | "auto" (default)
servicePath: "haircut", // optional: link straight to one service
hidePageDetails: false, // hide business header inside the embed
heightPx: 760, // inline iframe height
iframeBorderRadius: "medium", // none | sm | medium | lg | xl | 2xl
openMode: "popup", // "popup" | "newTab" for the script widget
buttonText: "Book now", // popup button label
});
console.log(embed.bookingUrl); // shareable URL
console.log(embed.iframe); // <style>…</style><div>…<iframe …></div>
console.log(embed.popupScript); // <script … data-booking-url="…"></script>
console.log(embed.appliedOptions); // options after defaults appliedBy default the popup widget renders its own button. To open the popup from a
button that already exists on your page, set buttonMode: "existing" and pass
the element's id (without #) as triggerId:
// For an element like <button id="book-appointment">Book</button>
const embed = await fox.business.getEmbedCode({
buttonMode: "existing",
triggerId: "book-appointment",
});
// embed.popupScript now wires the popup to your element instead of injecting a button.
// embed.appliedOptions.buttonMode === "existing"
// embed.appliedOptions.triggerId === "book-appointment"An invalid or missing triggerId while buttonMode is "existing" is rejected
by the API.
OAuth: connect other users' businesses
Use OAuth when you are building an integration (e.g. a CRM or reporting tool) and Fox Scheduling users need to authorize your app to access their business.
OAuth is a browser-based flow. At a high level:
Your app Fox Scheduling User's browser
| | |
|-- 1. Build authorize URL --->| |
|-- 2. Redirect user -------------------------------------->|
| |<-- 3. User signs in & allows
|<-- 4. Redirect to your URL with ?code=... ---------------|
|-- 5. Exchange code for tokens ->| |
|-- 6. Call Partner API with stored tokens |You never type the authorization code yourself. Fox appends it to your redirect URL after the user clicks Allow.
Before you write code
- Create an OAuth app under Developers → OAuth apps.
- Note the client ID and client secret (secret is shown once).
- Add a redirect URI — the exact URL Fox will send users back to, e.g.
https://your-app.com/oauth/callback. It must match character-for-character in your code.
Step 1 — Send the user to Fox
Create the SDK client and build the authorization URL:
import { FoxScheduling, FileTokenStore } from "@foxscheduling/sdk";
const fox = new FoxScheduling({
auth: {
type: "oauth",
clientId: process.env.FOX_CLIENT_ID!,
clientSecret: process.env.FOX_CLIENT_SECRET!,
redirectUri: "https://your-app.com/oauth/callback", // must match dashboard
tokenStore: new FileTokenStore("./fox-tokens.json"),
},
});
const { url, codeVerifier, state } = fox.oauth!.getAuthorizationUrl({
scopes: ["business:read", "bookings:read"],
});Important: Before redirecting the user, save codeVerifier and state somewhere tied to that user session (memory, Redis, database, encrypted cookie, etc.). You need them on the next step.
Then redirect the user's browser to url (e.g. res.redirect(url) in Express).
Step 2 — Handle the callback
When the user approves, Fox redirects to your redirectUri with query parameters:
https://your-app.com/oauth/callback?code=AUTHORIZATION_CODE&state=SAME_STATE_YOU_SAVEDIn your callback route:
- Read
codeandstatefrom the query string. - Confirm
statematches what you saved (prevents CSRF). - Load the saved
codeVerifierfor that session. - Exchange the code for tokens.
// Example: Express route GET /oauth/callback
app.get("/oauth/callback", async (req, res) => {
const code = req.query.code as string | undefined;
const returnedState = req.query.state as string | undefined;
if (!code) {
return res.status(400).send("Authorization was denied or failed.");
}
if (returnedState !== savedState) {
return res.status(400).send("Invalid state — possible CSRF.");
}
await fox.oauth!.exchangeCode(code, savedCodeVerifier);
// Tokens are saved automatically. Redirect to your app.
res.redirect("/dashboard");
});Step 3 — Use the API
Once exchangeCode succeeds, the SDK stores tokens and attaches them to every request. You can call the same resources as with an API key:
const business = await fox.business.get();
const bookings = await fox.bookings.list({ month: "2026-06" });The SDK refreshes access tokens automatically before they expire.
What is fox-tokens.json?
In the examples above, FileTokenStore("./fox-tokens.json") tells the SDK where to save OAuth tokens on disk.
- You do not create this file. The SDK writes it after a successful
exchangeCode. - The path is just an example — use any path you like.
- Keep it secret — it contains refresh tokens. Add it to
.gitignore. - One file = one connection. Fine for local scripts or a single test business.
Example contents (written by the SDK):
{
"accessToken": "eyJhbGciOiJSUzI1NiIs...",
"refreshToken": "opaque-refresh-token",
"expiresAt": 1710003600000,
"scope": "business:read bookings:read",
"tokenType": "Bearer"
}Production OAuth apps
If many Fox users will connect their businesses, do not use one shared file. Implement the TokenStore interface and save tokens in your database, keyed by your user ID (or Fox tenant_id):
import type { TokenStore, TokenSet } from "@foxscheduling/sdk";
class DatabaseTokenStore implements TokenStore {
constructor(private userId: string) {}
async load(): Promise<TokenSet | null> { /* read from DB */ }
async save(tokens: TokenSet): Promise<void> { /* write to DB */ }
async clear(): Promise<void> { /* delete from DB */ }
}For unit tests, use MemoryTokenStore() (tokens disappear when the process exits).
More detail: OAuth2 integration guide
Webhook signature verification
When Fox sends a webhook to your server, verify the X-Fox-Signature header:
import { verifyFoxWebhookSignature } from "@foxscheduling/sdk";
const ok = verifyFoxWebhookSignature({
secret: process.env.FOX_WEBHOOK_SECRET!,
timestamp: req.headers["x-fox-timestamp"],
rawBody: req.rawBody, // unparsed request body string
signatureHex: req.headers["x-fox-signature"],
});
if (!ok) {
return res.status(401).send("Invalid signature");
}Your HTTP framework must give you the raw body (before JSON parsing) for verification to work.
Scopes
Each API key or OAuth request uses scopes such as:
business:read, services:read, staff:read, availability:read, availability:write, bookings:read, bookings:write, customers:read, customers:write, webhooks:read, webhooks:write
Request only the scopes your integration needs. See the scope reference.
Links
Support
Questions or integration help: foxscheduling.com/contact
