Skip to content

Events & webhooks

The platform is event-driven: whenever something meaningful happens — a Resource is written, a Contract changes, a Product is published — the service responsible announces it as an event. Other services and your own integrations react to those announcements instead of polling an API on a timer. This page is the guide to consuming them: what the events are called, which ones you are allowed to build on, how they are delivered, and how they reach no-code tools such as Zapier and n8n as webhooks.

Events come in three tiers. Platform events (platform.*) are a stable contract: any platform service may subscribe, and their payloads only change in backwards-compatible ways within a major version. Internal events (internal.*) are a private implementation detail of the service that emits them — they chain that service's own workflows, can change without notice, and are never something you subscribe to. External events (packt.*) are how the platform reaches tools outside it — Zapier, n8n, webhook endpoints you host — through the Eventing service's curated catalogue, delivered as CloudEvents (see Webhooks and no-code tools). Inside the platform, everything you can integrate with is on the platform tier.

Each publishing service owns a platform bus named platform-<service> (for example platform-content-lake). You attach a target — an AWS Lambda, an SQS queue, another bus — to the events you care about, filtered by name or pattern. Producers never know who is listening, so a contract change quietly ripples out to rights across the estate without anyone wiring services together by hand.

A quick worked example: an author's manuscript is ingested, and the Content Lake publishes platform.content-lake.resource.v1.created for each chapter. Your integration is subscribed, so it registers the new content in a downstream catalogue and pings an editor in Slack — all without the Content Lake knowing your integration exists.

How events flow

Internally the platform speaks Amazon EventBridge. Events that reach tools outside the platform — a webhook, Zapier, n8n — are a separate, curated set: the Eventing service watches the platform buses and publishes external events as CloudEvents at the egress. An external event has its own name (packt.*) and its own payload, designed for workflow builders — it is not the internal event re-wrapped. Everything inside stays EventBridge-native.

flowchart LR
    subgraph inside [Inside the platform]
        direction LR
        SVC["A service<br/>(Content Lake, Contracts, Products)"]
        SVC -->|publishes| BUS["Platform bus<br/>platform-&lt;service&gt;"]
        BUS -->|filtered subscription| T["Your target<br/>Lambda · SQS · bus"]
    end
    BUS -->|watches| EV["Eventing service<br/>external catalogue"]
    EV -->|CloudEvents webhook| EXT["Your no-code<br/>automation"]

Read left to right: a service publishes to its platform bus; internal consumers (including your own AWS targets) subscribe directly to the EventBridge event; and tools outside the platform receive external catalogue events, published as CloudEvents by the Eventing service.

The naming convention

Platform event names are dot-separated and versioned:

platform.<service>.<entity>.v<major>.<verb>
  • packt — the fixed prefix for platform events. Private events use internal. instead, so the two are never confused.
  • <service> — the publishing service, e.g. content-lake, products.
  • <entity> — the domain entity the event is about, e.g. resource, work, product, pricing.
  • v<major> — the major version of the payload schema. Payloads evolve backwards-compatibly within a major version; a breaking change ships under a new major.
  • <verb> — what happened, in the past tense, e.g. created, updated, published, rights_changed.

Some examples you will meet below:

platform.content-lake.resource.v1.created
platform.content-lake.work.v1.rights_changed
platform.products.product.v1.published
platform.products.pricing.v1.updated

Contracts are a fold-in special case

Contract events drop the <entity> segment: they are named platform.contract.v1.<action> — for example platform.contract.v1.rights_changed — rather than platform.contracts.contract.v1.…. This is the one deliberate exception to the grammar above. Everything else follows the full five-part form.

Match on the event name (the detail-type) rather than parsing it — EventBridge pattern rules let you subscribe to, say, every platform.products.product.v1.* transition with one filter.

Platform, internal, external

Platform Internal External
Name prefix platform. internal. packt.
Bus platform-<service> internal-<service> None — delivered to your endpoint
Who may subscribe Any platform service Only the owning service Anyone with a subscription (webhook)
Stability A contract; versioned changes only Changes without notice A contract, owned by the Eventing service
Purpose Notify the platform something happened Chain the service's own workflows Feed workflows in tools outside the platform

