Gareth Wilson Gareth Wilson

Guide to Travtus Webhooks: Features and Best Practices

Published


Travtus makes Adam, an AI assistant for residential property management teams. Adam talks to residents by SMS, email and web chat, turns their requests into tasks, and keeps lease records alongside. Travtus webhooks push those changes to your property management system, and they also carry Adam's replies when you send resident messages through the Travtus Messaging API.

AspectValue
MethodPOST, JSON body
Signature headerssvix-id, svix-timestamp, svix-signature (Standard Webhooks)
SuccessAny 2xx within 15 seconds
RetriesImmediately, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, then 10 h again
Disabled endpointsAn endpoint failing for 5 days is disabled and an EndpointDisabledEvent is sent
DeliveryAt-least-once; svix-id is stable across retries

Below: how Travtus webhooks are requested and managed, the lease, task and messaging events, Svix verification, and the parts of Travtus's documentation that are thin.

What are Travtus webhooks?

Travtus delivers webhooks through Svix, which it calls its webhook partner. Its authentication page shows the Svix library verifying the svix-id, svix-timestamp and svix-signature headers with a whsec_ secret.

Setup starts with a person rather than a settings page: you email Travtus support with your URL and they send back the secret. After that, a Svix application portal, opened through a short-lived URL from the API, lets you add endpoints, choose event types, send test events and recover failed messages.

Travtus webhook features

FeatureDetails
SignatureStandard Webhooks HMAC-SHA256
Signature headersvix-signature
BodyJSON
Event routingBody field event_type
Delivery platformSvix
SetupEmail support@travtus.com, or the webhook portal
Secretwhsec_..., one per endpoint
Response window15 seconds
Retry spanA little over a day, ending with two 10-hour gaps
ReplaySingle resend, or bulk recovery from a date
Test eventsPortal Testing tab

Events and payloads

Every delivery has three top-level fields:

  • event_type is the event name.
  • message_id is the Svix-assigned message ID. Don't confuse it with body.message_id on Messaging API replies, which is the original message's identifier.
  • body is the lease, the task (with a creator object) or the AI reply.
  • Deduplicate on the svix-id header (stable across retries).

Common Event Types

EventTriggered Whenbody fields
lease.updateA lease is created or updatedidentifier, start_date, end_date, move_in_date, rent, deposit, notice_given, notice_date, move_out_date_requested
task.createA task is createdidentifier, external_ref, title, summary, category, topic, importance, status, queue_name, due_datetime, creator, and more
task.updateA task is updatedsame as task.create
task.deleteA task is deletedsame as task.create (status is deleted)
sms.respondAI response to an SMS messagemessage_id, conversation_id, sender_name, message_text, channel, company_id, human_handover_ind, portfolio_external_ref
email.respondAI response to an email messagesame as sms.respond
web_chat.respondAI response to a web chat messagesame as sms.respond

Travtus documents these 7 events, and the list may not be complete.

Task status is one of created, in_progress, completed, deleted. Task importance is low, normal, high or very_high. For Messaging API replies channel is sms, email or website, and human_handover_ind is true when Travtus suggests handing over to a human.

Tasks also carry priority_score (0 to 1000), escalation_flag, onsite_indicator (onsite or offsite) and a queue_name from a fixed list of departments such as Facilities, Leasing and Renewals, or null. Either identifier or external_ref can be used to look a task up again. relevant_conversation can hold the transcript that led to the task, and summary has no length limit, so plan storage accordingly.

Published payload example

This is the task.update sample from the webhook samples library. Use its fields to explore the payload; it is a documentation example.

