Quick start
Read your company profile and run a search from a Next.js app — environment variables, headers, the response envelope and pagination.
This page builds the smallest thing that works: a Next.js project that reads your company profile on the server and runs a search from the browser. Copy the four values out of Settings → Websites → Connect your website first.
1. Environment
# The CRM host plus /api. Every path below is relative to this.
NEXT_PUBLIC_API_BASE_URL=https://crm.example.com/api
# Which site this deployment is. Sent as X-Site.
NEXT_PUBLIC_SITE_KEY=my-site
# The publishable key (pk_…). Public by design: it is inlined into the browser
# bundle and only works from one of this site's own domains.
NEXT_PUBLIC_SITE_PUBLISHABLE_KEY=pk_my-site_0123456789abcdef01234567
# The secret key (sk_…) for server-side calls. NOT prefixed NEXT_PUBLIC_, so
# Next never puts it in a browser bundle. Optional — server calls fall back to
# the publishable key.
SITE_SECRET_KEY=Only NEXT_PUBLIC_* variables reach the browser. If you ever see sk_ in a
built JavaScript chunk, that key is public — revoke it in the Connect tab and
create a new one.
2. One place that builds the headers
Both transports — the browser and your server — must send the same identity, so define it once.
export function siteHeaders({ server = false } = {}) {
const headers: Record<string, string> = { Accept: 'application/json' };
const siteKey = process.env.NEXT_PUBLIC_SITE_KEY;
if (siteKey) headers['X-Site'] = siteKey;
// A server call carries no Origin for the API to check, which is exactly what
// the secret key replaces.
const apiKey =
(server ? process.env.SITE_SECRET_KEY : undefined) ??
process.env.NEXT_PUBLIC_SITE_PUBLISHABLE_KEY;
if (apiKey) headers['X-Api-Key'] = apiKey;
return headers;
}
export const API = process.env.NEXT_PUBLIC_API_BASE_URL!;An unset value must send no header at all, never an empty one: a missing key
is a request that does not claim an identity, while X-Api-Key: '' is a key
that cannot be resolved — a different, refusable thing.
3. Read the company profile (server)
import { API, siteHeaders } from '@/lib/wzgate';
export default async function Page() {
const res = await fetch(`${API}/public/company-profile`, {
headers: siteHeaders({ server: true }),
// The profile changes rarely; revalidate rather than fetching per request.
next: { revalidate: 300 },
});
if (!res.ok) throw new Error(`Company profile: ${res.status}`);
const { data } = await res.json();
return <h1>{data.name}</h1>;
}4. Search (browser)
import { API, siteHeaders } from '@/lib/wzgate';
export async function searchProperties(q: string, page = 1) {
const params = new URLSearchParams({ q, page: String(page), perPage: '24' });
const res = await fetch(`${API}/public/search?${params}`, {
headers: siteHeaders(),
});
const body = await res.json();
if (!res.ok) throw new Error(body.code ?? 'SEARCH_FAILED');
return { rows: body.data, pagination: body.pagination };
}The response envelope
Every answer is wrapped:
{ "success": true, "data": { }, "timestamp": "2026-09-14T09:30:27.019Z" }A paginated answer puts pagination beside data, not inside it:
{
"success": true,
"data": [],
"pagination": {
"total": 100, "lastPage": 9, "currentPage": 1, "perPage": 12,
"prev": null, "next": 2
},
"timestamp": "2026-09-14T09:30:27.019Z"
}Unwrap data once, in your HTTP layer, and let the rest of your code see plain
rows.
Not every list paginates. GET /public/me/favorites,
/public/me/saved-searches, /public/me/viewing-requests and
/public/owner-listings/mine return a bare array in data. Treat
"data is not an array" as "no rows" rather than calling .map on it.
Pagination
Lists take page (1-based) and perPage, and perPage is capped at
100 — asking for more silently gives you 100, it is not an error. There is
no limit parameter.
GET /public/properties?page=2&perPage=24Unknown query parameters are a 400
The API validates query strings strictly: a parameter it does not declare is a
400, not something ignored.
GET /public/properties?type=apartment
→ 400 { "message": ["property type should not exist"] }Strip your own UI-only parameters before calling. Do not forward the browser's query string untouched.
Next
- Identify your site — what happens when
X-Siteis missing, and why a preview deployment needs it. - Errors — what to branch on when a call fails.
- API reference — every endpoint and its parameters.
Build your own website
The Wzgate public API — what it serves, what you can build on it, and the four values you need from the CRM before you write a line of code.
Identify your site
How a public request is matched to one website — verified domain, X-Site, and the deprecated X-Subdomain — and what a draft or paused site answers.