Inside the platform, build only on the platform column. The private buses exist so a service can trigger its own downstream processing — search indexing, entity enrichment, image optimisation — and they are not consumable contracts. The external column is a different world: its events never appear on a platform bus, and platform events never reach a webhook — the two meet only inside the Eventing service.

Ingestion publishes outcomes, never stages

Ingestion is a special case worth calling out. It publishes no per-stage events: stage handoff travels its internal queues, and its private internal-content-lake-ingestion bus carries only a diagnostics stream (observation.v1.recorded) — do not subscribe to it. The signals that ingestion has happened are the run-outcome events on platform-content-lake-ingestion (resource.v1.ingested, resource.v1.failed, job.v1.completed) and the Content Lake's own lifecycle events: source.created, resource.created, plus work.rights_changed once the content is placed under a Contract.

The event envelope

Every event carries the EventBridge envelope. The event name is the detail-type; the publisher is identified by source (for example packt/svc-content-lake); and the domain payload is the detail object. Inside detail, three fields are always present, and enum fields carry their full protobuf identifiers on the wire (for example RESOURCE_TYPE_DOCUMENT, never DOCUMENT) — match those exact strings in your filters:

Field Meaning
tenant_id The Tenant the event belongs to. (Knowledge Graph events are the sole exception — see below.)
event_id A unique ID for this event, used for deduplication.
occurred_at When it happened, ISO 8601 GMT with a Z suffix.

The rest of detail is specific to the event. A resource.created looks like this:

{
  "source": "packt/svc-content-lake",
  "detail-type": "platform.content-lake.resource.v1.created",
  "detail": {
    "event_id": "e_resource_created",
    "tenant_id": "packt",

    "resource_id": "res_ml_go_ch3",
    "work_id": "wk_ml_go",
    "version": null,
    "type": "RESOURCE_TYPE_DOCUMENT",
    "title": "Chapter 3 — Goroutines and Channels",
    "uri": "cl://packt/document/res_ml_go_ch3/latest",

    "source_id": "src_ml_go_manuscript",
    "generated_by": "act_ingest_ml_go",
    "language": "en",
    "tags": ["go", "concurrency", "chapter"],

    "occurred_at": "2026-06-20T09:12:00Z",
  },
}

Payloads are deliberately lean

Events tell you that something changed and give you the IDs to act on it — they are a trigger, not the transport of record. To read the full current state, call the Public API with the IDs from the payload. Contract events in particular are thin: they never carry term content, so a consumer fetches the terms it needs rather than trusting a payload snapshot. This keeps events small and means a slow consumer always catches up to the truth rather than replaying stale data.

At-least-once delivery and idempotent consumers

Delivery is at-least-once: the platform guarantees an event will reach your consumer, but it may occasionally arrive more than once — after a retry, say, or a redelivery. Your consumer must therefore be idempotent: processing the same event twice must have the same effect as processing it once.

The simple recipe is to deduplicate on event_id. Keep a record of the event_id values you have already handled, and skip any you have seen before:

def handle(event):
    detail = event["detail"]
    if already_processed(detail["event_id"]):
        return  # duplicate — safe to ignore
    do_the_work(detail)
    mark_processed(detail["event_id"])

Order is not guaranteed either

Two events for the same entity can arrive out of order. Where ordering matters, use the version or timestamp fields in the payload — for example, ignore a resource.updated whose version is older than one you have already applied — rather than assuming arrival order reflects the order things happened. Attach a dead-letter queue to your subscription so a handler that keeps failing parks the event for inspection instead of blocking the stream.

The event catalogue

The tables below are the platform events you can subscribe to today, grouped by publishing service. Each is regenerated from the owning service's specification; that spec is always the source of truth for the full payload schema.

Content Lake — platform-content-lake

Published when Resources, Works, ContentUnits, Activities, or Sources change. Any service may subscribe.