{
  "body": {
    "action_type": "Assistance",
    "assignee_id": null,
    "attachments": {
      "file_A": "https://u.dev.hiadam.co/pi5fwe3"
    },
    "completed_datetime": null,
    "creator": {
      "email": "support@travtus.com",
      "unit_external_ref": "P~4770692~467~2016",
      "lease_external_ref": "A1234",
      "first_name": "Bob",
      "last_name": "James",
      "phone_number": "+447549111111",
      "resident_address": "10 Executive Park West NE",
      "unit_identifier": "4efdccb7-10ca-47b0-811c-de2fae9f6682",
      "unit_number": "2016"
    },
    "datetime_created": "2023-12-20T17:32:49.143000+00:00",
    "datetime_updated": "2024-01-15T15:07:27.440000+00:00",
    "description": "a description",
    "due_datetime": "2023-12-19T12:00:00+00:00",
    "external_ref": "1234567890",
    "identifier": "26d2f5f9-b128-48cd-add8-303e067df958",
    "importance": "very_high",
    "notes": null,
    "portfolio_external_ref": "P1234",
    "portfolio_id": "b0234194-e61b-4a57-967f-6eaaee4d5219",
    "priority_score": 500,
    "queue_name": null,
    "relevant_conversation": null,
    "reminder_datetime": null,
    "resident_id": null,
    "status": "created",
    "category": "Customer Support",
    "summary": "a summary",
    "title": "a title",
    "onsite_indicator": "onsite",
    "topic": "maintenance",
    "user_goal": "fix_shower",
    "escalation_flag": false
  },
  "event_type": "task.update",
  "message_id": "51d155b3-496f-434e-b664-406836be35a0"
}

In this sample the unit and lease references sit inside creator, while Travtus's field table lists unit_identifier, unit_number, lease_external_ref and resident_address at the top level of body as well. Read them from either place.

Setting up Travtus webhooks

  1. Email support@travtus.com with the URL that should receive events, for example https://example.com/webhooks/travtus.
  2. Travtus sets up the webhook and replies with a signing secret that starts whsec_. Store all of it, prefix included, as TRAVTUS_WEBHOOK_SECRET. Each endpoint has its own secret.
  3. To add or change endpoints yourself, request a portal link with GET https://api.travtus.com/webhook/app-portal-access/ and your API bearer token. The response is { "url": "..." }, a short-lived link to the portal.
  4. In the portal's Endpoints section, add the URL and pick event types. With none selected, the endpoint receives every event, which is handy while testing.
  5. Use the Testing tab to send example events, then open each message to see the payload and attempts. Svix Play gives you a temporary URL if your service isn't ready.

To receive deliveries on your own machine, use the Hookdeck CLI:

npx hookdeck-cli listen 3000 travtus --path /webhooks/travtus

With Hookdeck. Give Travtus a Travtus source's URL as your endpoint. Travtus sets up webhooks through support@travtus.com and sends you the whsec_ secret; use it as the source's verification secret. The source checks the svix-* headers, accepts only POST and answers 200 well inside Travtus's 15 seconds. Requests that fail verification are recorded as failed and go no further.

Securing Travtus webhooks

Travtus's examples use the Svix library, which computes HMAC-SHA256 over {svix-id}.{svix-timestamp}.{raw body} with the base64-decoded secret after whsec_ and compares the result with each v1, value in svix-signature. The same check without the library:

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['svix-id'] ? 'svix' : 'webhook';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Travtus asks you to verify the payload exactly as sent and to disable CSRF protection on the route. An Express handler that files tasks and passes replies on:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

const app = express();

app.post('/webhooks/travtus', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.TRAVTUS_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  const payload = parseJsonObject(req.body);
  if (!payload) return res.sendStatus(400); // signed, but not a JSON object
  const { event_type, message_id, body } = payload;
  if (typeof event_type !== 'string' || !message_id || !body || typeof body !== 'object') return res.sendStatus(400);
  try {
    if (await deliveries.seen(message_id)) return res.sendStatus(200);

    switch (event_type) {
      case 'task.create':
      case 'task.update':
      case 'task.delete':
        await workOrders.sync(body.identifier, body.status, body);
        break;
      case 'sms.respond':
      case 'email.respond':
      case 'web_chat.respond':
        if (body.human_handover_ind) await staff.assign(body.conversation_id);
        await conversations.reply(body.conversation_id, body.channel, body.message_text);
        break;
      case 'lease.update':
        await leases.upsert(body.identifier, body);
        break;
      default:
        break;
    }
    await deliveries.record(message_id);
  } catch {
    return res.sendStatus(503); // not processed, so don't acknowledge
  }
  return res.sendStatus(200);
});

