Client API

Complete reference for all client-facing methods, server-to-client events, data models, and error codes. Source definitions are in the Notiway Common repository.

Client-to-Server Methods

These are methods the client invokes on the Notiway hub using connection.invoke(...).


Subscribe

Join a Tenant or Group audience to start receiving notifications routed to it. Persisted notifications for that audience are replayed immediately after a successful subscribe.

Signature:

Subscribe(subscriptionDto: SubscriptionDto) → void

Parameters:

FieldTypeRequiredDescription
audienceTypenumberYes2 for Tenant, 3 for Group
audienceValuestringYesTenant ID or group name
tenantIdstringNoRequired when joining a tenant-scoped group

Behavior:

  • Adds the connection to the target audience
  • Replays persisted, non-expired notifications for that audience
  • Tenant validation is enforced when a Tenant Validation plugin is configured

Errors:

CodeWhen
UnauthorizedAuthentication failed or token expired
ForbiddenTenant validation rejected the request
WrongInputInvalid audienceType or missing audienceValue
NotSupportedAttempted to subscribe to Global, User, or Connection (these are automatic)

Example:

// Join a tenant audience
await connection.invoke("Subscribe", {
    audienceType: 2,
    audienceValue: "acme-corp"
});

// Join a named group
await connection.invoke("Subscribe", {
    audienceType: 3,
    audienceValue: "admins"
});

// Join a tenant-scoped group
await connection.invoke("Subscribe", {
    audienceType: 3,
    audienceValue: "admins",
    tenantId: "acme-corp"
});
// Join a tenant audience
await connection.InvokeAsync("Subscribe", new
{
    AudienceType = 2,
    AudienceValue = "acme-corp"
});

// Join a named group
await connection.InvokeAsync("Subscribe", new
{
    AudienceType = 3,
    AudienceValue = "admins"
});

// Join a tenant-scoped group
await connection.InvokeAsync("Subscribe", new
{
    AudienceType = 3,
    AudienceValue = "admins",
    TenantId = "acme-corp"
});
// Join a tenant audience
await connection.invoke("Subscribe", args: [
    {"audienceType": 2, "audienceValue": "acme-corp"}
]);

// Join a named group
await connection.invoke("Subscribe", args: [
    {"audienceType": 3, "audienceValue": "admins"}
]);

// Join a tenant-scoped group
await connection.invoke("Subscribe", args: [
    {"audienceType": 3, "audienceValue": "admins", "tenantId": "acme-corp"}
]);
// Join a tenant audience
try await connection.invoke("Subscribe", arguments: [
    ["audienceType": 2, "audienceValue": "acme-corp"]
])

// Join a named group
try await connection.invoke("Subscribe", arguments: [
    ["audienceType": 3, "audienceValue": "admins"]
])

// Join a tenant-scoped group
try await connection.invoke("Subscribe", arguments: [
    ["audienceType": 3, "audienceValue": "admins", "tenantId": "acme-corp"]
])
// Join a tenant audience
connection.invoke("Subscribe",
    mapOf("audienceType" to 2, "audienceValue" to "acme-corp"))

// Join a named group
connection.invoke("Subscribe",
    mapOf("audienceType" to 3, "audienceValue" to "admins"))

// Join a tenant-scoped group
connection.invoke("Subscribe",
    mapOf("audienceType" to 3, "audienceValue" to "admins", "tenantId" to "acme-corp"))

Unsubscribe

Leave a Tenant or Group audience. The client stops receiving notifications routed to that audience.

Signature:

Unsubscribe(subscriptionDto: SubscriptionDto) → void

Parameters:

Same as Subscribe.

Errors:

CodeWhen
WrongInputInvalid audienceType or missing audienceValue
NotSupportedAttempted to unsubscribe from Global, User, or Connection
NotFoundConnection was not a member of the specified audience

Example:

await connection.invoke("Unsubscribe", {
    audienceType: 2,
    audienceValue: "acme-corp"
});
await connection.InvokeAsync("Unsubscribe", new
{
    AudienceType = 2,
    AudienceValue = "acme-corp"
});
await connection.invoke("Unsubscribe", args: [
    {"audienceType": 2, "audienceValue": "acme-corp"}
]);
try await connection.invoke("Unsubscribe", arguments: [
    ["audienceType": 2, "audienceValue": "acme-corp"]
])
connection.invoke("Unsubscribe",
    mapOf("audienceType" to 2, "audienceValue" to "acme-corp"))