Event detail-type Published when
Resource created platform.content-lake.resource.v1.created A new Resource is written (by ingestion, derivation, or direct creation).
Resource updated platform.content-lake.resource.v1.updated A Resource is snapshotted, re-tagged, has metadata changed, or is enriched (change_type: RESOURCE_CHANGE_TYPE_VERSION, RESOURCE_CHANGE_TYPE_METADATA, RESOURCE_CHANGE_TYPE_ENRICHMENT, RESOURCE_CHANGE_TYPE_ACCESS).
Resource deleted platform.content-lake.resource.v1.deleted A Resource version is removed or a whole Resource is purged (reason: DELETE_REASON_COMPACTION, DELETE_REASON_EXPLICIT, DELETE_REASON_PURGED).
Work created platform.content-lake.work.v1.created A new Work (IP entity) is registered.
Work rights changed platform.content-lake.work.v1.rights_changed The effective rights on a Work change — contract assignment, state change, or amendment. This is the platform signal that rights moved.
ContentUnit created platform.content-lake.content_unit.v1.created A new ContentUnit (chapter, section, segment) is registered.
Source created platform.content-lake.source.v1.created A new Source entity is registered — the platform signal that ingestion of a new artifact has begun.
Source split platform.content-lake.source.v1.split Ingestion split one Source into multiple Resources — the file → chapters mapping in a single event.
Embedding generated platform.content-lake.embedding.v1.generated Embeddings were generated for a Resource in an embedding space — an announcement to reuse them (vectors are fetched by reference, never carried on the bus).
Entity discovered platform.content-lake.entity.v1.discovered Entity extraction found a new entity instance (a previously unseen author, repository, technology) with no Knowledge Graph match yet.
Activity logged platform.content-lake.activity.v1.logged Any Activity is recorded — the canonical stream for audit, AI-usage dashboards, and royalty triggers.

The two rights events pair up: a change to a Contract triggers the Content Lake to re-resolve the affected Work's rights and publish work.rights_changed, which is your cue to recompute anything that depends on what may be done with that Work's content.

work.rights_changed (EventBridge detail)
{
  "source": "packt/svc-content-lake",
  "detail-type": "platform.content-lake.work.v1.rights_changed",
  "detail": {
    "event_id": "e_work_rights_changed",
    "tenant_id": "packt",

    "work_id": "wk_ml_go",
    "contract_id": "ctr_chandra_2026",
    "contract_version": 3,
    "change_types": ["WORK_RIGHTS_CHANGE_TYPE_TERRITORIAL"],

    "occurred_at": "2026-04-01T12:00:00Z"
  }
}

change_types[] lists every affected family in one event: WORK_RIGHTS_CHANGE_TYPE_TERRITORIAL, WORK_RIGHTS_CHANGE_TYPE_MANIFESTATION, WORK_RIGHTS_CHANGE_TYPE_USAGE, WORK_RIGHTS_CHANGE_TYPE_ROYALTY, WORK_RIGHTS_CHANGE_TYPE_STATE, WORK_RIGHTS_CHANGE_TYPE_ASSIGNMENT. Recompute the resolved rights for every Resource under the Work and re-evaluate downstream availability.

Contracts — platform-contracts

Published by the Contracts service on every contract lifecycle event. Their primary consumer is the Content Lake, which reprojects rights on receipt — but any service may subscribe. Payloads are thin: IDs, version, and change_types only, never term content. Fetch terms from the Public API when you need them.

Event detail-type Published when
Created platform.contract.v1.created A new contract is created (version 1).
Version created platform.contract.v1.version_created A new version is created — an addendum, update, or automatic renewal.
State changed platform.contract.v1.state_changed A status transition (e.g. CONTRACT_STATUS_DRAFTCONTRACT_STATUS_ACTIVE, or the expiry sweep).
Rights changed platform.contract.v1.rights_changed A term that affects resolved rights changes — territorial, manifestation, usage, handling, royalty, duration, or Work assignment.
Renewal approaching platform.contract.v1.renewal_approaching An auto-renewing contract reaches its notice cutoff. Aimed at licensing-staff alerting and CRM integrations, not the Content Lake.

Contract events carry the contract's direction (CONTRACT_DIRECTION_INBOUND or CONTRACT_DIRECTION_OUTBOUND). The Content Lake ignores CONTRACT_DIRECTION_OUTBOUND events entirely — outbound contracts never affect resolved rights — and so should any consumer computing what may be done with content.

