Skip to content

Outgoing webhooks

Outgoing webhooks notify your own systems when your catalog changes. You register an HTTPS endpoint, and Componecat sends it a signed HTTP POST for each event it subscribes to. Outgoing webhooks depend on your plan — see Plans and limits.

Organization admins manage webhooks from Organization settings → Outgoing Webhooks: create one, choose which events it receives and its detail level, and review each webhook’s delivery history. The signing secret is shown once, when you create the webhook or rotate its secret — store it then.

The same operations are available on the REST API, with a token granted access to outgoing webhooks:

Operation Endpoint
List webhooks GET /api/rest/v1/outgoing-webhooks
Create a webhook (returns the secret) POST /api/rest/v1/outgoing-webhooks
Change URL, filters, headers, detail level, or status PATCH /api/rest/v1/outgoing-webhooks/{id}
Delete a webhook DELETE /api/rest/v1/outgoing-webhooks/{id}
Rotate the signing secret POST /api/rest/v1/outgoing-webhooks/{id}/rotate-secret
List the selectable record types, actions, and kinds GET /api/rest/v1/outgoing-webhooks/event-metadata
POST /api/rest/v1/outgoing-webhooks
{
"url": "https://hooks.example.com/componecat",
"description": "Jira automation",
"entityTypeFilters": ["CatalogEntity", "ScorecardEvaluation"],
"eventFilters": ["created", "updated", "evaluated"],
"customFilters": [{ "key": "kind", "values": ["<service kind id>"] }],
"customHeaders": [{ "name": "Authorization", "value": "Bearer …" }],
"detailLevel": "FULL"
}

The request and response schemas are in the OpenAPI document.

An organization can have up to 10 webhooks. The URL must be HTTPS.

Each event reports one change to one record. These record types send events:

Entity type What it is
CatalogEntity A catalog entity — see Entities and kinds
CatalogInterface An entity’s interface — see Interfaces
CatalogDocumentationSection A documentation section on an entity
CatalogDocumentationLink A documentation link on an entity
CatalogRelationship A relationship between two entities — see Relationships
ScorecardEvaluation An entity’s result against a scorecard — see Scorecards
Team A team that owns entities — for example, a rename or a change to a synced team’s external id
WikiPage A page in the organization’s wiki, whether written in Componecat or synced from a repository

Changes to kinds, fields, and integrations don’t send events.

Action Sent when Record types
created A record is created All except ScorecardEvaluation
updated A record changes All except ScorecardEvaluation
deleted A record is deleted All except ScorecardEvaluation
evaluated An entity’s score or attained level on a scorecard changes ScorecardEvaluation

evaluated is sent only when a result actually changes — a scorecard re-evaluation that leaves an entity’s score and level as they were sends nothing.

The event type is componecat.{record-type}.{action}, with the record type in kebab-case — for example componecat.catalog-entity.updated, componecat.team.deleted, componecat.wiki-page.created, or componecat.scorecard-evaluation.evaluated.

Moving a record to the trash sends deleted. Restoring it from the trash sends no event. Deleting a trashed record permanently sends a second deleted event for the same record, so a handler that reacts to deleted should tolerate receiving it twice for one id.

A webhook filters events on three settings. Leave a setting empty to receive everything it would otherwise narrow; an event is sent only when it passes all three.

Setting API field Narrows by
Record types entityTypeFilters The record type, e.g. CatalogEntity or Team
Actions eventFilters The action, e.g. updated
Entity kinds customFilters with key kind The kind of the catalog entity the event is about, by kind id

The entity kind filter applies to catalog entities and to the records that belong to them: interfaces, documentation sections and links, scorecard results, and relationships (by the relationship’s source entity). It doesn’t narrow Team events. A deleted interface, documentation link, or relationship can no longer be traced to its entity, so it is sent whatever kinds you picked.

GET /api/rest/v1/outgoing-webhooks/event-metadata lists the record types with the actions each one sends, and the entity kinds you can pick with their ids.

A webhook can send up to 10 extra HTTP headers with every delivery — for example an Authorization header your endpoint expects. Header values are stored encrypted and are never shown again after you save them; the API returns only their names (customHeaderNames). Transport and content headers such as Host, Content-Type, and User-Agent, and the webhook’s own X-Componecat-Signature and X-Webhook-* headers, can’t be set. Don’t name a custom header webhook-id, webhook-timestamp, or webhook-signature either: those carry the signature.