MarkAsRead

Update the read state of a persisted notification. The change is synced to all of the user’s connected devices via EventReadStatusChange.

Signature:

MarkAsRead(audienceId: string, eventId: string, isRead?: boolean) → void

Parameters:

ParameterTypeRequiredDefaultDescription
audienceIdstringYesAudience identifier from the notification’s audienceId field
eventIdstringYesNotification ID from the notification’s id field
isReadbooleanNotruePass false to mark as unread

Errors:

CodeWhen
NotFoundNotification does not exist or has expired
WrongInputMissing or empty audienceId or eventId
ErrorStorage plugin failed

Example:

// Mark as read
await connection.invoke("MarkAsRead", notification.audienceId, notification.id);

// Mark as unread
await connection.invoke("MarkAsRead", notification.audienceId, notification.id, false);
// Mark as read
await connection.InvokeAsync("MarkAsRead", notification.AudienceId, notification.Id);

// Mark as unread
await connection.InvokeAsync("MarkAsRead", notification.AudienceId, notification.Id, false);
// Mark as read
await connection.invoke("MarkAsRead", args: [notification.audienceId, notification.id]);

// Mark as unread
await connection.invoke("MarkAsRead", args: [notification.audienceId, notification.id, false]);
// Mark as read
try await connection.invoke("MarkAsRead", arguments: [notification.audienceId, notification.id])

// Mark as unread
try await connection.invoke("MarkAsRead", arguments: [notification.audienceId, notification.id, false])
// Mark as read
connection.invoke("MarkAsRead", notification.audienceId, notification.id)

// Mark as unread
connection.invoke("MarkAsRead", notification.audienceId, notification.id, false)

MarkAsDeleted

Update the deleted state of a persisted notification. Deleted notifications are excluded from replay. The change is synced to all of the user’s connected devices via EventDeletedStatusChange.

Signature:

MarkAsDeleted(audienceId: string, eventId: string, isDeleted?: boolean) → void

Parameters:

ParameterTypeRequiredDefaultDescription
audienceIdstringYesAudience identifier from the notification’s audienceId field
eventIdstringYesNotification ID from the notification’s id field
isDeletedbooleanNotruePass false to restore a deleted notification

Errors:

CodeWhen
NotFoundNotification does not exist or has expired
WrongInputMissing or empty audienceId or eventId
ErrorStorage plugin failed

Example:

// Mark as deleted
await connection.invoke("MarkAsDeleted", notification.audienceId, notification.id);

// Restore
await connection.invoke("MarkAsDeleted", notification.audienceId, notification.id, false);
// Mark as deleted
await connection.InvokeAsync("MarkAsDeleted", notification.AudienceId, notification.Id);

// Restore
await connection.InvokeAsync("MarkAsDeleted", notification.AudienceId, notification.Id, false);
// Mark as deleted
await connection.invoke("MarkAsDeleted", args: [notification.audienceId, notification.id]);

// Restore
await connection.invoke("MarkAsDeleted", args: [notification.audienceId, notification.id, false]);
// Mark as deleted
try await connection.invoke("MarkAsDeleted", arguments: [notification.audienceId, notification.id])

// Restore
try await connection.invoke("MarkAsDeleted", arguments: [notification.audienceId, notification.id, false])
// Mark as deleted
connection.invoke("MarkAsDeleted", notification.audienceId, notification.id)

// Restore
connection.invoke("MarkAsDeleted", notification.audienceId, notification.id, false)

Server-to-Client Events

These are events pushed from Notiway to connected clients. Register handlers using connection.on(...).


Notification Handlers

Notifications are delivered to handlers that match the notification’s type field. Register one handler per notification type.

Signature:

connection.on(notificationType: string, handler: (notification: NotificationDto) → void)

Payload: See NotificationDto.

Example:

connection.on("order-shipped", (notification) => {
    console.log(notification.id);
    console.log(notification.body);
    console.log(notification.isRead);
});
connection.On<dynamic>("order-shipped", notification =>
{
    Console.WriteLine($"ID: {notification.id}");
    Console.WriteLine($"Body: {notification.body}");
    Console.WriteLine($"Read: {notification.isRead}");
});
connection.on("order-shipped", (arguments) {
    final notification = arguments?[0];
    print("ID: ${notification['id']}");
    print("Body: ${notification['body']}");
    print("Read: ${notification['isRead']}");
});
connection.on("order-shipped") { arguments in
    let notification = arguments[0] as! [String: Any]
    print("ID: \(notification["id"]!)")
    print("Body: \(notification["body"]!)")
    print("Read: \(notification["isRead"]!)")
}
connection.on("order-shipped") { notification: Map<String, Any> ->
    println("ID: ${notification["id"]}")
    println("Body: ${notification["body"]}")
    println("Read: ${notification["isRead"]}")
}
Register handlers before they’re needed. Handlers for Global, User, and Connection audiences must be registered before calling start(). Handlers for Tenant and Group audiences must be registered before calling Subscribe. Persisted notifications are replayed immediately — unregistered handlers will miss them.

EventReadStatusChange

Fired when a notification’s read state is changed on another device. Use this to keep all connected clients in sync.

Payload:

FieldTypeDescription
eventIdstringThe notification ID
isReadbooleanNew read state

Example:

connection.on("EventReadStatusChange", (change) => {
    updateNotificationStatus(change.eventId, { isRead: change.isRead });
});
connection.On<dynamic>("EventReadStatusChange", change =>
{
    UpdateNotificationStatus(change.eventId, change.isRead);
});
connection.on("EventReadStatusChange", (arguments) {
    final change = arguments?[0];
    updateNotificationStatus(change['eventId'], isRead: change['isRead']);
});
connection.on("EventReadStatusChange") { arguments in
    let change = arguments[0] as! [String: Any]
    updateNotificationStatus(change["eventId"] as! String, isRead: change["isRead"] as! Bool)
}
connection.on("EventReadStatusChange") { change: Map<String, Any> ->
    updateNotificationStatus(change["eventId"] as String, isRead = change["isRead"] as Boolean)
}

EventDeletedStatusChange

Fired when a notification’s deleted state is changed on another device.

Payload:

FieldTypeDescription
eventIdstringThe notification ID
isDeletedbooleanNew deleted state

Example:

connection.on("EventDeletedStatusChange", (change) => {
    updateNotificationStatus(change.eventId, { isDeleted: change.isDeleted });
});
connection.On<dynamic>("EventDeletedStatusChange", change =>
{
    UpdateNotificationStatus(change.eventId, change.isDeleted);
});
connection.on("EventDeletedStatusChange", (arguments) {
    final change = arguments?[0];
    updateNotificationStatus(change['eventId'], isDeleted: change['isDeleted']);
});
connection.on("EventDeletedStatusChange") { arguments in
    let change = arguments[0] as! [String: Any]
    updateNotificationStatus(change["eventId"] as! String, isDeleted: change["isDeleted"] as! Bool)
}
connection.on("EventDeletedStatusChange") { change: Map<String, Any> ->
    updateNotificationStatus(change["eventId"] as String, isDeleted = change["isDeleted"] as Boolean)
}

Data Models

Models used in hub method parameters and notification payloads. Source: Notiway.Common.Core.


NotificationDto

The notification object received by client handlers.

{
  "id": "order-service-order-shipped-2025-01-15T10:00:00Z",
  "type": "order-shipped",
  "body": { "orderId": "ORD-12345", "message": "Your order has been shipped!" },
  "routing": {
    "tenantId": null,
    "audienceValue": "user-123",
    "audienceId": "user:user-123",
    "audienceType": 4
  },
  "metadata": {
    "timeStamp": "2025-01-15T10:00:00Z",
    "producer": "order-service",
    "isPersisted": true,
    "persistedTTL": "2025-01-15T10:30:00Z"
  },
  "audienceId": "user:user-123",
  "isRead": false,
  "isDeleted": false
}
FieldTypeDescription
idstringUnique identifier. Format: {producer}-{type}-{timestamp}
typestringNotification type — matches the handler name
bodyobject | nullCustom JSON payload defined by the producer
routingRoutingDtoAudience targeting information
metadataMetadataDtoProducer and persistence metadata
audienceIdstringResolved audience identifier (see AudienceId format)
isReadbooleanWhether marked as read (persisted notifications only)
isDeletedbooleanWhether marked as deleted (persisted notifications only)

