La conținut
Type — API pentru site
Secțiuni

Webhook-uri și verificarea semnăturii

Webhook-ul este un apel de la CRM spre site, când s-au schimbat obiectele, prețurile sau ocuparea. După el site-ul golește cache-ul și nu trebuie să interogheze des API-ul.

Configurare

Setări → Chirii → Site:

  1. Indicați adresa webhook-ului pe site, de exemplu https://site.example.com/api/type-webhook. Se acceptă doar https:// cu adresă publică.
  2. Apăsați „Secret nou” — CRM-ul arată secretul semnăturii o singură dată. Păstrați-l în variabila de mediu TYPE_WEBHOOK_SECRET pe serverul site-ului.
  3. Apăsați „Verificați” — CRM-ul trimite apelul de test rental.site_ping.

Cererea

CRM-ul trimite POST la adresa webhook-ului cu un corp JSON:

json
{"event":"rental.site_changed","changes":["occupancy","prices"],"unit_ids":["11111111-2222-4333-8444-555555555555"],"at":"2026-12-23T10:00:00Z"}
CâmpCe înseamnă
eventrental.site_changed — datele s-au schimbat; rental.site_ping — verificare din CRM
changesce să goliți: units (texte, fotografii, facilități, arhivă), prices (prețuri, date speciale, reduceri), occupancy (rezervări și blocări)
unit_idsobiectele afectate; null — toate
atcând a avut loc schimbarea
  • Modificările din 10 secunde CRM-ul le adună într-un singur apel.
  • Răspundeți cu orice 2xx. Dacă răspunsul e altul sau site-ul nu a răspuns, CRM-ul repetă apelul după 1, 5 și 30 de minute.
  • La rental.site_ping răspundeți simplu 2xx, cache-ul nu-l atingeți.

Semnătura

Antetele cererii:

AntetValoare
X-Type-Timestampora trimiterii, secunde Unix
X-Type-Signaturehex HMAC-SHA256 cu secretul webhook-ului din șirul "{timestamp}.{body}"

body sunt octeții bruți ai corpului cererii, nu JSON-ul parsat și recompus: orice spațiu în plus schimbă semnătura.

Cum se verifică:

  1. Citiți corpul brut.
  2. Respingeți apelul dacă X-Type-Timestamp diferă de ora curentă cu mai mult de 300 de secunde — astfel repetarea unei cereri interceptate nu va funcționa.
  3. Calculați semnătura și comparați-o cu X-Type-Signature în timp constant (timingSafeEqual, hash_equals). Dacă nu coincide — răspundeți 401 și nu goliți nimic.

Semnătura webhook-ului protejează site-ul de apeluri false. Nu este o cheie de acces la API: cheia API nu se trimite în webhook.

Cu pachetul

ts
// app/api/type-webhook/route.ts
import { revalidateTag } from 'next/cache';
import { verifyWebhook } from '@type/site-client';

export async function POST(req: Request) {
  const body = await req.text(); // corpul brut
  const res = await verifyWebhook(body, req.headers, process.env.TYPE_WEBHOOK_SECRET!);
  if (!res.ok) return Response.json({ error: res.reason }, { status: 401 });
  if (res.payload.event === 'rental.site_changed') {
    for (const change of res.payload.changes) revalidateTag(`rental:${change}`); // units | prices | occupancy
  }
  return Response.json({ ok: true });
}

verifyWebhook întoarce { ok: true, payload } sau { ok: false, reason }, unde reason este missing (lipsesc antetele), timestamp (în afara ferestrei de 300 s), signature sau payload (corpul nu este JSON-ul webhook-ului).

Dacă puneți în cache pe obiecte, folosiți etichete de forma rental:occupancy:<unit_id> și goliți doar pe cele afectate; la unit_ids: null — toate.

Fără pachet (Node.js)

ts
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verify(body: string, ts: string | null, sig: string | null, secret: string): boolean {
  if (!ts || !sig || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${ts}.${body}`).digest();
  const got = Buffer.from(sig.toLowerCase(), 'hex');
  return got.length === expected.length && timingSafeEqual(got, expected);
}

Fără pachet (PHP)

php
function type_webhook_ok(string $body, string $ts, string $sig, string $secret): bool {
    if ($ts === '' || abs(time() - (int) $ts) > 300) {
        return false;
    }
    $expected = hash_hmac('sha256', $ts . '.' . $body, $secret);
    return hash_equals($expected, strtolower($sig));
}

Exemplu pentru verificare

Toate valorile sunt fictive — folosiți-le în testul verificării dvs. (fără fereastra de 300 s):

  • secretul: whsec_example_0123456789abcdef0123456789abcdef;
  • X-Type-Timestamp: 1798000000;
  • corpul — JSON-ul din secțiunea „Cererea” de mai sus, octet cu octet.

Semnătura așteptată X-Type-Signature:

text
99f5257e8baf058067be3cbc81276a70060c67ada36dec102cfcbe116bb71ece