Detail level The payload carries
SUMMARY (default) Which record changed and how — its type, id, name, and action, plus the changed field names for an update. Fetch the record through the API if you need more.
FULL Everything in SUMMARY, plus data.entity: a snapshot of the record as it is when the event is delivered.

deleted events never carry a snapshot, whatever the detail level — the record is gone.

Every event is a CloudEvents 1.0 event in structured mode, sent with Content-Type: application/cloudevents+json:

{
"specversion": "1.0",
"id": "5f0c2b8e7d6a4e1f9a3b2c1d0e9f8a7b",
"source": "componecat://organizations/550e8400-e29b-41d4-a716-446655440000",
"type": "componecat.catalog-entity.updated",
"time": "2026-09-27T10:30:00.0000000+00:00",
"datacontenttype": "application/json",
"subject": "CatalogEntity/7c9e6679-7425-40de-944b-e07fc1f90ae7",
"data": {
"entityType": "CatalogEntity",
"entityId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"entityName": "payments-api",
"action": "updated",
"organizationId": "550e8400-e29b-41d4-a716-446655440000",
"userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"changedFields": ["lifecycle", "description"],
"entity": { "…": "FULL detail level only — see below" }
}
}
Field Description
id Unique event id. Deliveries are at-least-once, so use it to ignore duplicates.
source componecat://organizations/{organizationId}
type componecat.{record-type}.{action} — see Events.
time When the change happened (ISO 8601).
subject {entityType}/{entityId}
data.entityType The record type — see Events.
data.entityId The record’s id. For ScorecardEvaluation, this is the evaluation’s id; the entity’s id is data.entity.catalogEntityId.
data.entityName The record’s name, when it has one.
data.action created, updated, deleted, or evaluated.
data.organizationId Your organization’s id.
data.userId The user who made the change. Absent for changes made by Componecat itself, such as a sync or scan.
data.changedFields updated only: the names of the fields that changed.
data.entity FULL only, not on deleted: the record’s snapshot.
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"name": "payments-api",
"namespace": "payments",
"displayName": "Payments API",
"description": "Card and wallet payments.",
"lifecycle": "Production",
"tags": ["pci"],
"fields": { "tier": "1" },
"kindKey": "service",
"kindDisplayName": "Service",
"ownerId": "3f2e6679-7425-40de-944b-e07fc1f90ae7",
"ownerName": "Payments Team",
"ownerExternalId": "a1b2c3d4-team",
"ownerExternalSource": "Atlassian",
"repository": "https://github.com/acme/payments-api",
"languages": ["C#"],
"createdAt": "2026-04-11T10:30:00.0000000+00:00",
"updatedAt": "2026-09-27T10:30:00.0000000+00:00"
}

ownerId and ownerName identify the owning team. For a team synced from an external system, ownerExternalId and ownerExternalSource are the team’s id in that system and the system’s name, so you can map owners onto your own records without a lookup; both are null for a team created in Componecat. Renaming a team or changing its external id sends a Team updated event — reassigning an entity to a different team sends a CatalogEntity updated event. Moving a team’s entities in bulk sends one such event per entity.

{
"evaluationId": "9c3e6679-7425-40de-944b-e07fc1f90ae7",
"scorecardId": "1c3e6679-7425-40de-944b-e07fc1f90ae7",
"scorecardName": "production-readiness",
"scorecardDisplayName": "Production Readiness",
"catalogEntityId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"catalogEntityName": "payments-api",
"catalogEntityDisplayName": "Payments API",
"score": 80,
"previousScore": 100,
"attainedLevel": "Silver",
"previousLevel": "Gold",
"passedRuleCount": 4,
"totalRuleCount": 5,
"evaluatedAt": "2026-09-27T10:30:00.0000000+00:00",
"failingRules": [
{
"name": "has-runbook",
"failureMessage": "Add a runbook",
"remediationUrl": "https://docs.example.com/runbooks",
"detail": null
}
]
}

previousScore and previousLevel are null on an entity’s first evaluation against the scorecard.