contract.rights_changed (EventBridge detail)
{
  "source": "packt/svc-contracts",
  "detail-type": "platform.contract.v1.rights_changed",
  "detail": {
    "event_id": "e_contract_rights_changed",
    "tenant_id": "packt",

    "contract_id": "ctr_chandra_2026",
    "version": 3,
    "change_types": ["RIGHTS_CHANGE_TYPE_TERRITORIAL", "RIGHTS_CHANGE_TYPE_USAGE"],
    "direction": "CONTRACT_DIRECTION_INBOUND",
    "work_ids": ["wk_ml_go"],

    "occurred_at": "2026-04-01T12:00:00Z"
  }
}

change_types[] names every term family the amendment touched: RIGHTS_CHANGE_TYPE_TERRITORIAL, RIGHTS_CHANGE_TYPE_MANIFESTATION, RIGHTS_CHANGE_TYPE_USAGE, RIGHTS_CHANGE_TYPE_HANDLING, RIGHTS_CHANGE_TYPE_ROYALTY, RIGHTS_CHANGE_TYPE_DURATION, RIGHTS_CHANGE_TYPE_WORKS (Work assignment changed). When it includes RIGHTS_CHANGE_TYPE_WORKS, work_ids carries every affected Work — the union of the assignment set before and after the change — so a Work removed from the contract is reprojected too.

Usage — platform-usage

Consumption records, on their own high-volume bus. Every surface that serves content — the Public API, MCP tools, assistants — emits one platform.usage.content.v1.served event per served response, enumerating every piece of content the response drew on: the Resource and pinned version, its Work, whether it was shown or only consulted, whether it was cited, and the contract basis it was served under. This is the stream royalty attribution and AI-usage reporting are built on. An MCP answer that drew on 37 passages is one event with 37 items — the whole response, kept together.

The event contract is owned by the forthcoming Royalty service; treat the payload as append-only facts (what was served) rather than computed outcomes (what is owed) — the owing is derived downstream, so the division policy can be tuned without rewriting history.

Product Management — platform-products

Published on every Product lifecycle transition and on pricing, composition, and catalogue changes. The Distribution service is a major consumer, but any service may subscribe.

Event detail-type Published when
Product created platform.products.product.v1.created A new Product is created.
Product updated platform.products.product.v1.updated Product metadata changes.
Product deleted platform.products.product.v1.deleted A Product is removed.
Drafted platform.products.product.v1.drafted A Product enters DRAFT.
Submitted platform.products.product.v1.submitted A Product enters REVIEW.
Approved platform.products.product.v1.approved A Product enters APPROVED.
Published platform.products.product.v1.published A Product enters PUBLISHED.
Retired platform.products.product.v1.retired A Product enters RETIRED (terminal).
Withdrawn platform.products.product.v1.withdrawn A Product enters WITHDRAWN (terminal).
Pricing updated platform.products.pricing.v1.updated A price changes (base or a territory).
Content attached platform.products.content.v1.attached A composition node referencing Content Lake content is added.
Content detached platform.products.content.v1.detached A composition node is removed.
Content updated platform.products.content.v1.updated Referenced Content Lake content changes, or a node is re-pinned.
Manifestation asset uploaded platform.products.manifestation.v1.asset_uploaded An artifact is uploaded or replaced for a Manifestation's format.
Promotion created platform.products.promotion.v1.created A new promotion is created.
Promotion started platform.products.promotion.v1.started A promotion activates.
Promotion ended platform.products.promotion.v1.ended A promotion expires.
Catalogue created platform.products.catalogue.v1.created A new Catalogue is created.
Catalogue updated platform.products.catalogue.v1.updated Catalogue rules or metadata change.
Catalogue archived platform.products.catalogue.v1.archived A Catalogue is archived.
Catalogue product added platform.products.catalogue.v1.product_added A Product enters a Catalogue (manual pick or rule match).
Catalogue product removed platform.products.catalogue.v1.product_removed A Product leaves a Catalogue.

