Skip to main content

Webhooks

Get notified the moment something happens in Fullview — signed HTTPS deliveries, retries, and verification.

Written by Sofia

Webhooks let Fullview notify your systems the moment something happens — no polling. When an event you've subscribed to occurs, Fullview sends an HTTPS POST with a signed JSON body to an endpoint you control.

Typical uses: log meeting activity into your CRM or helpdesk, trigger follow-up workflows when a customer accepts or declines a call invitation, or kick off processing of a session recording the moment the customer leaves the meeting.

Setting up an endpoint

Webhook endpoints are managed in the Fullview dashboard under Settings → Webhooks (you need organisation-settings permissions, e.g. an admin role).

  1. Add an endpoint. Provide the HTTPS URL Fullview should deliver to, an optional description, and select the event types you want to receive.

  2. Copy the signing secret. Each endpoint gets its own secret, starting with whsec_. You'll use it to verify that deliveries really come from Fullview.

  3. Verify the endpoint. A new endpoint starts in Pending verification and receives no events yet. Use Send test event in the dashboard — Fullview synchronously sends a test.ping event, and a 2xx response from your endpoint activates it.

Endpoint URL requirements:

  • Must be https://.

  • Must resolve to a publicly routable address — private, loopback and link-local addresses are refused, both when you save the endpoint and again before every delivery.

  • Must not contain credentials (https://user:pass@… is refused).

  • Redirects are not followed. The URL you register must answer directly.

You can have up to 10 endpoints per organisation. Each endpoint has its own secret and its own event-type subscription.

Endpoint statuses

Status

Meaning

Pending verification

Newly created; receives no events until a test send succeeds.

Active

Receiving events.

Disabled

Turned off by a teammate. No events are delivered or queued for it.

Auto-disabled

Fullview stopped delivering after repeated failures — see Automatic disabling below.

Event types

Event type

Fires when

meeting.customer_invited

A customer is invited to a Fullview meeting.

meeting.customer_accepted

The customer accepts the meeting invitation.

meeting.customer_declined

The customer declines the meeting invitation.

meeting.customer_left

The customer leaves the meeting. If the meeting produced a recording, this event carries the session reference.

test.ping

You click Send test event in the dashboard. Sent regardless of subscription.

More event types will be added over time. Build your receiver to ignore event types it doesn't recognise — that's what makes new types a safe, non-breaking addition.

The delivery request

Every delivery is an HTTPS POST with a JSON body and these headers:

Header

Value

Content-Type

application/json

fullview-signature

Signature of the body — see Verifying signatures.

fullview-webhook-id

The event id (same as id in the body). Stable across retries — your dedupe key.

fullview-event-type

The event type (same as type in the body), so you can route without parsing.

Payload shape

All meeting events share one flat envelope — there is no wrapper object:

{
  "id": "evt_5f2c1a7e3b904d1e8a7b3c9d0e1f2a3b",
  "type": "meeting.customer_left",
  "createdAt": "2026-08-19T10:12:45.000Z",
  "agent": {
    "id": "usr_20aa...",
    "name": "Sam Agent",
    "email": "sam@yourcompany.com"
  },
  "customer": {
    "id": "cus_71be...",
    "name": "Ada Lovelace",
    "email": "ada@example.com",
    "externalId": "user-1815",
    "environment": "production",
    "roles": ["admin"]
  },
  "meeting": {
    "id": "meet_43d1...",
    "recordingId": "ses_9f2c1a7e3b90",
    "integration": {
      "type": "intercom",
      "caseId": "12345"
    }
  },
  "session": {
    "id": "ses_9f2c1a7e3b90"
  }
}

Field notes:

  • id — stable identifier for the event. It never changes across retries or replays, which makes it your idempotency key.

  • type — one of the event types above.

  • createdAt — ISO timestamp of when the event happened, not when it was sent.

  • agent — the host / inviting agent. null when unknown.

  • customer — the customer, as your system knows them: externalId is the id you supplied via the SDK identify() call (your natural join key), and environment / roles are the SDK-supplied tags — useful for filtering test vs. production traffic. Any field not known at send time is null.

  • meeting — the meeting the event belongs to. integration is set when the meeting was started from a third-party tool (e.g. intercom, zendesk), with caseId being the ticket it was started from — correlate it with your own helpdesk. null for meetings started in Fullview itself.

  • sessionmeeting.customer_left only: the session the meeting produced, null for every other event type and for meetings without a recording. session.id is exactly what the Fullview API's GET /v1/sessions/{id} takes. The guaranteed field is id; when the session is already indexed at send time the object is enriched best-effort with the same fields the API returns, but never rely on more than id being present — fetch the rest by id.

Test event payload

{
  "id": "evt_...",
  "type": "test.ping",
  "createdAt": "2026-08-19T10:00:00.000Z",
  "message": "Test event from Fullview. A 2xx response verifies this endpoint.",
  "endpointId": "whe_..."
}

Verifying signatures

Every delivery is signed so you can be sure it came from Fullview and wasn't tampered with. The fullview-signature header looks like:

fullview-signature: t=1755598365,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t is a Unix timestamp (seconds) of when the request was signed.

  • v1 is HMAC-SHA256(secret, "<t>.<raw body>"), hex-encoded.

To verify:

  1. Split the header on ,, then each part on the first =, to get t and v1.

  2. Concatenate t, a literal ., and the raw request body — the exact bytes you received. Re-serialising parsed JSON produces a different string and a false mismatch, so verify before parsing.

  3. Compute the HMAC-SHA256 of that string, keyed with your endpoint's signing secret. The key is the full secret string including the whsec_ prefix — this is the most common integration mistake.

  4. Compare against v1 with a constant-time comparison.

  5. Reject if t is more than 5 minutes from now — this bounds replay of a captured request.

Node.js example (Express):

const crypto = require('node:crypto');
 
const SECRET = process.env.FULLVIEW_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 300;
 
// Use a raw-body parser on the webhook route:
// the signature covers the exact bytes received.
app.post('/webhooks/fullview', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.header('fullview-signature') ?? '';
  const parts = Object.fromEntries(
    header.split(',').map((s) => [
      s.slice(0, s.indexOf('=')).trim(),
      s.slice(s.indexOf('=') + 1).trim(),
    ]),
  );
 
  const timestamp = Number(parts.t);
  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${timestamp}.${req.body.toString('utf8')}`)
    .digest('hex');
 
  const fresh =
    Number.isInteger(timestamp) &&
    Math.abs(Date.now() / 1000 - timestamp) <= TOLERANCE_SECONDS;
 
  const valid =
    fresh &&
    parts.v1?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
 
  if (!valid) {
    return res.status(401).send();
  }
 
  const event = JSON.parse(req.body.toString('utf8'));
 
  // Acknowledge fast; do real work asynchronously.
  res.status(200).send();
 
  processFullviewEvent(event); // your code
});

Rotating a secret

You can rotate an endpoint's secret from the dashboard at any time — for example after a suspected leak. New deliveries are signed with the new secret immediately, so update your receiver's stored secret at the same time. A leaked webhook secret grants no access to your Fullview data; the only risk is someone forging webhook requests to your own receiver, which rotation cures.

Responding to a delivery

  • Respond with any 2xx status within 10 seconds. That's the whole contract.

  • Anything else — a non-2xx status, a timeout, a connection failure — counts as a failed attempt and will be retried.

  • Do heavy work after acknowledging, not before. Queue the event and return 200 immediately; a receiver that processes inline will hit the 10-second timeout under load.

  • Response bodies are ignored (the first 1 KB is stored in the delivery log to help you debug).

Retries, ordering and deduplication

Retry schedule

Each event is attempted up to 4 times per endpoint: immediately, then — after each failure — again after 1 minute, 5 minutes and 15 minutes. If the last attempt fails, the delivery is marked exhausted and won't be retried automatically (you can replay it — see below).

Deliver-at-least-once

Fullview guarantees at-least-once delivery: in rare cases you may receive the same event more than once. The id field (and the fullview-webhook-id header) is constant across retries and replays — treat it as an idempotency key and skip events you've already processed.

Ordering

Events are not guaranteed to arrive in order — a retried meeting.customer_accepted can arrive after the meeting.customer_left for the same meeting. Use createdAt (when the event happened) rather than arrival time when ordering matters.

Automatic disabling

If 10 consecutive deliveries to an endpoint exhaust all their retries — roughly 3.5 hours of the receiver being completely unreachable — the endpoint flips to Auto-disabled and stops receiving new events. This is a circuit breaker, not a punishment: a brief outage or a deploy will never trip it.

  • Any successful delivery (real or test) resets the failure counter.

  • Re-enable an auto-disabled endpoint from the dashboard once your receiver is healthy. Events that occurred while the endpoint was disabled are not back-filled.

Monitoring and debugging deliveries

The dashboard's webhooks page shows the delivery log for each endpoint: every delivery, its status (pending, processing, succeeded, exhausted, cancelled), and every attempt with the response status code, the first 1 KB of the response body, the duration, and the error when no response arrived at all.

  • Replay: any delivery can be replayed from the dashboard — it re-arms the full retry cycle with a byte-identical body and the same event id (so your dedupe still works).

  • Retention: delivery history is kept for 30 days.

  • Send test event: available at any time, not just at setup — useful for checking a receiver change end to end.

Best practices

  1. Verify every signature. Your endpoint URL is guessable in principle; the signature is what makes a request trustworthy.

  2. Return 200 fast, process async. The 10-second timeout includes your handler.

  3. Dedupe on id. At-least-once delivery means duplicates are rare but expected.

  4. Ignore unknown event types. New types will appear; your receiver should log-and-skip rather than fail.

  5. Don't rely on enriched session fields. Only session.id is guaranteed on meeting.customer_left; fetch the full session from GET /v1/sessions/{id} when you need it.

  6. Use one endpoint per consumer. With up to 10 endpoints, give each integration its own endpoint and secret so you can rotate or disable one without touching the others.

  7. Filter with customer.environment. If your SDK identify calls tag environments, use the field to keep test traffic out of production workflows.

FAQ

Can I receive webhooks on a non-HTTPS or internal URL?
No. Endpoints must be HTTPS and publicly routable. For local development, use a tunnel (e.g. ngrok) that gives you a public HTTPS URL.

What IP addresses do webhooks come from?
Delivery IPs are not fixed. Authenticate deliveries by verifying the signature, not by IP allowlisting.

Why does my signature verification fail even though the secret is right?
The two usual causes: (1) hashing a re-serialised JSON body instead of the raw request bytes, or (2) using only the random part of the secret as the HMAC key — the key is the full string including the whsec_ prefix.

Do disabled endpoints queue events for later?
No. Events that fire while an endpoint is disabled (or auto-disabled) are not delivered to it and are not back-filled on re-enable.

Is there an API for managing webhook endpoints?
Endpoint management is dashboard-only today. The public API (see The Fullview API article) is where you fetch the session data a webhook points at.

Did this answer your question?