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:
- Indicați adresa webhook-ului pe site, de exemplu
https://site.example.com/api/type-webhook. Se acceptă doarhttps://cu adresă publică. - Apăsați „Secret nou” — CRM-ul arată secretul semnăturii o singură dată. Păstrați-l în variabila de mediu
TYPE_WEBHOOK_SECRETpe serverul site-ului. - 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:
{"event":"rental.site_changed","changes":["occupancy","prices"],"unit_ids":["11111111-2222-4333-8444-555555555555"],"at":"2026-12-23T10:00:00Z"}| Câmp | Ce înseamnă |
|---|---|
event | rental.site_changed — datele s-au schimbat; rental.site_ping — verificare din CRM |
changes | ce să goliți: units (texte, fotografii, facilități, arhivă), prices (prețuri, date speciale, reduceri), occupancy (rezervări și blocări) |
unit_ids | obiectele afectate; null — toate |
at | câ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_pingrăspundeți simplu2xx, cache-ul nu-l atingeți.
Semnătura
Antetele cererii:
| Antet | Valoare |
|---|---|
X-Type-Timestamp | ora trimiterii, secunde Unix |
X-Type-Signature | hex 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ă:
- Citiți corpul brut.
- Respingeți apelul dacă
X-Type-Timestampdiferă de ora curentă cu mai mult de 300 de secunde — astfel repetarea unei cereri interceptate nu va funcționa. - Calculați semnătura și comparați-o cu
X-Type-Signatureîn timp constant (timingSafeEqual,hash_equals). Dacă nu coincide — răspundeți401ș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
// 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)
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)
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:
99f5257e8baf058067be3cbc81276a70060c67ada36dec102cfcbe116bb71ece