Note the distinction between product.v1.updated (product metadata changed) and content.v1.updated (the underlying Content Lake content a latest-tracking node points at changed). Subscribe to both if you need to react to any change to a Product.

Knowledge Graph — platform-content-lake-knowledge

The Knowledge Graph publishes entity lifecycle and metadata-refresh events (for example when a GitHub repository's stats or a technology's release information changes).

Knowledge Graph events carry no tenant_id

The Knowledge Graph is a global registry shared across tenants, so its events are the one exception to the rule that every payload carries tenant_id. A consumer that references an entity fans the event out itself, one action per tenant that references it. Do not expect a tenant_id field on these events.

Webhooks and no-code tools

Everything above assumes an AWS target inside the platform. To receive events outside the platform — in a webhook endpoint you host, or in a no-code automation tool — the Eventing service is the plug-in point. It watches the platform buses and publishes a curated catalogue of external events, designed for workflow builders.

The egress is upcoming

The Eventing service's external egress is specified but not yet built. What follows is the contract it will ship against; catalogue entries may still be refined until it does.

External events are not platform events re-wrapped:

  • Names follow packt.<entity>.v<major>.<verb> — for example packt.order.v1.placed, packt.product.v1.published, packt.contract.v1.renewal_approaching. They name business entities, not internal services, and never reuse a platform event name — internal services can be split or renamed without your workflow noticing.
  • Payloads are hand-authored for workflows: IDs and the facts you filter or branch on — never an internal payload passed through, and never personal data. A workflow that needs more calls back through the Public API with its own credentials.
  • Versions are independent of the platform events behind an entry: an internal v1v2 migration never breaks your Zap.

Deliveries use the CloudEvents v1.0 envelope — the open, widely-supported format that tools like Zapier and n8n understand out of the box:

An external event as delivered to your webhook
{
  "specversion": "1.0",
  "id": "0197f3a2-4b1c-7d3e-9a4f-2c8d1e5b6a70",
  "type": "packt.order.v1.placed",
  "source": "https://events.packtpub.services",
  "subject": "ord_9f4e2a71",
  "time": "2026-07-17T09:41:00Z",
  "datacontenttype": "application/json",
  "data": {
    "tenant_id": "packt",
    "order_id": "ord_9f4e2a71",
    "customer_id": "u_5c21d0aa",
    "vertical": "gamedev",
    "product_ids": ["prod_0197c1f4"],
    "channel": "shopify",
    "currency": "GBP",
    "total": "39.99"
  }
}

The prefixed IDs under data feed straight back into the Public API. The subject names the primary entity, so simple tools can route on it without parsing the payload.

The at-least-once and idempotency rules apply to webhook delivery too: deduplicate on the id — it is stable across every redelivery of the same occurrence — and return a 2xx quickly so the Eventing service does not retry a delivery your handler already accepted.

See No-code & AI tools for connecting Zapier, n8n, and LLM agents end to end.

Verifying deliveries

Inside the platform, publishing is locked down with IAM: only the owning service can put events onto its bus, and only under its own source value. The source field on every event you receive is therefore trustworthy as-is — you never need to second-guess which service produced an event.

For webhook deliveries to your own endpoints:

  • Deliveries authenticate to your endpoint with the credentials you configure on the destination — an API key header or an OAuth client. Treat possession of those credentials as proof the delivery came from the platform, and rotate them like any other secret.
  • Payload signing arrives with the Eventing service: each delivery will carry a timestamped HMAC signature header computed with a per-destination secret, so you can verify origin and integrity even if your endpoint URL leaks. The scheme will be documented here when it ships.
  • EventBridge itself does not sign deliveries — do not build against an x-eventbridge-signature header. Verify by credentials today, and by the platform signature once it is available.
  • The Public API — read the full state an event points at, using the IDs in the payload.
  • Authentication — the WorkOS tokens your consumers and API calls carry.
  • No-code & AI tools — wire events into Zapier, n8n, and AI agents as webhooks.
  • Contracts & Rights — where contract events originate and how rights resolve.
  • Content Lake — the estate whose lifecycle events are the platform signal of ingestion.
  • Explorer — Chapter I — the service map and how events tie the platform together.