Vertesia Documentation

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:

CategoryTypical use
contentcontent object create, update, revision, delete, type changes
workflowworkflow lifecycle events
securityauthentication, authorization, and permission events
billinginference, embedding, and image generation usage events
systemplatform maintenance and system events
externalevents 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/subscriptions
  • POST /api/v1/events/subscriptions
  • GET /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/channels
  • POST /api/v1/events/channels
  • GET /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

  1. Open Store > Event Bus > Subscriptions.
  2. Select Create subscription.
  3. Enter a name and optional description.
  4. Choose project scope. Account-scoped subscriptions are reserved for a future account-wide automation model.
  5. Set the filter.
  6. Set Target type to webhook.
  7. Enter the webhook URL and timeout.
  8. 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:

  • accept
  • content-type
  • user-agent
  • x-request-id
  • x-correlation-id
  • x-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:

HeaderValue
X-Vertesia-Event-IdThe platform event ID
X-Vertesia-Delivery-IdThe delivery intent ID, stable across retries
X-Vertesia-TimestampUnix timestamp in seconds, generated when the webhook delivery workflow starts
X-Vertesia-Signaturev1=<hex HMAC-SHA256 signature>
X-Vertesia-Event-CategoryEvent category, such as content
X-Vertesia-Event-ActionEvent 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 exclude
  • action: event actions
  • resource_type: resource types
  • condition: a JSONLogic object evaluated against the event envelope
  • semantic_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. Set enrich_with_content: true to include an excerpt of the content object's text, and optionally interaction_ref to 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 the evaluating state until the agent finishes.
  • mode: enforce (default) skips delivery on a negative verdict; shadow records 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_open delivers 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:

  • automation
  • executor
  • reader

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

  1. Open Store > Event Bus > Ingest Channels.
  2. Select Create channel.
  3. Enter a name and a source label (stamped on events as external:<source>, e.g. salesforce).
  4. Optionally set a default action / resource type, a transform, and signature verification.
  5. 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>, an x-vertesia-ingest-token header, 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.
  • algorithmsha256 (default) or sha1.
  • encodinghex (default) or base64.
  • 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:

TemplateResult
"$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, and resource_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

Was this page helpful?