deliveries, workOrders, staff, conversations and leases stand for your own code. Travtus describes the top-level message_id as the Svix-assigned message ID, and its task page says it's there for deduplication. For anything that takes more than a few seconds, queue the event and answer first.

Travtus says the signed timestamp helps prevent replay attacks, and the Svix library rejects one more than five minutes old. Behind Hookdeck that check would fail on retries and replays, because Hookdeck keeps the original svix-timestamp and applies no tolerance of its own. The function above checks only the signature.

Make Travtus webhooks production-ready. Hookdeck Event Gateway answers Travtus within its 15-second window, retries your service when it's down, and routes task events and AI replies to different systems.

Travtus webhook limitations and pain points

Setup goes through support

The Problem: You can't create the first webhook or get its secret on your own.

Why It Happens: Travtus's setup instructions start with an email to support@travtus.com, and the secret comes back from support.

Workarounds:

  • Ask for staging and production endpoints in the same request.
  • Each endpoint has its own secret, so a re-created endpoint means asking for the new whsec_ value.
  • Once you have API access, use the portal link to add further endpoints yourself.

How Hookdeck Can Help: Point Travtus at one Hookdeck source and add or change your own destinations in Hookdeck, without going back to support.

A 15-second limit and disabling after 5 days

The Problem: Slow handlers cause retries, and a long outage disables the endpoint.

Why It Happens: Travtus counts a response after 15 seconds as a failure, and disables an endpoint that fails for 5 days.

Workarounds:

  • Answer first and process from a queue.
  • After an outage, re-enable the endpoint in the portal with Enable Endpoint, then use Recover Failed Messages.

How Hookdeck Can Help: Hookdeck answers Travtus straight away, so the endpoint stays healthy while your service is down, then retries delivery to you.

Lease events are thinly documented

The Problem: You can't be sure which lease events exist or what every field looks like.

Why It Happens: The lease page says events fire when a lease is created or updated but shows only a lease.update example, which is missing a comma and isn't valid JSON. No lease.create is documented, and sms.respond and email.respond have no examples of their own.

Workarounds:

  • Subscribe to all events while testing, and log any event_type you don't recognise.
  • Parse fields defensively until you've seen real deliveries.

How Hookdeck Can Help: Hookdeck stores every request in full, so you can see the real lease payloads and event names.

Two message IDs in reply events

The Problem: Deduplicating on the wrong ID drops real replies or keeps duplicates.

Why It Happens: Messaging replies have a top-level message_id (the Svix message) and a body.message_id (the resident's original message).

Workarounds:

  • Deduplicate on the top-level message_id or the svix-id header, and use body.message_id only to link the reply to its conversation.

How Hookdeck Can Help: Hookdeck's deduplication can key on the svix-id header, so retried deliveries are dropped before they reach you.

Best practices

Plan for human handover

When human_handover_ind is true, route the conversation to staff rather than only posting Adam's reply.

Sort tasks by priority

Use importance, priority_score and escalation_flag to order work, and queue_name to send it to the right team.

Keep track of endpoint health

The portal shows attempts per message. Check it after any deployment that touches the webhook route.

Make handlers idempotent

Travtus keeps retrying for more than a day, so store the message ID with each change. Our webhook idempotency guide explains how.

Conclusion

Travtus webhooks connect Adam's resident conversations to the rest of your property stack: tasks for your maintenance and leasing teams, lease changes, and AI replies to deliver. The Svix delivery is solid and well described. Setup through support, the 15-second limit and thin lease documentation are the parts to plan for.

Hookdeck Event Gateway keeps your Travtus endpoint responsive and routes each event to the right system. Sign up and send Travtus support a Travtus source URL.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.