К содержимому
Type — API сайта
Разделы

Установка и быстрый старт

@type/site-client — серверный клиент API сайта: типизированные методы, ошибки как классы и проверка подписи вебхука. Зависимостей нет; нужны fetch и Web Crypto, поэтому пакет работает в Node.js 20+ и в Edge-рантайме. К фреймворку не привязан — ниже примеры для Next.js (App Router).

Установка

Пакет пока не опубликован в публичном реестре npm. Архив type-site-client-0.5.0.tgz выдаёт владелец аккаунта CRM; положите его в репозиторий сайта (например, в vendor/) и установите:

bash
pnpm add ./vendor/type-site-client-0.5.0.tgz
# или: npm install ./vendor/type-site-client-0.5.0.tgz

Текущая версия — 0.5.0, изменения — в истории версий.

Пакет работает только на сервере. При импорте в браузерную сборку он сразу бросает ошибку — так ключ не попадёт на страницу случайно. В Next.js дополнительно поставьте import 'server-only' в модуль, который создаёт клиент.

Переменные окружения

Только серверные — без префикса NEXT_PUBLIC_:

bash
TYPE_API_URL=https://crm.example.com      # адрес вашей CRM, без пути
TYPE_API_KEY=tk_<key_id>_<secret>         # ключ с правом «Аренда: сайт»
TYPE_WEBHOOK_SECRET=<секрет вебхука>      # Настройки → Аренда → Сайт

Файлы .env* с этими значениями не должны попадать в репозиторий.

Первый запрос

ts
// lib/crm.ts
import 'server-only';
import { createSiteClient } from '@type/site-client';

export const crm = createSiteClient({
  baseUrl: process.env.TYPE_API_URL!,
  apiKey: process.env.TYPE_API_KEY!,
});

// Проверка подключения: компания, валюта, языки, время заезда и выезда.
const info = await crm.ping();

baseUrl принимается только с https:// (кроме localhost), иначе ключ ушёл бы открытым текстом.

Методы

МетодМаршрутЧто возвращает
ping()GET /pingкомпания, валюта, языки, время заезда и выезда
groups()GET /groupsрайоны с числом объектов
units({ lang })GET /unitsвсе объекты (кроме архивных)
unit(id, { lang })GET /units/{id}один объект
occupancy({ date_from, date_to, unit_ids })GET /occupancyзанятые интервалы; date_to не включительно, период до 400 дней
quote({ unit_id, check_in, check_out, guests })GET /quoteрасчёт по ночам; при занятых датах — available: false и violations
prices({ check_in, check_out, guests, group_id })GET /pricesцена и доступность всех объектов на даты
createBookingRequest(body, { visitorIp })POST /booking-requestsзаявка на бронь
legacyUrls()GET /legacy-urlsстарые адреса для переадресаций
photoUrl(url)—абсолютный адрес фото

Отдельной функцией, без клиента, экспортируется verifyWebhook(body, headers, secret) — проверка подписи вебхука.

Пути маршрутов — от /api/modules/rental/site. Каждый метод последним аргументом принимает { signal, init }: signal — для отмены, init — дополнительные поля fetch (заголовок Authorization ими не переопределить).

Клиент по умолчанию ставит cache: 'no-store'. Кэшируйте результаты сами — например, unstable_cache с тегами — и сбрасывайте их вебхуком.

Каталог: серверный компонент

tsx
// app/[lang]/apartamente/page.tsx
import { unstable_cache } from 'next/cache';
import { crm } from '@/lib/crm';
import type { Lang } from '@type/site-client';

const getUnits = unstable_cache((lang: Lang) => crm.units({ lang }), ['rental-units'], {
  tags: ['rental:units'],
  revalidate: 3600,
});

export default async function Catalog({ params }: { params: Promise<{ lang: Lang }> }) {
  const { lang } = await params;
  const { units } = await getUnits(lang);
  return (
    <ul>
      {units.map((u) => (
        <li key={u.id}>
          <a href={`/${lang}/apartament/${u.slug}`}>
            {u.photos[0] && <img src={crm.photoUrl(u.photos[0].url)} alt={u.name} />}
            <h2>{u.name}</h2>
            <p>{u.price} {u.currency}</p>
          </a>
        </li>
      ))}
    </ul>
  );
}

Ссылки на фото постоянные и открываются без ключа: CRM отвечает переадресацией 302 на файл в хранилище. Для next/image разрешите в images.remotePatterns хост CRM и хост хранилища или используйте unoptimized.

Заявка на бронь: route handler

ts
// app/api/booking/route.ts
import { RentalUnavailableError, SiteNetworkError, TooManyAttemptsError } from '@type/site-client';
import { crm } from '@/lib/crm';

export async function POST(req: Request) {
  const f = await req.json();
  // IP посетителя — из заголовка вашего прокси; по нему CRM считает лимит заявок.
  const visitorIp = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim();
  try {
    const res = await crm.createBookingRequest(
      {
        request_id: f.request_id, // UUID формы: повтор с тем же id не создаст вторую заявку
        unit_id: f.unit_id,
        check_in: f.check_in,
        check_out: f.check_out,
        guests: Number(f.guests ?? 1),
        name: f.name,
        phone: f.phone || null,
        email: f.email || null,
        message: f.message || null,
        lang: f.lang,
        page_url: f.page_url ?? null,
        consent: true,
        hp: f.hp ?? '', // скрытое поле-ловушка, должно быть пустым
        elapsed_ms: Number(f.elapsed_ms ?? 0), // сколько форма была открыта
      },
      visitorIp ? { visitorIp } : {},
    );
    return Response.json(res, { status: 201 });
  } catch (e) {
    if (e instanceof RentalUnavailableError) return Response.json({ violations: e.violations }, { status: 422 });
    if (e instanceof TooManyAttemptsError) {
      return Response.json({ error: 'too_many_attempts' }, { status: 429, headers: { 'Retry-After': String(e.retryAfter ?? 60) } });
    }
    if (e instanceof SiteNetworkError) return Response.json({ error: 'unavailable' }, { status: 503 });
    throw e;
  }
}

Заявка создаёт в CRM сделку на этапе «Заявка» с расчётом цены; клиент ищется по телефону, затем по e-mail. Если CRM была недоступна, повторите отправку с тем же request_id — вторая сделка не появится.

Антиспам: поле hp должно быть пустым, elapsed_ms — не меньше 3000. Если проверка не пройдена, ответ такой же, как при успехе, но заявка не создаётся.

Ошибки

Все ошибки наследуют SiteApiError с полями status, code и body.

КлассКогда
ApiKeyInvalidError401 api_key_invalid — ключа нет, он неверный, отозван, истёк или IP не разрешён. Причину видно только в CRM, в журнале ключа
ScopeMissingError403 scope_missing — у ключа нет права; scope — какого (rental:site)
ScopeUnavailableError403 scope_unavailable — право есть, но модуль аренды выключен
TooManyAttemptsError429 too_many_attempts; retryAfter — секунды из Retry-After или null
RentalUnavailableError422 rental_unavailable у заявки; violations — почему
SiteApiErrorпрочее: validation_error, not_found, bad_response…
SiteNetworkErrorCRM не ответила: сеть или тайм-аут (5 с для чтения, 10 с для заявки)

Ошибки ключа (401, 403) — проблема настройки сайта, а не посетителя: залогируйте code и покажите посетителю нейтральное «сервис временно недоступен». Сам ключ в сообщения ошибок не попадает.