{
"id": "5d2e6679-7425-40de-944b-e07fc1f90ae7",
"slug": "guides/onboarding",
"title": "Onboarding",
"sourcePath": null,
"referenceCount": 2,
"unresolvedReferenceCount": 1,
"references": [
{ "targetType": "Team", "targetId": null, "rawRef": "platform", "origin": "Tag" },
{ "targetType": "Entity", "targetId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "rawRef": "service://payments/api", "origin": "Inline" }
],
"createdAt": "2026-09-27T10:30:00.0000000+00:00",
"updatedAt": "2026-09-27T10:30:00.0000000+00:00"
}

The snapshot leaves out the page body; fetch the page through the API if you need it. references lists what the page refers to, as written: targetType is Entity, Team, EntityKind, RelationshipKind, FieldDefinition, Collection, Scorecard, or WikiPage, and origin says whether the reference is one of the page’s tags or is inline in the body. targetId is null for a reference that didn’t match anything when the page was last saved or synced. sourcePath is the file the page was synced from, or null for a page written in Componecat.

Interfaces, documentation sections, documentation links, and relationships snapshot the record’s stored fields as camelCase properties — its id, createdAt, and updatedAt alongside the fields specific to the record type.

Header Value
Content-Type application/cloudevents+json
webhook-id The event id. The same on every retry of the event.
webhook-timestamp When this attempt was sent, in Unix seconds
webhook-signature v1, followed by the base64 Standard Webhooks signature
X-Componecat-Signature sha256= followed by the lowercase hex HMAC-SHA256 of the request body
X-Webhook-Event The event type, e.g. componecat.catalog-entity.updated
X-Webhook-Delivery The event id
User-Agent Componecat-Webhooks/1.0

Every request carries two signatures made with the webhook’s secret. Check either one before trusting a request. Prefer the Standard Webhooks signature: it covers a timestamp, so you can also reject a replayed request.

Deliveries follow the Standard Webhooks specification, so any of its libraries can verify them. Give the library whsec_ followed by the webhook’s secret:

// Node.js: npm install standardwebhooks
import { Webhook } from 'standardwebhooks';
const webhook = new Webhook(`whsec_${secret}`);
// Throws if the signature doesn't match or the timestamp is too old.
const event = webhook.verify(rawBody, {
'webhook-id': headers['webhook-id'],
'webhook-timestamp': headers['webhook-timestamp'],
'webhook-signature': headers['webhook-signature'],
});
# Python: pip install standardwebhooks
from standardwebhooks.webhooks import Webhook
webhook = Webhook(f"whsec_{secret}")
# Raises if the signature doesn't match or the timestamp is too old.
event = webhook.verify(raw_body, headers)

To verify by hand:

  1. Base64-decode the webhook’s secret. The result is the signing key.
  2. Join the webhook-id header, a ., the webhook-timestamp header, a ., and the raw request body: the bytes as received, before parsing the JSON.
  3. Compute the HMAC-SHA256 of that string with the key, and base64-encode it.
  4. The webhook-signature header holds one or more space-separated signatures, each prefixed with v1,. Accept the request if one of them matches, comparing in constant time.
  5. Reject the request if webhook-timestamp is more than a few minutes away from your clock.

webhook-id is the event id, the same on every retry, so you can also use it to skip an event you’ve already handled.

The original signature, kept so existing receivers keep working. It covers only the body, with no timestamp, so it can’t tell a replayed request from a new one.

  1. Read the X-Componecat-Signature header and strip the sha256= prefix.
  2. Compute the HMAC-SHA256 of the raw request body, keyed with the secret exactly as shown (not base64-decoded).
  3. Compare the two in constant time, and reject the request if they differ.
Node.js
const crypto = require('node:crypto');
function verify(rawBody, secret, header) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const received = (header || '').replace(/^sha256=/, '');
return received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'));
}
# Python
import hashlib, hmac
def verify(raw_body: bytes, secret: str, header: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, (header or "").removeprefix("sha256="))

When you rotate the secret, the new one signs every delivery from then on, for both signatures.

  • Timing. Deliveries are sent from a background queue after the change is saved, not during the request that made it, so each one arrives a short while after the change.
  • Success is any 2xx response within 10 seconds.
  • Retries. A failed delivery is retried after 30 seconds, 2 minutes, 10 minutes, 1 hour, and 6 hours, then recorded as failed.
  • At least once. An event can arrive more than once, and events can arrive out of order — deduplicate on id (also sent as webhook-id) and order by time.
  • Rate limit. Each webhook receives up to 100 deliveries a minute. Deliveries over the limit are held back and sent shortly after, not dropped.
  • Degraded webhooks. After a long run of consecutive failures, a webhook’s status becomes DEGRADED and it stops receiving events. Fix the endpoint, then set the status back to ACTIVE. A PAUSED webhook also receives nothing until you make it active again.
  • History. Each webhook’s delivery history is kept for 30 days.