API Reference#
This page provides detailed reference information for both the Event Aggregation Service (Publisher API) and Event Consumer Service (Consumer API).
API Types#
Both services provide dual APIs:
gRPC API: Protocol Buffer-based, high-performance, strongly-typed
REST API: HTTP/JSON-based, easier for testing and web clients
Use gRPC for production microservices and REST for web clients or when easier HTTP tooling is preferred.
Event Aggregation Service (Publisher API)#
The Event Aggregation Service accepts events from publishers and routes them to consumers.
Base URLs#
gRPC:
your-aggregation-service.example.com:50051REST:
https://your-aggregation-service.example.com
Authentication#
Include a bearer token in all requests:
gRPC:
metadata = [('authorization', 'Bearer YOUR_TOKEN')]
REST:
Authorization: Bearer YOUR_TOKEN
Limits#
Both APIs limit each event’s event_type, message, occurred_at and resource_id, allow 1 to 1000 events per batch, and bound each request’s size, 4 MiB by default. A request that breaks any of these limits publishes nothing. See Event Structure for the limits and When a Request Breaks a Limit for how each is refused.
Publisher gRPC API#
Service Definition#
service EventPublishingService {
rpc PublishEvent(PublishEventRequest) returns (PublishEventResponse);
rpc BatchPublishEvents(BatchPublishEventsRequest) returns (BatchPublishEventsResponse);
}
PublishEvent#
Publishes a single event.
Request: PublishEventRequest
message PublishEventRequest {
Event event = 1;
}
message Event {
string event_type = 1;
google.protobuf.Struct message = 2;
google.protobuf.Timestamp occurred_at = 3;
EventResource resource = 4; // Optional, except for the four storage types; see publishing-events.md#resource
}
message EventResource {
string resource_id = 1;
}
Response: PublishEventResponse
message PublishEventResponse {
PublishingResult result = 1;
}
message PublishingResult {
Event event = 1;
bool success = 2;
string failure_reason = 3; // Only set if success is false
}
Errors:
gRPC Status Code |
Meaning |
|---|---|
|
Invalid or expired authentication token |
|
Not authorized to publish this event type |
|
Invalid event structure, an event that breaks a limit, an empty |
|
Request too large - don’t resend it unchanged; see When a Request Breaks a Limit |
|
Server error |
Example:
from nvidia.omniverse.notifications.publisher.v1beta import event_publisher_pb2
# Create event
event = event_publisher_pb2.Event(
event_type='myapp.user.created',
message=message_struct,
occurred_at=timestamp,
resource=event_publisher_pb2.EventResource(
resource_id='/users/123'
)
)
# Publish
request = event_publisher_pb2.PublishEventRequest(event=event)
response = stub.PublishEvent(request, metadata=metadata)
if response.result.success:
print("Published successfully")
BatchPublishEvents#
Publishes multiple events, one after another rather than in parallel. See Batch Publishing Multiple Events.
Request: BatchPublishEventsRequest
message BatchPublishEventsRequest {
repeated Event events = 1;
}
Response: BatchPublishEventsResponse
message BatchPublishEventsResponse {
repeated PublishingResult results = 1;
}
Note: Each event in a batch that passes the checks under Errors below has its own result: some may publish while others fail to.
Errors: the same as PublishEvent, for each event in the batch. An empty batch, or a batch of more than 1000 events, is also refused with INVALID_ARGUMENT. One refused event refuses the whole batch, and nothing is published.
Example:
# Create multiple events
events = [event1, event2, event3]
# Batch publish
request = event_publisher_pb2.BatchPublishEventsRequest()
for event in events:
request.events.append(event)
response = stub.BatchPublishEvents(request, metadata=metadata)
# Check individual results
for idx, result in enumerate(response.results):
if result.success:
print(f"Event {idx} published")
else:
print(f"Event {idx} failed: {result.failure_reason}")
Publisher REST API#
Endpoints#
POST /api/v1beta/events#
Publishes a single event.
Request Body:
{
"event": {
"event_type": "string",
"message": {"key": "value"},
"occurred_at": "2024-10-16T14:30:00Z",
"resource": {
"resource_id": "/path/to/resource"
}
}
}
Response (200 OK):
{
"result": {
"event": { /* echo of event */ },
"success": true
}
}
Response (200 OK, but publish failed):
{
"result": {
"event": { /* echo of event */ },
"success": false,
"failure_reason": "Unable to connect to message broker"
}
}
Error Responses:
Status Code |
Description |
|---|---|
400 |
An empty |
401 |
Unauthenticated |
403 |
Permission denied |
413 |
Request too large - don’t resend it unchanged; see When a Request Breaks a Limit |
422 |
Validation error, such as an event that breaks a limit; |
Example:
curl -X POST https://your-aggregation-service.example.com/api/v1beta/events \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event": {
"event_type": "myapp.user.created",
"message": {
"user_id": "123",
"username": "john_doe"
},
"occurred_at": "2024-10-16T14:30:00Z",
"resource": {
"resource_id": "/users/123"
}
}
}'
POST /api/v1beta/events/batch#
Publishes multiple events, one after another rather than in parallel. See Batch Publishing Multiple Events.
Request Body:
{
"events": [
{
"event_type": "string",
"message": {"key": "value"},
"occurred_at": "2024-10-16T14:30:00Z",
"resource": {
"resource_id": "/path/to/resource"
}
}
]
}
Response (200 OK):
{
"results": [
{
"event": { /* event 1 */ },
"success": true
},
{
"event": { /* event 2 */ },
"success": false,
"failure_reason": "Publish failed"
}
]
}
Error Responses:
Status Code |
Description |
|---|---|
400 |
An empty batch, an empty |
401 |
Unauthenticated |
403 |
Permission denied |
413 |
Request too large - don’t resend it unchanged; see When a Request Breaks a Limit |
422 |
Validation error, such as an event that breaks a limit or a batch of more than 1000 events; |
Example:
curl -X POST https://your-aggregation-service.example.com/api/v1beta/events/batch \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"event_type": "myapp.file.uploaded",
"message": {"file": "doc1.pdf"},
"occurred_at": "2024-10-16T14:30:00Z"
},
{
"event_type": "myapp.file.uploaded",
"message": {"file": "doc2.pdf"},
"occurred_at": "2024-10-16T14:30:01Z"
}
]
}'
Event Consumer Service (Consumer API)#
The Event Consumer Service allows clients to stream events with filtering.
Base URLs#
gRPC:
your-consumer-service.example.com:50052REST:
https://your-consumer-service.example.com
Consumer gRPC API#
Service Definition#
service EventConsumerService {
rpc ConsumeNonDurableEvents(stream ConsumeNonDurableEventsRequest)
returns (stream ConsumeNonDurableEventsResponse);
// Deprecated: use ConsumeDurableEventsWithAcks instead.
rpc ConsumeDurableEvents(ConsumeDurableEventsRequest)
returns (stream ConsumeDurableEventsResponse);
rpc ConsumeDurableEventsWithAcks(stream ConsumeDurableEventsWithAcksRequest)
returns (stream ConsumeDurableEventsWithAcksResponse);
rpc CreateDurableQueue(CreateDurableQueueRequest)
returns (CreateDurableQueueResponse);
rpc UpdateDurableQueue(UpdateDurableQueueRequest)
returns (UpdateDurableQueueResponse);
rpc DeleteDurableQueue(DeleteDurableQueueRequest)
returns (DeleteDurableQueueResponse);
rpc ManageDeadLetteredEvents(stream ManageDeadLetteredEventsRequest)
returns (stream ManageDeadLetteredEventsResponse);
}
ConsumeNonDurableEvents#
Streams events using a non-durable queue (bidirectional streaming).
Reattaching to the queue it names is what lets you reconnect after a drop and share one queue across parallel clients. When several clients share a queue this way, at most one of them may change its filters, and that client alone can follow the acknowledgement-driven replay recipe described below.
Pass back a token the server issued, verbatim. A token must be 1 to 200 printable ASCII characters — no spaces, no control characters, nothing non-ASCII — and one that is not is rejected with INVALID_ARGUMENT, with a message that names the field and the rule and quotes none of the token. The tokens this service issues are random identifiers well inside that rule, so echoing back what you were given always passes it. A token of legal shape that no longer names a live queue is reported as NOT_FOUND instead — see below. Omit the field entirely to start a fresh stream.
The stream returns INTERNAL, with the message Internal Server Error: The broker cancelled this stream's consumer. Reconnect to resume consuming., when the broker cancels this stream’s consumer or kills the channel it lives on — reconnect with the reconnect_token, and the queue normally survives with its buffered backlog. That resumes the queue with the bindings it already holds, so a gRPC client that has changed filters mid-stream also re-sends the updates the server did not acknowledge — see the filter_update_ack description under Response below. INTERNAL does not by itself confirm that the queue survived: if the queue no longer exists, the reconnect returns NOT_FOUND, and the recovery for a queue that is gone applies. It returns NOT_FOUND when the queue is gone: a non-durable queue outlives its consumer only for the deployment’s reconnect-grace window (20 seconds by default), after which a reconnect_token reconnect returns NOT_FOUND again and you should start a fresh stream with filter_groups and no token. See Handle Disconnections Gracefully.
Request: ConsumeNonDurableEventsRequest (stream)
message ConsumeNonDurableEventsRequest {
repeated FilterGroup filter_groups = 1;
optional string reconnect_token = 2;
repeated FilterGroup previous_filter_groups = 3; // Required when updating filters
}
message FilterGroup {
string event_type = 1;
repeated ResourceFilter filters = 2;
}
message ResourceFilter {
enum FilterType {
FILTER_TYPE_UNSPECIFIED = 0;
FILTER_TYPE_EQ = 1;
FILTER_TYPE_STARTS_WITH_LAZY = 2;
FILTER_TYPE_STARTS_WITH_GREEDY = 3;
}
FilterType filter_type = 1;
string resource_id = 2;
}
Response: ConsumeNonDurableEventsResponse (stream)
message ConsumeNonDurableEventsResponse {
repeated Event events = 1;
string reconnect_token = 2;
bool supports_filter_update_acks = 3;
FilterUpdateAck filter_update_ack = 4;
}
message FilterUpdateAck {
repeated FilterGroup applied_filter_groups = 1;
bool unchanged = 2;
}
message Event {
string event_type = 1;
string principal_identity = 2;
google.protobuf.Timestamp occurred_at = 3;
google.protobuf.Timestamp published_at = 4;
google.protobuf.Struct message = 5;
}
supports_filter_update_acks is true on every response of a stream served by a server that acknowledges the filter sets its requests name. It describes the server serving this stream rather than the individual response, so read it from any response you receive, including the first one as soon as the stream opens. A server that sets it sends one response carrying a filter_update_ack for every request it acts on that names a filter set, including a request that required no bind or unbind call because it named the set you had already asked for. A server that does not set it may still act on the filter sets its requests name, but never reports doing so, so you cannot know which of your requests took effect.
filter_update_ack is present on a response that acknowledges a request whose filter set the server acted on, and absent on every other response. Receiving it means the server is done with that request: any bind and unbind calls the request led to have returned. It does not report how many calls that was, and the answer may be none. Its two fields:
applied_filter_groups— a verbatim copy of thefilter_groupsof the request being acknowledged. The server holds no filter state and does not read a non-durable queue’s bindings back from the broker, so this echoes what you sent rather than reporting what the broker holds.unchanged—truewhen the acknowledged request’sfilter_groupsequalled itsprevious_filter_groupsas you sent them, so the server issued no bind or unbind call.falsemeans the two differed as sent, or that the request named no previous set at all, as a fresh connect does. This is a statement about the request the server received, not about the queue: it does not confirm that the queue’s bindings matchapplied_filter_groups, and afalsevalue does not promise that any bind or unbind call was issued — a request that re-sends the same groups reordered, or with a group repeated, differs as sent and still changes no bindings.
The first response on the stream carries an acknowledgement when the opening request named a filter set the server acted on. A fresh connect, which names filter_groups and no reconnect_token, is acknowledged with unchanged false. A reconnect that names filter_groups and previous_filter_groups together is a filter update applied before the stream yields anything, and is acknowledged with unchanged set according to whether the two sets were equal as you sent them. A reconnect carrying only a reconnect_token names no set and gets no acknowledgement, because the server keeps no filter state and cannot report a set it was not told. A reconnect that names filter_groups without previous_filter_groups is not a resume either: it is an incomplete filter update and is rejected with INVALID_ARGUMENT, so filter groups the server does not apply are never acknowledged as applied. Two kinds of mid-stream request are never acknowledged: one that names neither filter_groups nor previous_filter_groups, which is ignored, and one that is rejected, which ends the stream with INVALID_ARGUMENT. Nor is an update still in flight when the stream ends with a terminal error, as it does when the broker cancels the consumer (see the INTERNAL and NOT_FOUND cases above): the stream ends and the update is never acknowledged. Acknowledgements are interleaved with event responses in no guaranteed order, but are ordered with respect to one another, in the order the updates were sent. See Updating Filters Dynamically for the recipe that uses them to resume a stream whose filters changed.
Example:
# Create filter
filter_group = event_consumer_pb2.FilterGroup(
event_type='myapp.user.created'
)
# No filters added - receive all events of this type
# Request stream
def request_stream():
request = event_consumer_pb2.ConsumeNonDurableEventsRequest()
request.filter_groups.append(filter_group)
yield request
# Consume
responses = stub.ConsumeNonDurableEvents(request_stream(), metadata=metadata)
for response in responses:
reconnect_token = response.reconnect_token # Save for reconnection
for event in response.events:
print(f"Event: {event.event_type}")
print(f"Message: {event.message}")
ConsumeDurableEvents (Deprecated)#
Deprecated: Use
ConsumeDurableEventsWithAcksinstead. This RPC acknowledges messages automatically after a short delay, which has a window for message loss on disconnect. It will be removed in a future release.
Streams events from a durable queue (server streaming). Messages are automatically acknowledged by the server after a short delay.
This RPC returns NOT_FOUND when the queue no longer exists — either the queue_id is wrong, or the queue was auto-deleted after going without an attached consumer for the deployment’s idle window (24 hours by default); recover by calling CreateDurableQueue again with the same queue_id and the same filter_groups, then resuming. See Durable Queue Lifetime.
The stream returns INTERNAL, with the message Internal Server Error: The broker cancelled this stream's consumer. Reconnect to resume consuming., when the broker cancels this stream’s consumer or kills the channel it lives on. Reconnect and resume consuming; do not recreate the queue. The queue normally survives with its buffered messages, and messages that were in flight unacknowledged are redelivered once you reconnect. INTERNAL does not by itself confirm that the queue survived: if the queue no longer exists, the reconnect returns NOT_FOUND, and the recovery for a queue that is gone applies. See Handle Disconnections Gracefully.
Request: ConsumeDurableEventsRequest
message ConsumeDurableEventsRequest {
string queue_id = 1;
}
Field |
Type |
Description |
|---|---|---|
|
|
Required. The id returned by |
Response: ConsumeDurableEventsResponse (stream)
message ConsumeDurableEventsResponse {
repeated Event events = 1;
}
Example:
request = event_consumer_pb2.ConsumeDurableEventsRequest(
queue_id='saved-queue-id'
)
responses = stub.ConsumeDurableEvents(request, metadata=metadata)
for response in responses:
for event in response.events:
process_event(event)
ConsumeDurableEventsWithAcks#
Streams events from a durable queue with explicit client acknowledgment (bidirectional streaming). The client settles each received message by sending its delivery tag back in a subsequent request: in ack_delivery_tags to acknowledge it, or in nack_delivery_tags to have it delivered again. A nacked message is requeued for another attempt; a message that is neither acknowledged nor nacked is redelivered once the client reconnects. Redelivery is bounded rather than indefinite — the delivery-count budget and the dead-letter queue it ends at are described below.
This RPC returns NOT_FOUND when the queue no longer exists — either the queue_id is wrong, or the queue was auto-deleted after going without an attached consumer for the deployment’s idle window (24 hours by default); recover by calling CreateDurableQueue again with the same queue_id and the same filter_groups, then resuming. See Durable Queue Lifetime.
The stream returns INTERNAL, with the message Internal Server Error: The broker cancelled this stream's consumer. Reconnect to resume consuming., when the broker cancels this stream’s consumer or kills the channel it lives on — an acknowledgement timeout on a slow consumer is the usual cause. Reconnect and resume consuming; do not recreate the queue. The queue normally survives with its buffered messages, and messages that were in flight unacknowledged are redelivered once you reconnect. INTERNAL does not by itself confirm that the queue survived: if the queue no longer exists, the reconnect returns NOT_FOUND, and the recovery for a queue that is gone applies. See Handle Disconnections Gracefully.
Request: ConsumeDurableEventsWithAcksRequest (stream)
message ConsumeDurableEventsWithAcksRequest {
string queue_id = 1; // Required on first request; ignored after
repeated int64 ack_delivery_tags = 2; // Delivery tags to acknowledge
repeated int64 nack_delivery_tags = 3; // Delivery tags to have delivered again
}
The first message in the stream must include queue_id to identify the durable queue. It must match ^[A-Za-z0-9._-]{1,200}$; an id that does not is rejected with INVALID_ARGUMENT before the broker is contacted. Queue ids share one deployment-wide namespace, so this must be the id of a queue that was created by (or for) your client; see CreateDurableQueue. Subsequent messages carry ack_delivery_tags to acknowledge receipt of previously delivered events, and nack_delivery_tags to request redelivery of events the client failed to process. One request may carry both fields; acknowledgments are processed first, so a tag appearing in both is acknowledged. A tag the server does not recognize — already settled, or from an earlier connection — is skipped and does not end the stream.
Settle promptly. An unacknowledged message is redelivered when the client reconnects, and a nacked message is requeued and redelivered immediately, possibly on the same stream: the server applies no retry delay, so a client that wants one implements backoff before it nacks.
Every redelivery increments the message’s delivery count, whichever way the message was requeued — by a nack, by a disconnect, or by the broker closing the stream’s channel. That count is cumulative over the message’s whole lifetime and is never reset, so it is a lifetime budget for the message rather than a per-nack one; nacking in a tight loop exhausts it in well under a second. A message whose delivery count passes the deployment’s delivery limit is moved to the dead-letter queue rather than redelivered forever — as is one that outlives the queue’s configured message TTL or is evicted because the queue reached its configured maximum length. The dead-letter queue is the destination for these poison, expired, and overflowed messages alike. See the bounded redelivery and dead-letter queue explanation in the consuming-events guide for the fuller treatment.
Response: ConsumeDurableEventsWithAcksResponse (stream)
message ConsumeDurableEventsWithAcksResponse {
repeated Event events = 1;
int64 delivery_tag = 2; // Tag to send back to acknowledge or nack this message
}
Example:
import queue
ack_queue = queue.Queue()
def request_stream(queue_id):
# First request: identify the durable queue
yield event_consumer_pb2.ConsumeDurableEventsWithAcksRequest(
queue_id=queue_id
)
# Subsequent requests: send acknowledgments
while True:
tags = ack_queue.get()
if tags is None:
break
yield event_consumer_pb2.ConsumeDurableEventsWithAcksRequest(
ack_delivery_tags=tags
)
responses = stub.ConsumeDurableEventsWithAcks(
request_stream('saved-queue-id'), metadata=metadata
)
for response in responses:
for event in response.events:
process_event(event)
# Acknowledge after processing
ack_queue.put([response.delivery_tag])
To hand a message back for another attempt instead, send its delivery_tag in nack_delivery_tags — after a backoff delay, since redelivery is immediate. The consuming-events guide carries a worked example under Step 2: Consume from Durable Queue, and the full retry-budget treatment under Bounded Redelivery and the Dead-Letter Queue.
CreateDurableQueue#
Creates a durable queue for persistent event storage.
This operation has ensure-exists semantics when you supply a queue_id: it creates the queue if no queue with that id exists, and is a no-op if a queue with that id already exists. The created flag in the response tells you which path the call took.
A queue_id you supply must be unique across the whole deployment. Queue ids share a single namespace. If a queue with your id already exists — whoever created it, and whatever filter_groups you send — and you are permitted to create it, this call attaches to that queue and returns it with its existing filters and created set to false. Two callers that pick the same id and are both permitted it share one queue: each receives the other’s events, and either can delete it if permitted to. Use a UUID, or a namespaced convention such as <service-name>-<purpose>; never a bare generic word like events.
Because an existing queue keeps its existing filters, a client that needs the queue to have exactly the filter groups it stores calls this RPC with its queue_id and those filter groups, then UpdateDurableQueue with the same filter groups. The update replaces whatever filters the queue has with them.
Authorization. Three permissions govern durable queues: create-durable-queues (creating a queue, attaching to an existing one, and updating a queue’s filters), consume-durable-queues and delete-durable-queues. Each operation is checked before the queue is looked up, so a refused call returns PERMISSION_DENIED whether or not the queue exists.
By default these permissions are deployment-wide: any authenticated principal that knows a durable queue’s id can create (attach to), consume from, update or delete that queue if it holds the deployment-wide permission, and receives only the events its own event permissions allow. Treat a
queue_idas configuration, not as a secret that protects a queue, and ask your operator to grant these permissions to the service accounts that consume events rather than to broad user groups.Where the deployment enforces per-queue authorization, each check names the queue as
DurableQueue::"<queue_id>", and a principal can do each of these only if a permission-service policy grants it that permission on that queue, or on every queue; knowing the id grants nothing by itself. A call that omitsqueue_idis checked against the id the server assigns, so it needs a grant covering every queue; a client whose grants name its queues supplies its ownqueue_id. See Per-Queue Authorization.
The queue exists until you delete it with DeleteDurableQueue, or until it has gone without an attached consumer for the deployment’s idle window (24 hours by default), at which point it is deleted along with any messages it still holds. Only an attached consumer keeps a durable queue alive — publishing to it and re-declaring it do not. Consuming from a queue deleted this way returns NOT_FOUND; recover by calling this RPC again with the same queue_id and the same filter_groups, since the queue’s subscriptions are rebuilt from the filters. See Durable Queue Lifetime.
Request: CreateDurableQueueRequest
message CreateDurableQueueRequest {
repeated FilterGroup filter_groups = 1;
string queue_id = 2;
}
Field |
Type |
Description |
|---|---|---|
|
|
Filters to apply to events. If omitted or empty, all events for an event type are added to the queue. |
|
|
Optional, client-specified queue identifier. An empty string means not supplied — the server generates one. When supplied, it must match |
Response: CreateDurableQueueResponse
message CreateDurableQueueResponse {
string queue_id = 1; // Save this, together with your filter_groups!
bool created = 2;
}
Field |
Type |
Description |
|---|---|---|
|
|
The id of the durable queue. Persist this together with the |
|
|
|
Example:
filter_group = event_consumer_pb2.FilterGroup(
event_type='myapp.file.uploaded'
)
resource_filter = event_consumer_pb2.ResourceFilter(
filter_type=event_consumer_pb2.ResourceFilter.FILTER_TYPE_STARTS_WITH_GREEDY,
resource_id='/uploads/'
)
filter_group.filters.append(resource_filter)
request = event_consumer_pb2.CreateDurableQueueRequest()
request.filter_groups.append(filter_group)
# Optional: supply your own queue_id for ensure-exists semantics. Omit it
# (leave it empty) to have the server generate one. A supplied id must be
# unique across the deployment, so namespace it as <service-name>-<purpose>.
request.queue_id = 'myapp-uploads-queue'
response = stub.CreateDurableQueue(request, metadata=metadata)
queue_id = response.queue_id
if response.created:
print(f'Created new durable queue {queue_id}')
else:
print(f'Reusing existing durable queue {queue_id}')
# IMPORTANT: Save queue_id AND the filter_groups it was created with to
# configuration — both are needed to recreate the queue if it is auto-deleted
# after going unconsumed for the idle window. See "Durable Queue Lifetime"
# (consuming-events.md#durable-queue-lifetime).
UpdateDurableQueue#
Replaces the filters on an existing durable queue.
new_filter_groups is the complete set of filters the queue should have. The update replaces the queue’s whole filter set: afterwards the queue receives exactly the events new_filter_groups selects, and nothing that only an earlier filter selected. The service reads the queue’s current filters from the message broker, so you do not send them. Repeating an update with the same new_filter_groups changes nothing, and messages already in the queue stay there.
new_filter_groupsmust contain at least one filter group. An empty list is rejected withINVALID_ARGUMENT.current_filter_groupsis deprecated and ignored. Omit it. A request that still sends it keeps working, and what it carries has no effect.Concurrent updates of the same queue are not serialized. Two at once can leave a mix of both filter sets; the next update leaves exactly its own filters.
An
INTERNALstatus whose message says the queue’s current filters could not be read means nothing was changed. Retry the update.
To make an existing queue’s filters match the ones you store, call CreateDurableQueue with your queue_id and filter groups, then UpdateDurableQueue with the same filter groups.
This RPC is authorized by the create-durable-queues permission — the same one that authorizes creating a queue. There is no separate update permission. By default the permission is deployment-wide, so a principal that can create durable queues can re-filter any existing one. Where the deployment enforces per-queue authorization, the check names the queue as DurableQueue::"<queue_id>", so a principal can re-filter only a queue it is granted create-durable-queues on, or every queue if its grant covers every queue. A refused update returns PERMISSION_DENIED whether or not the queue exists.
Request: UpdateDurableQueueRequest
message UpdateDurableQueueRequest {
string queue_id = 1;
repeated FilterGroup current_filter_groups = 2 [deprecated = true];
repeated FilterGroup new_filter_groups = 3;
}
Field |
Type |
Description |
|---|---|---|
|
|
Required. The id of the durable queue to update. It must match |
|
|
Deprecated and ignored. Omit it. |
|
|
Required, with at least one filter group. The complete set of filters the queue should have; it replaces the queue’s existing filters. |
Response: UpdateDurableQueueResponse
message UpdateDurableQueueResponse {}
DeleteDurableQueue#
Deletes a durable queue.
Request: DeleteDurableQueueRequest
message DeleteDurableQueueRequest {
string queue_id = 1;
}
Field |
Type |
Description |
|---|---|---|
|
|
Required. The id of the durable queue to delete. It must match |
Response: DeleteDurableQueueResponse
message DeleteDurableQueueResponse {}
Example:
request = event_consumer_pb2.DeleteDurableQueueRequest(
queue_id=queue_id
)
stub.DeleteDurableQueue(request, metadata=metadata)
ManageDeadLetteredEvents#
Operator-facing, bidirectional streaming RPC for inspecting and triaging dead-lettered events across the deployment. The server streams dead-lettered messages with triage metadata; the client replies with per-message actions (ACTION_REPLAY, ACTION_DISCARD, ACTION_LEAVE) keyed by a session-scoped delivery tag; the server streams back a correlated outcome report for each action on a message the session holds.
gRPC-only: There is no REST endpoint for this RPC.
Required permission: event-consumer-service:inspect-and-replay-all-dlq. A caller lacking it is denied with PERMISSION_DENIED.
The stream returns INTERNAL, with the message Internal Server Error: The broker cancelled this stream's consumer. Reconnect to resume consuming., when the broker cancels this session’s consumer or kills the channel it lives on. Reconnect to resume triage: the dead-letter queue and everything in it normally survive, and dead-lettered messages that were in flight unacknowledged are redelivered once you reconnect. NOT_FOUND, on the stream or on the reconnect, means the deployment’s dead-letter queue is missing on the broker, which is an operator condition rather than something the caller can recover from.
This no-store bidirectional RPC is the permanent baseline DLQ-management surface; it requires no datastore beyond the message broker. It is the forward-compatible floor: future additive unary store-backed RPCs (list/filter/replay-by-id/stats) may be added later without changing this RPC. See Managing the Dead-Letter Queue (Operators) in the consuming-events guide.
Request: ManageDeadLetteredEventsRequest (stream)
message ManageDeadLetteredEventsRequest {
enum Action {
ACTION_UNSPECIFIED = 0;
ACTION_REPLAY = 1; // Republish to origin queue (confirm-gated, no-loss); refused when undecodable_body is set
ACTION_DISCARD = 2; // Ack and drop from the dead-letter queue
ACTION_LEAVE = 3; // Leave in place for later triage
}
message ActionEntry {
int64 delivery_tag = 1; // Session-scoped tag of the message to act on
Action action = 2; // Action to take on that message
}
repeated ActionEntry actions = 1; // Empty on the first (opening) message
}
The first message opens the stream and carries no actions (the dead-letter queue is global, so there is no queue_id). Subsequent messages carry one or more ActionEntry items, each pairing a session-scoped delivery_tag with the Action to take, so multiple dead-lettered messages can be actioned in a single request.
Request fields:
Field |
Type |
Description |
|---|---|---|
|
|
Per-delivery-tag triage actions. Empty on the opening message. |
|
|
Session-scoped tag of the dead-lettered message, as received in a |
|
|
|
Response: ManageDeadLetteredEventsResponse (stream)
Each response carries exactly one of two arms via a oneof payload: a dead_lettered_event (a browse item) or an action_outcome (a report correlated to a requested action by delivery_tag).
message ManageDeadLetteredEventsResponse {
message DeadLetteredEvent {
enum Reason {
REASON_UNSPECIFIED = 0; // No x-death record (e.g. published straight to the dead-letter exchange), or an unmapped reason
REASON_DELIVERY_LIMIT = 1; // Exceeded its redelivery limit
REASON_EXPIRED = 2; // Outlived the queue's configured message TTL
REASON_MAXLEN = 3; // Evicted when queue reached its maximum length
}
// Describes a message whose body could not be decoded into an event.
// Never carries the body itself.
message UndecodableBody {
enum Problem {
PROBLEM_UNSPECIFIED = 0; // A reason no other value describes
PROBLEM_NOT_UTF8 = 1; // Not valid UTF-8 (UTF-16/UTF-32 JSON with a byte-order mark; without one, usually PROBLEM_NOT_JSON)
PROBLEM_NOT_JSON = 2; // Valid UTF-8 but not JSON (an empty body included)
PROBLEM_NOT_AN_OBJECT = 3; // JSON whose top-level value is not an object
PROBLEM_INVALID_STRING = 4; // A JSON object holding a string not encodable as UTF-8, e.g. "\ud800"
PROBLEM_PARSER_LIMIT = 5; // Nests too deeply, or holds an integer with too many digits
}
int64 size_bytes = 1; // Size of the message body, in bytes
Problem problem = 2; // Why the body could not be decoded
}
Reason reason = 1;
string origin_queue = 2; // Origin durable queue name; empty with no x-death record
int64 delivery_count = 3; // Broker delivery attempts
google.protobuf.Timestamp dead_lettered_at = 4; // When it was dead-lettered; unset with no x-death record
Event event = 5; // event_type, principal_identity, message; unset when undecodable_body is set
string resource_id = 6; // Resource id of the event; empty when undecodable_body is set
int64 delivery_tag = 7; // Session-scoped tag to act on
UndecodableBody undecodable_body = 8; // Set only when the body could not be decoded into an event
}
message ActionOutcome {
// Values may be added: treat an outcome value your generated code does
// not recognize as "not applied".
enum Outcome {
OUTCOME_UNSPECIFIED = 0;
OUTCOME_REPLAYED = 1; // Republished to origin queue; DLQ copy removed
OUTCOME_DISCARDED = 2; // Acked and dropped from the dead-letter queue
OUTCOME_LEFT = 3; // Left in place for later triage
OUTCOME_ORIGIN_QUEUE_DELETED = 4; // Origin queue gone, or origin_queue empty (no replay target: discard or leave it); DLQ copy left in place (no-loss)
OUTCOME_REPLAY_FAILED = 5; // Replay unconfirmed; DLQ copy left in place (no-loss)
OUTCOME_REPLAY_REFUSED = 6; // undecodable_body is set; nothing changed, the session still holds the message
}
int64 delivery_tag = 1; // Matches the tag the client sent
Outcome outcome = 2;
string detail = 3; // Human-readable context; may be empty
}
oneof payload {
DeadLetteredEvent dead_lettered_event = 1;
ActionOutcome action_outcome = 2;
}
}
DeadLetteredEvent (browse item) fields:
Field |
Type |
Description |
|---|---|---|
|
|
Why the message was dead-lettered: |
|
|
Name of the durable queue the message was dead-lettered from. Empty when the message carries no |
|
|
Broker delivery attempts before dead-lettering. |
|
|
When the message was dead-lettered. Not set when the message carries no |
|
|
The dead-lettered event ( |
|
|
Resource id associated with the event. Empty when |
|
|
Session-scoped tag the client sends back to act on the message. Live-stream-only. |
|
|
Set only when the server could not decode the message’s body into an event. The body itself is never returned. |
|
|
The size of the message body, in bytes. |
|
|
Why the body could not be decoded: |
ActionOutcome (report) fields:
Field |
Type |
Description |
|---|---|---|
|
|
Session-scoped tag identifying the message the outcome pertains to. |
|
|
|
|
|
Human-readable context, such as the class of the failure for |
Each ACTION_REPLAY, ACTION_DISCARD, or ACTION_LEAVE on a delivery tag the session still holds produces exactly one correlated action_outcome, keyed by delivery_tag. An action on a tag the session does not hold, because the tag is unknown or was already actioned, produces no action_outcome, and neither does an ACTION_UNSPECIFIED action or an action value the server does not recognize. ACTION_REPLAY is confirm-gated and no-loss: the dead-letter copy is removed only after the publish to the origin queue is confirmed. An ACTION_REPLAY whose origin queue has been deleted does not lose the message — the server leaves the dead-letter copy in place (ACTION_LEAVE semantics) and reports an OUTCOME_ORIGIN_QUEUE_DELETED outcome to the operator rather than silently handling it; re-create the origin queue to obtain a viable replay target. A message with an empty origin_queue, such as one published straight to the dead-letter exchange, has no replay target even when its event is set: an ACTION_REPLAY of it reports OUTCOME_ORIGIN_QUEUE_DELETED and leaves it in place, so discard it or leave it. A replay that cannot be confirmed, for example because the broker rejected the republish, reports OUTCOME_REPLAY_FAILED and likewise leaves the copy in place. After an OUTCOME_ORIGIN_QUEUE_DELETED or an OUTCOME_REPLAY_FAILED, the message goes back to the dead-letter queue and is streamed again at once, possibly to the same session, so a loop that replays every entry must not replay it blindly: re-create the origin queue first, or discard the message.
A message whose body the server could not decode into an event is streamed like any other, as a dead_lettered_event with undecodable_body set and no event. Such a message reaches the dead-letter queue by expiring or overflowing on a durable queue before a consumer takes it, or by being published straight to the dead-letter exchange. ACTION_DISCARD and ACTION_LEAVE work on it as on any other message. ACTION_REPLAY is refused with OUTCOME_REPLAY_REFUSED, because the message could never be delivered from its origin queue: nothing changes, and the session still holds the message under the same delivery_tag, so follow up with ACTION_DISCARD or ACTION_LEAVE. If the stream ends first, the message returns to the dead-letter queue like any other message the session holds.
A session holds each message it receives until an action on it gets an outcome other than OUTCOME_REPLAY_REFUSED, or until the stream ends, so a message whose replay was refused, or one that gets no action, stays held. The number of messages a session holds is bounded by OMNI_EVENTS_DEAD_LETTER_BROWSE_PREFETCH_COUNT (Helm rabbitmq.dead_letter_browse_prefetch_count, 10 by default), and while the session holds that many, the server streams no new dead_lettered_event. Act on every message you receive, or end the stream to return the messages it holds to the dead-letter queue.
Outcome values may be added. A client whose generated code predates a value receives it as a number its code does not name; treat any outcome value your code does not recognize as “not applied”: the requested action did not take effect.
Scope: ACTION_REPLAY returns a message to the exact origin queue it dead-lettered from, taken from the message’s own broker metadata — an operator cannot redirect a replay to another queue. The permission is coarse and grants deployment-wide dead-letter visibility and action by design; re-consume of a replayed message still runs the existing per-event authorization check.
Consumer REST API#
Endpoints#
GET /api/v1beta/events/stream#
Streams events using a non-durable queue (Server-Sent Events).
Query Parameters:
Parameter |
Type |
Required |
Description |
|---|---|---|---|
|
string (JSON) |
No |
URL-encoded JSON array of FilterGroup objects |
|
string (JSON) |
No |
URL-encoded JSON array of the previous FilterGroup objects (required when updating filters) |
|
string |
No |
Deprecated — send the token in the |
Headers:
Header |
Required |
Description |
|---|---|---|
|
No |
The recommended way to send a reconnect token back. Carries exactly the value the |
Reconnecting and updating filters: sending previous_filter_groups alongside filter_groups is what makes a request a filter update, whichever carrier brought the token. A request that carries the token alone resumes the stream on the filters it already has. A browser’s automatic reconnect replays the URL the stream was opened on and sends the token in the Last-Event-ID header, so when that URL carried only the initial filter_groups, the header path resumes as-is.
The token identifies the queue. It is not a secret and grants no access on its own: presenting it requires a valid bearer token, and every delivered event is authorized against the presenting principal’s own permissions. Consumers attached to one queue split its events between them, so keeping the token within the application that received it is what ensures only that application’s own authorized consumers share those events.
FilterGroup Structure:
[
{
"event_type": "string",
"filters": [
{
"filter_type": "starts_with_greedy",
"resource_id": "/path/"
}
]
}
]
Filter Types:
eq: Exact matchstarts_with_lazy: Shallow prefix matchstarts_with_greedy: Deep prefix matchunspecified: Defaults toeq
Response (text/event-stream):
event: connected
id: my-queue-id
data: {"message":"Connection established","reconnect_token":"my-queue-id"}
event: event
data: {"event_type":"myapp.user.created","principal_identity":"user@example.com","occurred_at":"2024-10-16T14:30:00Z","published_at":"2024-10-16T14:30:01Z","message":{"user_id":"123"}}
The initial connected event includes the reconnect_token (which is the queue ID). Save this token for reconnection; it also lets multiple clients consume from the same queue in parallel, splitting its events between them.
Errors on an open stream: the HTTP response status is 200 OK as soon as the stream opens, so an error that ends the stream is not an HTTP status — it is delivered as an SSE event named error whose data carries a message and the status_code it would have had, after which the stream closes. Handle the error event explicitly; a client that watches only for event receives no further events and no visible failure. The stream ends this way with status_code 500 and the message Internal Server Error: The broker cancelled this stream's consumer. Reconnect to resume consuming. when the broker takes this stream’s consumer away — reconnect with your reconnect_token, and the queue normally survives with its buffered backlog — and with status_code 404 when the queue itself is gone, in which case start a fresh stream from your filter_groups with no reconnect_token. The 500 does not by itself confirm that the queue survived: if the queue no longer exists, the reconnect returns 404, and the recovery for a queue that is gone applies. See Handle Disconnections Gracefully.
Example:
# Simple consumption
curl -N https://your-consumer-service.example.com/api/v1beta/events/stream \
-H "Authorization: Bearer YOUR_TOKEN"
# With filtering
FILTERS='[{"event_type":"myapp.user.created","filters":[]}]'
curl -N https://your-consumer-service.example.com/api/v1beta/events/stream \
-H "Authorization: Bearer YOUR_TOKEN" \
-G --data-urlencode "filter_groups=$FILTERS"
GET /api/v1beta/events/stream-durable#
Streams events from a durable queue (Server-Sent Events). Messages are acknowledged to the broker automatically by the server after a short delay. Clients do not need to acknowledge individual messages.
If your application requires explicit client-side acknowledgment of each message (e.g. to guarantee at-least-once processing), or needs to negatively acknowledge a message it failed to process so the broker redelivers it, use the gRPC ConsumeDurableEventsWithAcks RPC instead. Its request carries both ack_delivery_tags and nack_delivery_tags. That RPC uses a bidirectional stream, which keeps the acknowledgment on the same connection and works correctly across multi-pod deployments — a delivery tag is scoped to the broker channel that issued it, so a separate REST call could not settle it reliably.
The consume-durable-queues check runs before the stream opens and before the queue is looked up: a caller it refuses gets an HTTP 403 response, whether or not the queue exists. Where the deployment enforces per-queue authorization, the check names this queue.
For a permitted caller, this endpoint returns 404 when the queue no longer exists — either the queue_id is wrong, or the queue was auto-deleted after going without an attached consumer for the deployment’s idle window (24 hours by default); recover by creating the queue again with the same queue_id and the same filter_groups, then resuming. See Durable Queue Lifetime.
Errors on an open stream: once the stream has opened its HTTP response status is 200 OK, so a status that arises after that point — including the 404 above, when the queue is deleted or reaped out from under a running stream — is not an HTTP status. It is delivered as an SSE event named error whose data carries a message and the status_code it would have had, after which the stream closes. Handle the error event explicitly; a client that watches only for event receives no further events and no visible failure. The stream also ends this way with status_code 500 and the message Internal Server Error: The broker cancelled this stream's consumer. Reconnect to resume consuming. when the broker takes this stream’s consumer away — reconnect with the same queue_id, and do not recreate the queue; the queue normally survives with everything buffered in it. The 500 does not by itself confirm that the queue survived: if the queue no longer exists, the reconnect returns 404, and the recovery for a queue that is gone applies. See Handle Disconnections Gracefully.
Query Parameters:
Parameter |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
ID of the durable queue. It must match |
Response (text/event-stream):
event: connected
id: my-queue-id
data: {"message":"Connection established","reconnect_token":"my-queue-id"}
event: event
data: {"event_type":"myapp.user.created",...}
event: event
data: {"event_type":"myapp.user.updated",...}
Example:
curl -N https://your-consumer-service.example.com/api/v1beta/events/stream-durable?queue_id=my-queue-id \
-H "Authorization: Bearer YOUR_TOKEN"
POST /api/v1beta/queues/durable#
Creates a durable queue.
This endpoint has ensure-exists semantics when you supply queue_id: it creates the queue if no queue with that id exists, and is a no-op if a queue with that id already exists. The created field in the response tells you which path the call took. An invalid queue_id (one that fails the ^[A-Za-z0-9._-]{1,200}$ validation rule) is rejected with 422 Unprocessable Entity.
A queue_id you supply must be unique across the whole deployment. Queue ids share a single namespace. If a queue with your id already exists — whoever created it, and whatever filter_groups you send — and you are permitted to create it, this endpoint attaches to that queue and returns it with its existing filters and created set to false. Two callers that pick the same id and are both permitted it share one queue: each receives the other’s events, and either can delete it if permitted to. Use a UUID, or a namespaced convention such as <service-name>-<purpose>; never a bare generic word like events.
Because an existing queue keeps its existing filters, a client that needs the queue to have exactly the filter groups it stores calls this endpoint with its queue_id and those filter groups, then PATCH /api/v1beta/queues/durable with the same filter groups. The update replaces whatever filters the queue has with them.
Authorization. Three permissions govern durable queues: create-durable-queues (creating a queue, attaching to an existing one, and updating a queue’s filters), consume-durable-queues and delete-durable-queues. Each operation is checked before the queue is looked up, so a refused call gets 403 Forbidden whether or not the queue exists.
By default these permissions are deployment-wide: any authenticated principal that knows a durable queue’s id can create (attach to), consume from, update or delete that queue if it holds the deployment-wide permission, and receives only the events its own event permissions allow. Treat a
queue_idas configuration, not as a secret that protects a queue.Where the deployment enforces per-queue authorization, each check names the queue as
DurableQueue::"<queue_id>", and a principal can do each of these only if a permission-service policy grants it that permission on that queue, or on every queue; knowing the id grants nothing by itself. A request that omitsqueue_idis checked against the id the server assigns, so it needs a grant covering every queue. See Per-Queue Authorization.
Request Body:
{
"filter_groups": [
{
"event_type": "string",
"filters": [
{
"filter_type": "starts_with_greedy",
"resource_id": "/path/"
}
]
}
],
"queue_id": "myapp-uploads-queue"
}
queue_id is optional and client-specified. Omit it or send an empty string to have the server generate one. When supplied, it must match ^[A-Za-z0-9._-]{1,200}$ and must be unique across the deployment, as described above.
The HTTP status code signals which path the request took:
201 Created— the request created a new durable queue (createdistrue). This is the case when you supply aqueue_idthat does not yet exist.200 OK— the request resolved to a queue that already existed (createdisfalse), or you omittedqueue_idand the server generated one (createdistrue).
Response (201 Created, created-new path):
{
"queue_id": "durable-queue-abc123",
"created": true
}
created is true when this call created a new queue and false when an existing queue with the supplied queue_id was found and returned unchanged (a no-op). Branch on the status code to tell the two apart.
Example:
curl -X POST https://your-consumer-service.example.com/api/v1beta/queues/durable \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"filter_groups": [
{
"event_type": "myapp.file.uploaded",
"filters": [
{
"filter_type": "starts_with_greedy",
"resource_id": "/uploads/"
}
]
}
],
"queue_id": "myapp-uploads-queue"
}'
PATCH /api/v1beta/queues/durable#
Replaces the filters on a durable queue.
new_filter_groups is the complete set of filters the queue should have: afterwards the queue receives exactly the events those filters select, and nothing that only an earlier filter selected. The service reads the queue’s current filters from the message broker, so you do not send them. Sending the same filters again changes nothing, and messages already in the queue stay there.
new_filter_groupsmust contain at least one filter group. An empty list is rejected with422 Unprocessable Entity.current_filter_groupsis deprecated and ignored. Omit it. If sent, it must still be well-formed filter groups (at most 100); their content has no effect.Concurrent updates of the same queue are not serialized. Two at once can leave a mix of both filter sets; the next update leaves exactly its own filters.
A
500whose message says the queue’s current filters could not be read means nothing was changed. Retry the update.
This endpoint is authorized by the create-durable-queues permission — the same one that authorizes creating a queue. There is no separate update permission. Where the deployment enforces per-queue authorization, the check names the queue as DurableQueue::"<queue_id>". A refused update gets 403 Forbidden whether or not the queue exists.
Request Body:
{
"queue_id": "string",
"new_filter_groups": [ /* array of FilterGroup, at least one */ ]
}
queue_id must match ^[A-Za-z0-9._-]{1,200}$; an id that does not is rejected with 422 Unprocessable Entity before the broker is contacted. Queue ids share one deployment-wide namespace, so this must be the id of a queue that was created by (or for) your client; see POST /api/v1beta/queues/durable.
Response (200 OK):
{
"success": true
}
DELETE /api/v1beta/queues/durable#
Deletes a durable queue.
Query Parameters:
Parameter |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
ID of the queue to delete. It must match |
Response (200 OK):
{
"success": true
}
Example:
curl -X DELETE "https://your-consumer-service.example.com/api/v1beta/queues/durable?queue_id=my-queue-id" \
-H "Authorization: Bearer YOUR_TOKEN"
Error Handling#
Common HTTP Status Codes#
Code |
Meaning |
Action |
|---|---|---|
200 |
Success |
Process response |
400 |
Bad Request |
Check request format |
401 |
Unauthorized |
Refresh authentication token |
403 |
Forbidden |
Check permissions |
404 |
Not Found |
Check queue_id or endpoint |
413 |
Content Too Large (Event Aggregation Service) |
Don’t resend the request unchanged: split it into smaller batches or shrink its events. See When a Request Breaks a Limit |
422 |
Validation Error |
Check request validation |
500 |
Internal Server Error |
Retry after delay, contact support. On an SSE consume stream this status arrives inside an |
503 |
Service Unavailable (Event Consumer Service): no capacity for the request right now |
Retry with exponential backoff |
Common gRPC Status Codes#
Code |
Meaning |
Action |
|---|---|---|
OK |
Success |
Process response |
INVALID_ARGUMENT |
Bad request |
Check request format |
UNAUTHENTICATED |
Auth failed |
Refresh token |
PERMISSION_DENIED |
No permission |
Check permissions |
NOT_FOUND |
Resource not found |
Check identifiers |
RESOURCE_EXHAUSTED |
From the Event Aggregation Service: the request is too large. From the Event Consumer Service: no capacity for the request right now |
Publisher: don’t resend the request unchanged; see When a Request Breaks a Limit. Consumer: retry with backoff |
UNAVAILABLE |
Connection lost (e.g. load balancer idle timeout) |
Reconnect automatically |
INTERNAL |
Server error |
Retry after delay. On a consume stream, |
Protocol Buffer Files#
The complete protocol buffer definitions are available in the released archive or protobuf registry.
Publisher Proto: nvidia/omniverse/notifications/publisher/v1beta/event_publisher.proto
Consumer Proto: nvidia/omniverse/notifications/consumer/v1beta/event_consumer.proto
OpenAPI Specifications#
Full OpenAPI specifications are available in the released archive
Rate Limits and Quotas#
There are no enforced rate limits at the application level. However:
The Event Consumer Service may return
RESOURCE_EXHAUSTED(gRPC) or503(HTTP) when it has no capacity for the request; retry with exponential backoffThe Event Aggregation Service has no such response: its
RESOURCE_EXHAUSTED(gRPC) and413(HTTP) mean the request is too large, and resending it unchanged fails the same way. See When a Request Breaks a LimitFor high-volume publishing, use batch publishing for better performance
Versioning#
The current API version is v1beta.
APIs are under the
/api/v1beta/path (REST) orv1betapackage (gRPC)Beta APIs may change, but we will maintain backward compatibility where possible
When the API reaches GA, it will be versioned as
v1
Service Health#
Both services expose health check endpoints:
gRPC: Standard gRPC health checking protocol
REST:
GET /health- Service health statusReturns 200 if healthy
Metrics and Monitoring#
Both services expose OpenTelemetry metrics and traces when telemetry is enabled via configuration.
Next Steps#
Try the Getting Started Guide for hands-on examples
Learn about Publishing Events
Learn about Consuming Events
Understand Permissions & Authorization