Webhooks

128 event types. Signed, retried, dead-lettered.

Telos fans out every state change as a webhook event. Subscribers receive a signed POST per matching event with exponential-backoff retries; terminal failures land in a dead-letter queue with Sentry alerts so you never silently lose a delivery.

Setup

Subscribing

Subscriptions are managed inside the app at Configurations → Webhooks by leadership and admin users. Each subscription stores a URL, shared secret, and the list of event types you want delivered. A Send test action exercises the full delivery path against your endpoint.

Subscriptions can also be managed programmatically at /api/v1/webhook-subscriptions (list, create, delete) with an API key or OAuth app token carrying webhook scopes. Subscriptions created by an OAuth app are disabled automatically when the org revokes the app's last grant.

Payload

Envelope

Every event uses the same envelope. The payload object varies by event type. The id is unique per event and is safe to use for idempotent processing on your side.

event
{
  "id": "01H8X3F4S0Z9KMQ4N6PYZR7C5A",
  "eventType": "opportunity.status_changed",
  "entityType": "opportunity",
  "entityId": "8c2c9d8e-1234-4abc-9def-1234567890ab",
  "payload": {
    "previousStatus": "shaping",
    "newStatus": "validated",
    "title": "Reduce onboarding drop-off"
  },
  "createdAt": "2026-05-18T14:23:11.842Z"
}
Trust

Signing & verification

Each delivery includes an X-Telos-Signature header of the form t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<rawBody>">, keyed by your subscription secret. Verify before doing anything with the payload: compare in constant time and reject timestamps more than 300 seconds from now, which blocks replay of captured deliveries.

node.js
import { createHmac, timingSafeEqual } from "node:crypto";

const TOLERANCE_SECONDS = 300;