RoutingDto

Defines how a notification is routed to its target audience.

FieldTypeDescription
audienceTypeAudienceTypeTarget audience type
audienceValuestringTarget identifier (tenant ID, group name, user ID, or connection ID)
tenantIdstring | nullTenant scope — used for tenant-scoped groups
audienceIdstringComputed audience identifier (read-only)

AudienceId Format

The audienceId is computed from the audience type and value:

AudienceTypePatternExample
Global (1)globalglobal
Tenant (2)tenant:{value}tenant:acme-corp
Group (3)group:{value}group:admins
Group (3) with tenantgroup:{tenantId}:{value}group:acme-corp:admins
User (4)user:{value}user:user-123

MetadataDto

Metadata attached to every notification.

FieldTypeDefaultDescription
timeStampstring (ISO 8601)Current UTC timeWhen the notification was created
producerstringName of the producing service
isPersistedbooleanfalseWhether the notification is stored for offline replay
persistedTTLstring (ISO 8601) | null30 minutes from creationExpiry time after which persisted notifications are no longer replayed

SubscriptionDto

Parameter for Subscribe and Unsubscribe hub methods.

FieldTypeRequiredDescription
audienceTypenumberYes2 (Tenant) or 3 (Group)
audienceValuestringYesTenant ID or group name
tenantIdstringNoRequired for tenant-scoped groups

Enums


AudienceType

Defines the target scope for notification routing.

ValueNameDescription
1GlobalAll connected clients
2TenantAll clients in a specific tenant
3GroupAll clients in a named group
4UserAll connections of a specific user
5ConnectionA single specific connection

Processing (Error Codes)

Status codes returned by hub methods and used internally by plugins. Source: Processing.cs.

CodeNameDescription
10SuccessOperation completed successfully
30AlreadyExistsResource already exists (e.g., duplicate subscription)
31ConflictConflicting state prevented the operation
32RetryAttemptsReachedMaximum retry attempts exhausted
33UnauthorizedAuthentication failed or token expired
34ForbiddenAuthorized but not permitted (e.g., tenant validation failed)
40NotFoundResource not found
41NotSupportedOperation not supported (e.g., subscribing to Global audience)
45WrongInputInvalid or missing input parameters
50ErrorInternal error (plugin failure, unexpected exception)
70TimeoutOperation timed out
71ThrottledRate limited — too many requests

Error Handling

When a method call fails, Notiway throws a HubException that client libraries surface as an exception or error. The exception message contains the Processing code name.

try {
    await connection.invoke("Subscribe", {
        audienceType: 2,
        audienceValue: "acme-corp"
    });
} catch (error) {
    // error.message contains the Processing code name
    // e.g., "Forbidden", "WrongInput", "Unauthorized"
    console.error("Subscribe failed:", error.message);
}
try
{
    await connection.InvokeAsync("Subscribe", new
    {
        AudienceType = 2,
        AudienceValue = "acme-corp"
    });
}
catch (HubException ex)
{
    // ex.Message contains the Processing code name
    Console.WriteLine($"Subscribe failed: {ex.Message}");
}
try {
    await connection.invoke("Subscribe", args: [
        {"audienceType": 2, "audienceValue": "acme-corp"}
    ]);
} catch (error) {
    // error contains the Processing code name
    print("Subscribe failed: $error");
}
do {
    try await connection.invoke("Subscribe", arguments: [
        ["audienceType": 2, "audienceValue": "acme-corp"]
    ])
} catch {
    // error.localizedDescription contains the Processing code name
    print("Subscribe failed: \(error.localizedDescription)")
}
try {
    connection.invoke("Subscribe",
        mapOf("audienceType" to 2, "audienceValue" to "acme-corp"))
} catch (e: Exception) {
    // e.message contains the Processing code name
    println("Subscribe failed: ${e.message}")
}