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) → voidParameters:
| Field | Type | Required | Description |
|---|---|---|---|
audienceType | number | Yes | 2 for Tenant, 3 for Group |
audienceValue | string | Yes | Tenant ID or group name |
tenantId | string | No | Required 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:
| Code | When |
|---|---|
Unauthorized | Authentication failed or token expired |
Forbidden | Tenant validation rejected the request |
WrongInput | Invalid audienceType or missing audienceValue |
NotSupported | Attempted 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) → voidParameters:
Same as Subscribe.
Errors:
| Code | When |
|---|---|
WrongInput | Invalid audienceType or missing audienceValue |
NotSupported | Attempted to unsubscribe from Global, User, or Connection |
NotFound | Connection 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) → voidParameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
audienceId | string | Yes | — | Audience identifier from the notification’s audienceId field |
eventId | string | Yes | — | Notification ID from the notification’s id field |
isRead | boolean | No | true | Pass false to mark as unread |
Errors:
| Code | When |
|---|---|
NotFound | Notification does not exist or has expired |
WrongInput | Missing or empty audienceId or eventId |
Error | Storage 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) → voidParameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
audienceId | string | Yes | — | Audience identifier from the notification’s audienceId field |
eventId | string | Yes | — | Notification ID from the notification’s id field |
isDeleted | boolean | No | true | Pass false to restore a deleted notification |
Errors:
| Code | When |
|---|---|
NotFound | Notification does not exist or has expired |
WrongInput | Missing or empty audienceId or eventId |
Error | Storage 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"]}")
}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:
| Field | Type | Description |
|---|---|---|
eventId | string | The notification ID |
isRead | boolean | New 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:
| Field | Type | Description |
|---|---|---|
eventId | string | The notification ID |
isDeleted | boolean | New 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
}| Field | Type | Description |
|---|---|---|
id | string | Unique identifier. Format: {producer}-{type}-{timestamp} |
type | string | Notification type — matches the handler name |
body | object | null | Custom JSON payload defined by the producer |
routing | RoutingDto | Audience targeting information |
metadata | MetadataDto | Producer and persistence metadata |
audienceId | string | Resolved audience identifier (see AudienceId format) |
isRead | boolean | Whether marked as read (persisted notifications only) |
isDeleted | boolean | Whether marked as deleted (persisted notifications only) |
RoutingDto
Defines how a notification is routed to its target audience.
| Field | Type | Description |
|---|---|---|
audienceType | AudienceType | Target audience type |
audienceValue | string | Target identifier (tenant ID, group name, user ID, or connection ID) |
tenantId | string | null | Tenant scope — used for tenant-scoped groups |
audienceId | string | Computed audience identifier (read-only) |
AudienceId Format
The audienceId is computed from the audience type and value:
| AudienceType | Pattern | Example |
|---|---|---|
| Global (1) | global | global |
| Tenant (2) | tenant:{value} | tenant:acme-corp |
| Group (3) | group:{value} | group:admins |
| Group (3) with tenant | group:{tenantId}:{value} | group:acme-corp:admins |
| User (4) | user:{value} | user:user-123 |
MetadataDto
Metadata attached to every notification.
| Field | Type | Default | Description |
|---|---|---|---|
timeStamp | string (ISO 8601) | Current UTC time | When the notification was created |
producer | string | — | Name of the producing service |
isPersisted | boolean | false | Whether the notification is stored for offline replay |
persistedTTL | string (ISO 8601) | null | 30 minutes from creation | Expiry time after which persisted notifications are no longer replayed |
SubscriptionDto
Parameter for Subscribe and Unsubscribe hub methods.
| Field | Type | Required | Description |
|---|---|---|---|
audienceType | number | Yes | 2 (Tenant) or 3 (Group) |
audienceValue | string | Yes | Tenant ID or group name |
tenantId | string | No | Required for tenant-scoped groups |
Enums
AudienceType
Defines the target scope for notification routing.
| Value | Name | Description |
|---|---|---|
1 | Global | All connected clients |
2 | Tenant | All clients in a specific tenant |
3 | Group | All clients in a named group |
4 | User | All connections of a specific user |
5 | Connection | A single specific connection |
Processing (Error Codes)
Status codes returned by hub methods and used internally by plugins. Source: Processing.cs.
| Code | Name | Description |
|---|---|---|
10 | Success | Operation completed successfully |
30 | AlreadyExists | Resource already exists (e.g., duplicate subscription) |
31 | Conflict | Conflicting state prevented the operation |
32 | RetryAttemptsReached | Maximum retry attempts exhausted |
33 | Unauthorized | Authentication failed or token expired |
34 | Forbidden | Authorized but not permitted (e.g., tenant validation failed) |
40 | NotFound | Resource not found |
41 | NotSupported | Operation not supported (e.g., subscribing to Global audience) |
45 | WrongInput | Invalid or missing input parameters |
50 | Error | Internal error (plugin failure, unexpected exception) |
70 | Timeout | Operation timed out |
71 | Throttled | Rate 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}")
}