Skip to content

Capture website leads via the API

Your website visitor has no BUZ login, so you don’t run OAuth on every form submission. Instead you connect once, then reuse a refresh token:

  1. Connect once — a user in your BUZ organisation signs in and approves your integration. You receive a refresh token.
  2. Store the refresh token securely on your server (never in the browser).
  3. On each enquiry — exchange the refresh token for a short-lived access token and POST the lead to the Sales API.

This is the same OAuth authorization-code + PKCE flow used to connect Xero or Google — the token BUZ issues is a real user token that carries your organisation and permissions, so nothing is hard-coded and no allow-listing is required.

Registering an OAuth client is not yet self-service, so contact BUZ support to register your integration. You’ll provide:

  • Redirect URI — where BUZ returns the user after they approve, e.g. https://yoursite.com/api/buz/callback
  • The scopes you need (see Scopes below)

You’ll receive a Client ID and Client secret. Keep the secret server-side.

Endpoint URL
Authorize https://login.buzmanager.com/connect/authorize
Token https://login.buzmanager.com/connect/token
List the user’s organisations https://api.buzmanager.com/identity/My/Permissions
Create a lead https://api.buzmanager.com/sales/leads

2. Connect once (authorization code + PKCE)

Section titled “2. Connect once (authorization code + PKCE)”

Add a one-time “Connect to BUZ” page. When clicked, generate a PKCE verifier and redirect to the authorize endpoint:

// GET /connect/start
import { randomBytes, createHash } from "node:crypto";
const b64url = (b) => b.toString("base64").replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");
const verifier = b64url(randomBytes(32));
const challenge = b64url(createHash("sha256").update(verifier).digest());
const state = b64url(randomBytes(16));
// Persist { state, verifier } in an HttpOnly, Secure, SameSite=Lax cookie (10 min).
const url = "https://login.buzmanager.com/connect/authorize?" + new URLSearchParams({
response_type: "code",
client_id: YOUR_CLIENT_ID,
redirect_uri: "https://yoursite.com/api/buz/callback",
scope: "openid profile offline_access buz.sales.read buz.sales.write buz.inventory.read",
state,
code_challenge: challenge,
code_challenge_method: "S256",
prompt: "login", // force an explicit sign-in as your integration user
});
// 302 redirect the browser to `url`.

The user signs in, sees a consent screen listing the permissions you requested, and approves. BUZ redirects back to your callback with ?code=…&state=…. Exchange the code for tokens:

// GET /api/buz/callback (verify `state` against the cookie first)
const res = await fetch("https://login.buzmanager.com/connect/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "https://yoursite.com/api/buz/callback",
code_verifier: verifier, // from the cookie
client_id: YOUR_CLIENT_ID,
client_secret: YOUR_CLIENT_SECRET,
}),
});
const { refresh_token, access_token } = await res.json();
// Store refresh_token securely (e.g. AWS SSM SecureString, a secrets manager, or an encrypted row).

The token carries every organisation the user belongs to — you don’t hard-code an org id. Ask BUZ which organisations this user has, and use that organisation’s id as the Selected-Organization header on subsequent calls:

const orgs = await fetch("https://api.buzmanager.com/identity/My/Permissions", {
headers: { Authorization: `Bearer ${access_token}` },
}).then((r) => r.json());
// orgs.userOrganizationPreferences[].organization.{ pkId, name }
// One org → use it. Several → let the person connecting pick one, then store the chosen pkId.

Connect as a user who belongs to one organisation (a dedicated “Website integration” user is ideal) and this is fully automatic.

Mint an access token from the stored refresh token, then POST the lead. Set the Selected-Organization header to the org id from step 3.

// Mint a fresh access token (cache it until it expires)
const { access_token } = await fetch("https://login.buzmanager.com/connect/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: STORED_REFRESH_TOKEN,
client_id: YOUR_CLIENT_ID,
client_secret: YOUR_CLIENT_SECRET,
}),
}).then((r) => r.json());
// Create the lead
await fetch("https://api.buzmanager.com/sales/leads", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Selected-Organization": ORG_ID,
Authorization: `Bearer ${access_token}`,
},
body: JSON.stringify({
CompanyName: "Jane's Interiors",
FirstName: "Jane",
LastName: "Doe",
Email: "jane@example.com",
Phone: "0400 000 000",
Mobile: "0400 000 000",
Description: "Website enquiry",
Notes: "Interested in roller blinds for a new home.",
// Optional tentative appointment (leave StartTime as "00:00" for an all-day/top-of-day slot)
AppointmentDate: "2026-08-01",
StartTime: "09:00",
// Products of interest — inventory group PkIds (see GET /inventory/Groups), qty 1 each
Items: [{ InventoryGroupPkId: "", Qty: 1 }],
// Your marketing questions, keyed by option code
MarketingAnswers: { Source: "Website", TYPE: "Retailer" },
}),
});
  • Access tokens are short-lived (~1 hour) — cache and re-mint from the refresh token as needed.
  • The refresh token is long-lived but will eventually lapse after a long idle period. If a refresh fails, send the person back through Connect once to re-establish it.
  • Store the refresh token encrypted and server-side only.

Request the least privilege you need:

Scope Grants
openid profile Sign-in / identity
offline_access A refresh token (required for connect-once)
buz.sales.read Read lead marketing options
buz.sales.write Create leads
buz.inventory.read Read inventory groups (products of interest)

Our own site, buzsoftware.com.au, is built exactly this way: connect once, refresh token stored server-side, org captured from the connected user, and each demo request posted as a lead — with the form fields driven live by the org’s marketing questions and products.