Event Subscriptions
Event subscriptions let a project react to platform events. A subscription matches events by category, action, resource type, and an optional JSONLogic condition, then delivers each matching event to a workflow, agent, process, or webhook.
Use subscriptions for integrations such as:
- notifying an external system when a document is created or updated
- starting a workflow, agent, or process when a content object changes
- routing content events differently for documents, images, audio, or video
- reacting to events pushed in from external systems (Salesforce, GitHub, ...) through an ingest channel
- building lightweight project automations without polling
Where to manage subscriptions
In Studio, open Store and select Event Bus. The page has these main views:
- Subscriptions: create, edit, duplicate, disable, or delete user-managed subscriptions.
- Webhooks: the subset of subscriptions whose target is an outbound webhook.
- Ingest Channels: create inbound channels that let external systems push events into the bus.
- Deliveries: inspect matched events and delivery state for workflow, agent, process, and webhook targets.
System subscriptions are shown for visibility when relevant, but protected system subscriptions cannot be edited or deleted. Projects that were migrated from legacy customized system rules may show a protected Override subscription. That override replaces the Vertesia-provided system subscription for the project instead of running beside it.
Event model
Every event has a stable envelope:
{
"event_id": "01HF7YAT00ABCDEFGHJKMNPQRS",
"event_category": "content",
"action": "create",
"resource_type": "content_object",
"resource_id": "object-id",
"account_id": "account-id",
"project_id": "project-id",
"tenant_id": "tenant-id",
"timestamp": "2026-05-23T00:00:00.000Z",
"source": "zeno-server",
"resource_data": {
"metadata": {
"type": "document"
}
}
}
The main event categories are:
| Category | Typical use |
|---|---|
content | content object create, update, revision, delete, type changes |
workflow | workflow lifecycle events |
security | authentication, authorization, and permission events |
billing | inference, embedding, and image generation usage events |
system | platform maintenance and system events |
external | events pushed in from outside Vertesia through an ingest channel |
API reference
The subscription management and delivery inspection APIs are generated from the OpenAPI spec:
GET /api/v1/events/subscriptionsPOST /api/v1/events/subscriptionsGET /api/v1/events/subscriptions/{subscriptionId}PUT /api/v1/events/subscriptions/{subscriptionId}DELETE /api/v1/events/subscriptions/{subscriptionId}POST /api/v1/events/deliveries/search
Ingest channels (inbound webhooks) are managed under the same topic:
GET /api/v1/events/channelsPOST /api/v1/events/channelsGET /api/v1/events/channels/{channelId}PUT /api/v1/events/channels/{channelId}DELETE /api/v1/events/channels/{channelId}
The public ingest endpoint external systems POST to — POST /webhooks/events/{accountId}/{projectId}/{channelId} — is token/signature authenticated and is not part of the authenticated Vertesia API; its request shape is documented in the ingest channels section.
See Event Subscriptions in the API reference. The outbound webhook request body is not a callable Vertesia API endpoint, so its payload shape is documented in the webhook section below.
Create a webhook subscription in Studio
- Open Store > Event Bus > Subscriptions.
- Select Create subscription.
- Enter a name and optional description.
- Choose
projectscope. Account-scoped subscriptions are reserved for a future account-wide automation model. - Set the filter.
- Set Target type to
webhook. - Enter the webhook URL and timeout.
- Save the subscription.
The webhook URL must use https. Local development can allow http only when the server is explicitly running with local-development SSRF allowances. Private/internal hosts and dangerous ports are blocked.
Webhook payload
For the default event_envelope payload mode, Vertesia sends:
{
"event": {
"event_id": "01HF7YAT00ABCDEFGHJKMNPQRS",
"event_category": "content",
"action": "create",
"resource_type": "content_object",
"resource_id": "object-id",
"account_id": "account-id",
"project_id": "project-id",
"tenant_id": "tenant-id",
"timestamp": "2026-05-23T00:00:00.000Z",
"source": "zeno-server",
"resource_data": {}
},
"delivery": {
"id": "delivery-intent-id",
"subscription_id": "subscription-id",
"attempt": 1
}
}
Vertesia delivers webhooks through a durable workflow. If the target is temporarily unavailable, the delivery is retried and its status is visible in the Deliveries tab.
Webhook headers and signing
User-managed webhook subscriptions support only safe plaintext headers:
acceptcontent-typeuser-agentx-request-idx-correlation-idx-vertesia-correlation-id
Credential-bearing headers such as Authorization, x-api-key, cookies, and auth tokens are rejected.
Webhook signing is optional. New webhook subscriptions are unsigned unless signing is enabled. Legacy subscriptions migrated from workflow rules also remain unsigned unless you enable signing later.
When signing is enabled, Vertesia creates a per-subscription signing secret and shows it once when the subscription is created or rotated. Store that value in your webhook receiver. Vertesia does not show the existing secret again.
Signed webhook requests include these headers:
| Header | Value |
|---|---|
X-Vertesia-Event-Id | The platform event ID |
X-Vertesia-Delivery-Id | The delivery intent ID, stable across retries |
X-Vertesia-Timestamp | Unix timestamp in seconds, generated when the webhook delivery workflow starts |
X-Vertesia-Signature | v1=<hex HMAC-SHA256 signature> |
X-Vertesia-Event-Category | Event category, such as content |
X-Vertesia-Event-Action | Event action, such as create |
The signature is HMAC-SHA256 over:
<timestamp>.<delivery_id>.<raw request body>
Use the exact raw HTTP request body bytes when verifying the signature. Reject requests with timestamps outside your
accepted replay window. Vertesia's shared verifier uses a 15-minute replay window by default. Deduplicate by
X-Vertesia-Delivery-Id or by the pair
event.event_id + delivery.subscription_id.
Define filters
A subscription filter can match:
event_category: array of categories, or["*"]exclude_event_category: categories to excludeaction: event actionsresource_type: resource typescondition: a JSONLogic object evaluated against the event envelopesemantic_condition: an optional LLM-evaluated natural-language predicate (see below)
Example: match created or updated content objects:
{
"event_category": ["content"],
"resource_type": ["content_object"],
"action": ["create", "update"]
}
Example: match only document content objects:
{
"event_category": ["content"],
"resource_type": ["content_object"],
"action": ["create", "update", "revision_created"],
"condition": {
"==": [
{ "var": "resource_data.metadata.type" },
"document"
]
}
}
Example: match updates where the source changed:
{
"event_category": ["content"],
"resource_type": ["content_object"],
"action": ["update", "revision_created"],
"condition": {
"==": [
{ "var": "details.dirty.source" },
true
]
}
}
Semantic conditions (optional LLM filtering)
A semantic_condition is a natural-language predicate evaluated by an LLM after all structural filters
(event_category, action, resource_type, condition) have already matched. Use it when the routing decision
cannot be expressed as a structural rule — for example "the document looks like a signed contract amendment".
{
"event_category": ["content"],
"resource_type": ["content_object"],
"action": ["create", "update"],
"semantic_condition": {
"instruction": "the document appears to be a signed contract amendment",
"mode": "enforce",
"on_error": "fail_closed",
"evaluator": {
"type": "interaction",
"enrich_with_content": true
}
}
}
evaluator.type:interaction(default) — a single LLM classification call. Setenrich_with_content: trueto include an excerpt of the content object's text, and optionallyinteraction_refto use a stored classifier interaction.agent— a non-interactive agent run that may use tools (fetch documents, inspect processes) to decide. Slower and more expensive; the delivery sits in theevaluatingstate until the agent finishes.
mode:enforce(default) skips delivery on a negative verdict;shadowrecords the verdict on the delivery without ever blocking it — useful for trialing a condition before enforcing it.on_error:fail_closed(default) does not deliver when evaluation errors out;fail_opendelivers anyway.
Verdicts are visible per delivery in the Deliveries tab.
Create a webhook subscription with the CLI
Create a JSON file:
{
"name": "Notify CRM on document changes",
"description": "Sends document create/update events to the CRM ingestion endpoint.",
"scope": "project",
"enabled": true,
"priority": "normal",
"run_as_role": "automation",
"filter": {
"event_category": ["content"],
"resource_type": ["content_object"],
"action": ["create", "update"],
"condition": {
"==": [
{ "var": "resource_data.metadata.type" },
"document"
]
}
},
"target": {
"type": "webhook",
"url": "https://example.com/vertesia/events",
"signing_mode": "signed",
"payload_mode": "event_envelope",
"timeout_ms": 30000,
"headers": {
"content-type": "application/json"
}
}
}
Apply it:
vertesia events subscriptions apply --file crm-document-webhook.json
Omit signing_mode or set it to legacy_unsigned for an unsigned webhook.
Useful commands:
vertesia events subscriptions list
vertesia events subscriptions get <subscription-id>
vertesia events subscriptions update <subscription-id> --file subscription.json
vertesia events subscriptions delete <subscription-id>
Create a workflow subscription
A workflow target starts a named workflow for each matching event. The workflow receives the event reference and any configured variables.
{
"name": "Run contract analysis",
"scope": "project",
"run_as_role": "automation",
"filter": {
"event_category": ["content"],
"resource_type": ["content_object"],
"action": ["create"],
"condition": {
"==": [
{ "var": "resource_data.metadata.type" },
"document"
]
}
},
"target": {
"type": "workflow",
"endpoint": "AnalyzeContract",
"workflow_class": "contract-analysis",
"vars": {
"analysis_mode": "standard"
}
}
}
run_as_role is required on every subscription. It sets the project service token the delivery runs as, so a subscription always has an explicit run-as identity and never falls back to running as the event's originating user. User-managed subscriptions currently support:
automationexecutorreader
Create an agent subscription
An agent target starts an autonomous agent run for each matching event. The target can reference an interaction by ID,
app ref, or system ref. If interaction_ref is omitted, Vertesia uses the general-purpose system agent.
The target data object is passed to the interaction and can contain event templates:
{
"name": "Review changed policy documents",
"scope": "project",
"run_as_role": "automation",
"filter": {
"event_category": ["content"],
"resource_type": ["content_object"],
"action": ["update"],
"condition": {
"==": [
{ "var": "resource_data.metadata.type" },
"document"
]
}
},
"target": {
"type": "agent",
"interaction_ref": "sys:GeneralAgent",
"data": {
"message": "Review {{ event.resource_id }} and summarize the policy impact.",
"event": "$event",
"event_ref": "$event_ref",
"object_id": "$event.resource_id",
"touched_fields": "$event.details.dirty"
},
"max_iterations": 12
}
}
Event subscription agent runs are stored with source_type: "event_subscription" and include the event subscription ID
and event reference.
Create a process subscription
A process target starts a process run for each matching event. The target can reference a stored process or provide an inline process definition.
{
"name": "Start renewal process",
"scope": "project",
"run_as_role": "automation",
"filter": {
"event_category": ["content"],
"resource_type": ["content_object"],
"action": ["create"]
},
"target": {
"type": "process",
"process_ref": "contract-renewal",
"run_type": "programmatic",
"data": {
"message": "Start renewal review for {{ event.resource_id }}.",
"event_ref": "$event_ref",
"object_id": "$event.resource_id"
}
}
}
Process target data is merged into the process initial context, so the target process definition must accept the fields
you pass.
IDP document events
When IDP completes, Vertesia publishes a document_processed content event in addition to the generic workflow
lifecycle event. It is scoped to the processed content object, so webhook, workflow, agent, and process subscriptions can
filter it directly without inspecting a workflow result.
The event contains operational aggregates and references only. Extracted data, citations, bounding boxes, prompts, and page images remain in artifact storage.
{
"event_category": "content",
"action": "document_processed",
"resource_type": "content_object",
"resource_id": "document-id",
"meters": [
{ "category": "processing", "type": "documents", "quantity": 1 },
{ "category": "processing", "type": "pages", "quantity": 75 },
{ "category": "processing", "type": "ocr_pages", "quantity": 4 },
{ "category": "processing", "type": "vision_pages", "quantity": 6 },
{ "category": "extraction", "type": "properties", "quantity": 14 },
{ "category": "extraction", "type": "citations", "quantity": 218 },
{ "category": "verification", "type": "digitally_verified", "quantity": 211 },
{ "category": "verification", "type": "ai_verified", "quantity": 5 },
{ "category": "verification", "type": "unverified", "quantity": 2 },
{ "category": "review", "type": "issues", "quantity": 1 }
],
"details": {
"schema_version": 1,
"pipeline": "grounded_extraction",
"page_count": 75,
"ocr_page_count": 4,
"vision_page_count": 6,
"property_count": 14,
"citation_count": 218,
"verification": {
"total": 218,
"digitally_verified": 211,
"ai_verified": 5,
"unverified": 2
},
"confidence": 0.94,
"coverage_min": 0.72,
"hardness": 0.32,
"escalated": false,
"reviewed": true,
"review_issue_count": 1,
"verdict": "needs_review",
"verdict_reason": "Two values could not be verified against the source.",
"models_used": [
{
"role": "extraction",
"run_id": "interaction-run-id",
"model": "model-id",
"environment_id": "environment-id",
"provider": "provider-id"
}
],
"result_path": "magic-pdf/document-id/grounded-extraction.json",
"workflow_id": "grounded-workflow-id",
"workflow_run_id": "temporal-run-id",
"workflow_type": "PdfGroundedExtractionWorkflow"
}
}
Inference audit events produced by direct extraction and review calls carry the same details.workflow_run_id. Join on
that field to analyze document outcomes by the actual inference model and token usage without duplicating accounting data
in the document event. Large-document review agents are identified by details.review_agent_run_id when present.
For operational dashboards, POST /api/v1/audit-trail/aggregate accepts bounded, typed aggregations over event counts and
meters. The server always applies the authenticated account scope and forces the authenticated project for project-scoped
principals; client-provided scope never replaces those predicates. For example, the following query returns daily document
and page throughput:
{
"projectId": "project-id",
"from": "2026-07-01T00:00:00.000Z",
"to": "2026-08-01T00:00:00.000Z",
"filter": {
"actions": ["document_processed"],
"details": [
{ "field": "pipeline", "values": ["grounded_extraction"] }
]
},
"groupBy": [
{ "dimension": "time", "resolution": "day" }
],
"metrics": [
{ "id": "documents", "operation": "count" },
{
"id": "pages",
"operation": "sum_meter",
"meterCategory": "processing",
"meterType": "pages"
}
]
}
The following subscription sends IDP outcomes that need review to a signed webhook:
{
"name": "Notify on IDP review",
"scope": "project",
"run_as_role": "automation",
"filter": {
"event_category": ["content"],
"resource_type": ["content_object"],
"action": ["document_processed"],
"condition": {
"==": [
{ "var": "details.verdict" },
"needs_review"
]
}
},
"target": {
"type": "webhook",
"url": "https://example.com/vertesia/idp-events",
"signing_mode": "signed",
"payload_mode": "event_envelope",
"timeout_ms": 30000
}
}
The generic workflow_completed and workflow_failed events are still published for workflow lifecycle automation.
IDP workflow lifecycle events also target the content object and identify the workflow in details.workflow_type.
Inbound external event channels (incoming webhooks)
Subscriptions deliver events out of Vertesia. An ingest channel is the reverse: a per-channel public URL that
external systems (Salesforce, GitHub, Slack, ...) POST to in order to push events into the bus. Each ingested event
is published with event_category: "external" and source: "external:<source>", then matched by subscriptions exactly
like any internal event — so you react to it with a workflow, agent, process, or webhook the same way.
Vertesia has no other generic "incoming webhook". To push data in without a channel, use the REST API directly.
Create a channel in Studio
- Open Store > Event Bus > Ingest Channels.
- Select Create channel.
- Enter a name and a
sourcelabel (stamped on events asexternal:<source>, e.g.salesforce). - Optionally set a default action / resource type, a transform, and signature verification.
- Save. The ingest token (and the signing secret, if signature verification is enabled) are shown once — copy them now; Vertesia never shows them again.
The channel's ingest URL is:
POST https://<store-base-url>/webhooks/events/<accountId>/<projectId>/<channelId>
Authenticating inbound requests
A channel authenticates senders one of two ways:
- Ingest token (default). Pass the token as
Authorization: Bearer <token>, anx-vertesia-ingest-tokenheader, or a?token=query parameter (for senders that cannot set headers). - HMAC signature (see below). When signature verification is configured the sender signs the request body instead, and the ingest token becomes optional.
Request body
The body is JSON. The full raw body is always preserved under the event's details.payload. Top-level fields, when
present, set the corresponding event fields directly:
{
"action": "received",
"resource_type": "lead",
"resource_id": "00Q5g00000ABCDEments",
"idempotency_key": "evt-12345",
"timestamp": "2026-05-23T00:00:00.000Z",
"payload": { "any": "domain data" },
"details": { "extra": "merged into event.details" }
}
All fields are optional. When omitted, the channel's transform and defaults apply. Sending the same idempotency_key
twice produces the same event_id, so retries are de-duplicated. A successful POST returns 202 with { "event_id": "..." }.
Transforms
Most providers send their own payload shape and cannot wrap it in the envelope above. A transform declaratively maps
dot-paths in the raw body to event fields (array indices supported, e.g. commits.0.id):
{
"transform": {
"action_path": "action",
"resource_type_path": "resource",
"resource_id_path": "pull_request.id",
"idempotency_key_path": "delivery_id",
"timestamp_path": "created_at",
"static_details": { "provider": "github" }
}
}
Extracted values override the channel defaults; the raw body is still kept under details.payload.
Signature verification
For senders that sign their requests (GitHub, Stripe, a Salesforce Apex callout), enable optional HMAC verification on
the channel instead of relying on the ingest token. The server recomputes HMAC(algorithm, signing_secret, rawBody) and
compares it (timing-safe) to the header value after stripping the configured prefix.
{
"signature": {
"header": "x-hub-signature-256",
"algorithm": "sha256",
"encoding": "hex",
"prefix": "sha256="
}
}
header— request header carrying the signature.algorithm—sha256(default) orsha1.encoding—hex(default) orbase64.prefix— optional literal stripped from the header value before comparison.
The signing secret is generated by Vertesia and shown once on create or rotation; configure the sender to HMAC the
raw request body with it. When signature verification is configured, the ingest token is not required (providers
authenticate by signature alone). Rotate the secret by updating the channel with rotate_signing_secret: true.
Create a channel with the API
Ingest channels are managed through the REST API under /api/v1/events/channels (POST to create, GET to
list, GET/PUT/DELETE by id). Send the channel definition as the request body:
{
"name": "Salesforce leads",
"source": "salesforce",
"default_action": "received",
"default_resource_type": "lead",
"transform": { "resource_id_path": "lead.id" },
"signature": { "header": "x-vertesia-signature", "algorithm": "sha256", "encoding": "hex" }
}
curl -X POST "$VERTESIA_API/api/v1/events/channels" \
-H "authorization: Bearer $VERTESIA_TOKEN" \
-H "content-type: application/json" \
--data @salesforce-leads.json
Then create a subscription that matches the external events and runs your automation:
{
"name": "Qualify inbound leads",
"scope": "project",
"run_as_role": "automation",
"filter": {
"event_category": ["external"],
"resource_type": ["lead"],
"action": ["received"]
},
"target": { "type": "agent", "interaction_ref": "sys:GeneralAgent" }
}
Event templates
Agent and process target data supports event templates:
| Template | Result |
|---|---|
"$event.resource_id" | Inserts the referenced value while preserving its original JSON type |
"{{ event.resource_id }}" | Inserts the referenced value while preserving its original JSON type |
"Document {{ event.resource_id }} changed" | Replaces the template inline as text |
"$event" | Passes the complete event object |
"$event_ref" | Passes the event reference |
"$delivery.id" | Passes the delivery intent ID |
If a template references a missing field, the delivery fails because retrying the same event cannot resolve the missing field.
Delivery behavior
Event subscriptions are at-least-once. Delivery can be retried after transient failures, so webhook handlers should be idempotent. Use event.event_id and delivery.subscription_id as stable identifiers when deduplicating work.
For all delivery target types, Vertesia starts the underlying Temporal workflow with a stable workflow identifier based on the event and subscription, so repeated delivery attempts do not intentionally start duplicate work for the same event/subscription pair.
Troubleshooting
Use Store > Event Bus > Deliveries to inspect:
- whether an event matched the subscription
- delivery status: pending, running, succeeded, retrying, failed, or cancelled
- workflow IDs and run IDs for started deliveries
- recent errors and retry state
If no delivery appears:
- check that the subscription is enabled
- confirm the subscription is in the same project as the event
- check
event_category,action, andresource_type - test the JSONLogic condition against the event shape
- confirm the event source includes the fields referenced by the condition
If webhook delivery fails:
- confirm the URL is reachable from Vertesia
- keep the timeout below 50 seconds
- return a successful HTTP status quickly
- deduplicate using
event_id - inspect the delivery error in the Deliveries tab