export function verifyTelosWebhook(
  rawBody: string,
  signature: string, // "t=<unix seconds>,v1=<hex hmac>"
  secret: string,
): boolean {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(signature);
  if (!match) return false;
  const t = Number(match[1]);
  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
  const expected = createHmac("sha256", secret)
    .update(t + "." + rawBody)
    .digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(match[2], "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}
python
import hmac
import hashlib
import re
import time

TOLERANCE_SECONDS = 300

def verify_telos_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
    match = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", signature)
    if not match:
        return False
    t = int(match.group(1))
    if abs(time.time() - t) > TOLERANCE_SECONDS:
        return False
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, match.group(2))
Delivery

Retries

Each delivery is attempted up to six times. Non-2xx responses or connection failures trigger a retry on a fixed schedule. The request timeout is 10 seconds. After the sixth failure the delivery is marked dead and you receive a Sentry alert.

AttemptDelay
1Immediate
2+30 seconds
3+2 minutes
4+10 minutes
5+1 hour
6+6 hours
Dead-letter (Sentry alert)
Reference

Event catalog

Auto-generated from the source registry. When the team ships a new event type, it appears here without any docs work.

Audit — Webhook subscriptions

3
  • webhook_subscription.created
  • webhook_subscription.updated
  • webhook_subscription.deleted

Comments

3
  • comment.created
  • comment.updated
  • comment.deleted

Customers

7
  • customer.created
  • customer.updated
  • customer.deleted
  • customer.opportunity_linked
  • customer.opportunity_unlinked
  • customer.update_posted
  • customer.contact_added

Entity subscriptions

2
  • subscription.created
  • subscription.muted

External references

1
  • external_ref.attached

Good vibes

1
  • good_vibes.posted

Insights

8
  • insight.created
  • insight.updated
  • insight.archived
  • insight.pinned
  • insight.unpinned
  • insight.status_changed
  • insight.linked
  • insight.unlinked

Intake

11
  • intake.created
  • intake.updated
  • intake.owner_claimed
  • intake.shaping_requested
  • intake.needs_context
  • intake.accepted
  • intake.evaluated
  • intake.rejected
  • intake.declined
  • intake.reassigned_to_team
  • intake.promoted_to_opportunity

Messages

7
  • message.created
  • message.edited
  • message.deleted
  • message.pinned
  • message.unpinned
  • message.reaction_added
  • message.reaction_removed

Metrics

3
  • metric.value_updated
    payload: metricName, previousValue, newValue, unit
  • metric.target_reached
    payload: metricName, currentValue, targetValue, unit
  • metric.deleted

Notifications

3
  • notification.created
  • notification.pinned
  • notification.unpinned

Objectives

7
  • objective.created
  • objective.updated
  • objective.deleted
  • objective.deadline_approaching
    payload: objectiveTitle, deadline, daysRemaining
  • objective.metric_linked
  • objective.metric_unlinked
  • objective.metric_link_updated

Opportunities

17
  • opportunity.status_changed
    payload: previousStatus, newStatus, title
  • opportunity.created
  • opportunity.updated
  • opportunity.deleted
  • opportunity.completed
  • opportunity.cancelled
  • opportunity.synthesized
  • opportunity.risk_vote_cast
  • opportunity.risk_vote_changed
  • opportunity.insight_added
  • opportunity.tags_changed
  • opportunity.prototype_added
  • opportunity.prototype_removed
  • opportunity.objective_linked
  • opportunity.objective_unlinked
  • opportunity.objective_link_updated
  • opportunity.content_updated

Planning

5
  • team_queue.reordered
  • team_queue.chain_added
  • team_queue.chain_removed
  • team_queue.cross_team_chain_added
  • team_queue.window_set

PRD templates

3
  • prd_template.created
  • prd_template.updated
  • prd_template.deleted

Rooms

9
  • room.created
  • room.updated
  • room.archived
  • room.member_added
  • room.member_removed
  • room.pinned
  • room.unpinned
  • room.notification_level_changed
  • room.marked_read

Strategy

4
  • strategy.created
  • strategy.updated
  • strategy.published
  • strategy.archived

support

3
  • support.access_granted
  • support.access_revoked
  • support.access_expired

Tag themes

3
  • tag_theme.created
  • tag_theme.updated
  • tag_theme.deleted

Tags

3
  • tag.created
  • tag.updated
  • tag.deleted

Tasks

4
  • task.status_changed
    payload: previousStatus, newStatus, title
  • task.assigned
  • task.unassigned
  • task.tags_changed

Teams

8
  • team.created
  • team.updated
  • team.lead_changed
  • team.member_added
  • team.member_removed
  • team.invite_sent
  • team.invite_revoked
  • team.invite_accepted

Time entries

4
  • time_entry.created
  • time_entry.updated
  • time_entry.deleted
  • time_entry.range_upserted

Users

1
  • user.updated

Vision

2
  • vision.updated
    payload: visionLabel, action
  • vision.deleted

Workflows

6
  • workflow.created
  • workflow.updated
  • workflow.deleted
  • workflow.assigned
  • workflow.step_status_changed
  • workflow.checklist_item_toggled
Inbound

Inbound webhooks

Telos also accepts inbound events. Create a Generic Webhook integration at Configurations → Integrations, then POST JSON to /api/integrations/generic/<integrationId>/webhook. The recommended payload is a batch of typed actions, up to 20 per request, validated against the shared action schemas (any zod failure is rejected as a whole batch and the error names the failing index and field, e.g. actions[2].data.teamId: Invalid UUID).

request body
{
  "actions": [
    {
      "type": "intake.create",
      "data": {
        "title": "Refund request from #support",
        "teamId": "8c2c9d8e-1234-4abc-9def-1234567890ab",
        "priority": "high",
        "externalRef": { "externalKind": "zendesk:ticket", "externalId": "4821" }
      }
    },
    {
      "type": "metric.record_value",
      "data": { "metricId": "1f6b8a3c-0d2e-4f7a-9b1c-2d3e4f5a6b7c", "value": 1240.5 }
    },
    {
      "type": "task.transition",
      "data": { "taskRef": "PLT-42", "toStatus": "done" }
    }
  ]
}

For intake.create, the item's source is always recorded as webhook server-side, regardless of input. The optional externalRef object links the created entity back to the external system.

ActionRequired / optional fields
intake.createtitle, teamId / description, priority, externalRef
insight.createcustomerId, verbatim, source / interpretation, externalRef
metric.record_valuemetricId, value / recordedAt, note
task.createtitle / description, teamId, opportunityId, workflowId, priority, externalRef
opportunity.createtitle / description, teamId, workflowId, externalRef
task.transitiontaskRef, toStatus
external_ref.attachentityType, entityId, externalKind, externalId / url, metadata
Inbound

Signing inbound requests

Sign every request with your integration's webhook secret and send the result in the X-Telos-Signature header. The scheme is the same one Telos uses for outbound deliveries: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<rawBody>">. Telos verifies in constant time and rejects timestamps more than 300 seconds from now, which blocks replay of captured requests.

node.js
import { createHmac } from "node:crypto";

export function signTelosRequest(rawBody: string, secret: string): string {
  const t = Math.floor(Date.now() / 1000);
  const v1 = createHmac("sha256", secret)
    .update(t + "." + rawBody)
    .digest("hex");
  return "t=" + t + ",v1=" + v1;
}

// POST the raw body with headers:
//   Content-Type: application/json
//   X-Telos-Signature: <signTelosRequest(rawBody, secret)>
//   X-Telos-Idempotency-Key: <stable id for this logical delivery>
Inbound

Idempotency & the activity log

Send an X-Telos-Idempotency-Key header (up to 200 characters) that is stable per logical delivery. Redeliveries with the same key are acknowledged and dropped, so retrying senders never duplicate side effects. Without the header, Telos falls back to a hash of the raw body.

Every delivery is persisted to the integration's activity log at Configurations → Integrations before processing: the payload, the derived action list, per-action results, and any validation error. Failed deliveries can be replayed; actions that already succeeded are skipped.