> ## Documentation Index
> Fetch the complete documentation index at: https://www.composer.id/llms.txt
> Use this file to discover all available pages before exploring further.

# Events and the timeline

> The append-only timeline behind every Intent ID, the verified webhooks that feed it in both directions, and reconciliation after go-live.

This page covers the append-only timeline behind every Intent ID, how destination events reach it, how composerID signs its own deliveries, and how drift is detected after go-live. Read it if you operate a destination integration or consume composerID's webhooks.

```mermaid theme={"system"}
sequenceDiagram
  participant D as Destination platform
  participant W as composerID webhook endpoint
  participant T as Intent timeline
  D->>W: POST /webhooks/{platform} with X-Signature-HMAC-SHA256-{n}
  W->>W: HMAC-SHA256 over the raw body, every active key, constant time
  alt no key matches
    W-->>D: 400 rejected, not queued
  else verified
    W->>W: resolve intent_id, reject if unknown
    W->>T: append webhook.received
    W-->>D: 202 WebhookAck (ok, intent_id, provider)
  end
```

## The timeline

Every step composerID takes is written as an event with a `type`, a timestamp (`at`) and an `actor` (a user id, `system` or `webhook`), plus the `version` of the intent at the time. The `provider` is present on stamp, link and publish events, and the `external_id` on stamp and link events. Events are never edited or deleted; a correction is a new event.

`GET /intent/{intent_id}/timeline` returns them in order as `{intent_id, events[]}`. Scope: `intents:read`.

| Family         | Event types                                                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Intent         | `intent.created`, `intent.updated`, `intent.version_bumped`, `intent.stamped`, `intent.linked`, `intent.published`, `intent.failed`, `intent.cancelled` |
| Enrichment     | `enrichment.requested`, `enrichment.fulfilled`                                                                                                          |
| Reconciliation | `reconciliation.detected`, `reconciliation.resolved`                                                                                                    |
| Inbound        | `webhook.received`                                                                                                                                      |

The `TimelineEvent` schema fixes this list. `intent.stamped` records the Intent ID being written into a destination record; `intent.linked` records a linkage kept on the composerID side only.

## Webhooks in: destinations

Where a destination pushes events, composerID subscribes to the start of each lifecycle it cares about and lands each verified delivery on the timeline against the Intent ID.

<Steps>
  <Step title="Receive">
    The destination posts to `POST /webhooks/{platform}`. No bearer token: the caller is a destination platform, so the delivery authenticates by its signature header instead. It is the only route authenticated by signature rather than by a bearer token; the other routes that declare no bearer token, `POST /oauth/token` and `POST /sandbox/keys`, issue credentials rather than touch an intent.
  </Step>

  <Step title="Verify">
    composerID computes HMAC-SHA256 over the raw request body with every active signing key and compares in constant time against every `X-Signature-HMAC-SHA256-{n}` header supplied. A delivery with no header, or one no active key matches, is rejected with `400`, not queued.
  </Step>

  <Step title="Correlate">
    The body must reference an Intent ID this tenant minted; an unknown destination or a foreign `intent_id` returns `404`. The `WebhookEvent` envelope also declares a `delivery_id` for de-duplication, because platforms document that duplicates can occur: it is the key a receiver discards repeats on, and the Beeline receiver de-duplicates on the delivery's event `id`.
  </Step>

  <Step title="Record">
    The event lands on the intent's timeline as `webhook.received` and drives back-sync. The response is `202` with `{ok: true, intent_id, provider}`.
  </Step>
</Steps>

<AccordionGroup>
  <Accordion title="Destination-specific receivers" description="What a platform adds on top of the generic endpoint" icon="plug">
    Some platforms need more than the generic route. The Beeline receiver completes the `webhook.validation` handshake that keeps a new subscription `pending` until its validation code is echoed back, de-duplicates on the event `id`, and correlates Beeline record identifiers such as `assignmentId` to the Intent ID from publish receipts. Where a destination offers an event-history endpoint, as Beeline does, recovery after an outage sweeps history through the same ingestion path rather than hoping the push arrived.
  </Accordion>
</AccordionGroup>

## Webhooks out: composerID

The Intent API document declares the events composerID delivers to an endpoint you register, using the same `WebhookEvent` envelope: `event`, `intent_id`, `intent_version`, `provider`, `external_id`, `timestamp`, `payload` and `delivery_id`. Four are declared under webhooks:

* `vms.requisition.updated`: a requisition changed in the destination system
* `vms.assignment.updated`: an assignment changed
* `vms.timesheet.updated`: a timesheet changed
* `vms.connection.revoked`: a tenant's authorisation was revoked

Each is signed with HMAC-SHA256 over the raw body under the convention below, and an unverifiable delivery is rejected rather than queued.

<Info>
  Publish confirmations, drift and enrichment gaps are not among the declared outbound events. You read them from the API instead: the `201` receipt from `POST /publish`, the drift report from `POST /reconcile`, the `422` `enrichment_request` from preflight, and the timeline events behind all three.
</Info>

<Info>
  The current event taxonomy is VMS-scoped. Additional event families (assessment, document, workflow) are planned and will use the same envelope and signing convention.
</Info>

## One signing convention

Outbound deliveries are signed the way the [Manuscript API](/api-reference/manuscript/overview) signs its own.

* One or more `X-Signature-HMAC-SHA256-{n}` headers. The numeric suffix is an identifier only and maps to no particular key.
* Each value is the Base64-encoded HMAC-SHA256 digest of the raw request body under one active signing key. Never digest a parsed or re-serialised payload.
* Several keys are active during rotation, so a request may carry several headers. A request is valid if any computed digest matches any supplied header.
* Comparison is constant time. A request with no valid signature is rejected, never queued.
* The signature travels in the headers, never in the body, so the signed bytes are exactly the bytes received. The body's `signature` field is deprecated and ignored.

<Check>
  One convention means one verifier for everything that arrives from Deployed: the reference implementation is `adapter/webhook_verify.py`, its rotation rules run as tests, and the comparison itself uses `hmac.compare_digest`.
</Check>

## Reconciliation

`POST /reconcile` reads a destination record back and compares it, field by field through the mapping profile, with the payload that was published, not with the intent as it stands today. The request names the `intent_id` and `platform`, plus the `external_id` for records composerID linked rather than created. Scope: `reconcile`.

The response is a drift report: `intent_id`, `platform`, `external_id` and a `drift` array in which each entry names the destination `field`, the value `published` and the value `current`. An empty array means the record is in sync.

<Info>
  Drift is the destination no longer matching what was written to it. A pending amendment on composerID's side is not drift. Each check that finds drift puts a `reconciliation.detected` event on the timeline; its resolution is a `reconciliation.resolved` event.
</Info>

<Info>
  Inbound verification, the timeline and reconciliation are implemented in the reference sandbox. Outbound delivery is documented contract only: the sandbox verifies and records what arrives, it does not emit the `vms.*` events to a registered endpoint. The destination-specific receivers, such as the Beeline receiver with its validation handshake and event-history sweep, are proven against specification-faithful fakes; a live subscription is confirmed per tenant at onboarding.
</Info>

## Next steps

<Columns cols={3}>
  <Card title="Manuscript API webhooks" icon="radio" href="/api-reference/manuscript/webhooks">
    The signing rules as the manuscript service documents them.
  </Card>

  <Card title="Beeline guide" icon="plug" href="/guides/beeline">
    Subscription, handshake and event-history sweep against a fake tenant.
  </Card>

  <Card title="The Compliance File" icon="shield-check" href="/concepts/compliance-file">
    How timeline events become the evidence pack behind a decision.
  </Card>
</Columns>
