WzGate
Build your own website

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

.env.local
# 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.

lib/wzgate.ts
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)

app/page.tsx
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)

lib/search.ts
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=24

Unknown 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-Site is missing, and why a preview deployment needs it.
  • Errors — what to branch on when a call fails.
  • API reference — every endpoint and its parameters.

On this page