This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Observability

How to react to state changes and events in an EDC runtime

    Overview

    EDC processes are asynchronous. State changes — such as a contract negotiation reaching agreement or a transfer process starting — are communicated through an internal eventing system. There are three ways to observe these state changes from outside the runtime or from within a custom extension:

    ApproachWhere the consumer runsHow it is registered
    In-process event subscriberSame JVM as the EDC runtimeCustom extension, injected EventRouter
    Per-request webhooksExternal HTTP endpointcallbackAddresses in Management API request bodies
    Static callbacksExternal HTTP endpointedc.callback.* configuration properties

    1. In-process event subscribers

    This approach is for custom EDC extensions that need to react to events inside the same runtime process. It requires writing a Java extension that is bundled into your EDC distribution.

    The entry point is the EventRouter service. Register an EventSubscriber against an event type during extension initialization:

    @Inject
    private EventRouter eventRouter;
    
    @Override
    public void initialize(ServiceExtensionContext context) {
        eventRouter.register(TransferProcessEvent.class, new MyTransferEventHandler());
    }
    

    Subscribers are typed over a class hierarchy. Registering against TransferProcessEvent (a base class) will receive all transfer-process-related events. Registering against a concrete class such as TransferProcessStarted will only receive that specific event.

    1.1 Async vs. sync dispatch

    EventRouter offers two dispatch modes:

    • register (async): The subscriber is invoked on a separate thread. The main processing thread is not blocked. Use this for notifications and fire-and-forget scenarios.
    • registerSync (sync): The subscriber is invoked on the same thread that published the event. A failure or exception in the subscriber propagates to the caller and can roll back a transaction. Use this when you need reliable, at-least-once delivery (e.g. persisting events to an audit log).

    1.2 Example: auditing extension

    public class AuditingExtension implements ServiceExtension {
    
        @Inject
        private EventRouter eventRouter;
    
        @Override
        public void initialize(ServiceExtensionContext context) {
            // sync so that audit entries are written within the same transaction
            eventRouter.registerSync(TransferProcessEvent.class, new AuditingEventHandler());
        }
    }
    
    public class AuditingEventHandler implements EventSubscriber {
    
        @Override
        public <E extends Event> void on(EventEnvelope<E> envelope) {
            if (envelope.getPayload() instanceof TransferProcessEvent event) {
                // write event to audit store
            }
        }
    }
    

    For a full explanation of the eventing model, including how to emit custom events, see the Service Layers documentation.

    2. Webhooks (per-request callbacks)

    When you cannot bundle a custom extension into the runtime, you can supply an HTTP callback URL directly in the Management API request that initiates a process. EDC will POST the event payload to that URL when the corresponding state change occurs.

    Callback addresses are provided in the callbackAddresses array of the request body. For example, when initiating a contract negotiation:

    // POST /v4/contractnegotiations
    {
      "@context": ["https://w3id.org/edc/connector/management/v2"],
      "@type": "ContractRequest",
      "counterPartyAddress": "http://provider-address",
      "protocol": "dataspace-protocol-http",
      "policy": { "...": "..." },
      "callbackAddresses": [
        {
          "uri": "https://my-backend/edc-events",
          "events": ["contract.negotiation", "transfer.process"],
          "transactional": false,
          "authKey": "Authorization",
          "authCodeId": "my-secret-vault-alias"
        }
      ]
    }
    

    The events field accepts dot-separated event type prefixes. A value of "contract.negotiation" matches all contract-negotiation events; a value of "transfer.process.started" matches only the started event.

    This approach is tightly scoped: the webhook is only active for the process initiated by that specific request.

    3. Static callbacks

    Static callbacks are configured at runtime startup and apply globally to all matching events, regardless of which Management API request triggered the process. This is useful when you want a single backend endpoint to receive all events of a given type without modifying individual API calls.

    Static callbacks require the callback-static-endpoint extension to be present in your distribution.

    Each callback is defined under the edc.callback.<name> configuration prefix, where <name> is an arbitrary identifier that groups the settings for one endpoint. Multiple callbacks can be configured by using different names.

    # Register a callback named "audit" that receives all transfer-process events
    edc.callback.audit.uri=https://my-backend/edc-events
    edc.callback.audit.events=transfer.process
    
    # Register a second callback named "monitor" for all contract-negotiation events
    edc.callback.monitor.uri=https://monitoring.internal/hooks
    edc.callback.monitor.events=contract.negotiation
    

    3.1 Configuration reference

    PropertyDescriptionRequiredDefault
    edc.callback.<name>.uriThe HTTP endpoint to callyes—
    edc.callback.<name>.eventsComma-separated list of event type prefixes. Leave empty to receive all events.no(all events)
    edc.callback.<name>.transactionalWhether to invoke the callback synchronously within the processing transaction (see below)nofalse
    edc.callback.<name>.auth.keyThe HTTP header name to use for authenticationno—
    edc.callback.<name>.auth.codeidVault alias of the secret token to send in the authentication headerno—

    3.2 Authentication

    If the target endpoint requires authentication, provide the header name in auth.key and store the secret in EDC’s vault. The auth.codeid property is then set to the vault alias under which the secret is stored.

    edc.callback.audit.uri=https://my-backend/edc-events
    edc.callback.audit.events=transfer.process
    edc.callback.audit.auth.key=Authorization
    edc.callback.audit.auth.codeid=my-api-key-alias
    

    auth.codeid is required when auth.key is set. Providing auth.key without auth.codeid will cause the runtime to fail at startup.

    3.3 Transactional callbacks

    Setting transactional=true causes the callback to be dispatched synchronously, within the transaction that triggered the event. A failure (e.g. the endpoint is unreachable) will propagate back as a processing error and may cause the state machine to retry the transition. Use this only when the callback endpoint is highly reliable and you need guaranteed delivery. For most monitoring use cases, leave transactional=false.

    Choosing the right approach

    • Use in-process event subscribers when you are already writing a custom EDC extension and need the lowest latency or transactional semantics.
    • Use per-request webhooks when you want to track specific operations triggered from a controller or orchestration layer that already constructs the Management API calls.
    • Use static callbacks when you want a centralized, always-on sink for a class of events without modifying individual API requests or shipping code to the runtime.