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.
Managing webhooks
Section titled “Managing webhooks”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.
Events
Section titled “Events”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.
Choosing which events a webhook receives
Section titled “Choosing which events a webhook receives”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.
Custom headers
Section titled “Custom headers”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
Section titled “Detail level”| 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.
Payload
Section titled “Payload”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. |
Catalog entity snapshot
Section titled “Catalog entity 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.
Scorecard evaluation snapshot
Section titled “Scorecard evaluation snapshot”{ "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.
Wiki page snapshot
Section titled “Wiki page snapshot”{ "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.
Other snapshots
Section titled “Other snapshots”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.
Headers
Section titled “Headers”| 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 |
Verifying the signature
Section titled “Verifying the signature”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.
Standard Webhooks
Section titled “Standard Webhooks”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 standardwebhooksimport { 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 standardwebhooksfrom 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:
- Base64-decode the webhook’s secret. The result is the signing key.
- Join the
webhook-idheader, a., thewebhook-timestampheader, a., and the raw request body: the bytes as received, before parsing the JSON. - Compute the HMAC-SHA256 of that string with the key, and base64-encode it.
- The
webhook-signatureheader holds one or more space-separated signatures, each prefixed withv1,. Accept the request if one of them matches, comparing in constant time. - Reject the request if
webhook-timestampis 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.
X-Componecat-Signature
Section titled “X-Componecat-Signature”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.
- Read the
X-Componecat-Signatureheader and strip thesha256=prefix. - Compute the HMAC-SHA256 of the raw request body, keyed with the secret exactly as shown (not base64-decoded).
- Compare the two in constant time, and reject the request if they differ.
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'));}# Pythonimport 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.
Delivery
Section titled “Delivery”- 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
2xxresponse 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 aswebhook-id) and order bytime. - 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
DEGRADEDand it stops receiving events. Fix the endpoint, then set the status back toACTIVE. APAUSEDwebhook also receives nothing until you make it active again. - History. Each webhook’s delivery history is kept for 30 days.