What is QueueBridge?
QueueBridge is a smart queue-management platform for hospitals, banks, ministries, and service centers. It coordinates four kinds of clients in real time: a kiosk where customers take tickets, an employee portal where staff call and complete tickets, large-screen displays in the waiting area, and an admin dashboard for operators.
This document covers everything an integration partner needs to embed QueueBridge into another product: how to authenticate, the full ticket lifecycle, the 62 REST endpoints exposed across 9 resource groups, and the SignalR real-time channel that broadcasts every state change as it happens.
System surface
| Component | Detail |
|---|---|
| Stack | ASP.NET Core 6.0 · C# · SignalR · JSON file persistence |
| Default base URL | http://localhost:5050 — configurable; production deployments typically expose behind a reverse proxy. |
| SignalR hub | /queueHub — WebSocket transport, also reachable as ws://<host>/queueHub |
| Content type | application/json; charset=utf-8 |
| JSON casing | camelCase — configured globally in Program.cs |
| Auth scheme | Bearer token (custom Base64 — see Authentication) |
| i18n | Server error messages are returned in Arabic for end-user display. Partners should treat them as opaque strings and translate as needed. |
| Interactive Swagger | Available in development at /swagger |
Conceptual model
Three nouns drive every workflow. Departments are top-level service categories (e.g. "Cardiology"). Each department has one or more Sub-Departments — the actual service desks (e.g. "Dr. Ali, Room 12"). Customers take Tickets against a sub-department. Each ticket carries a unique displayNumber like A-H-001 (department prefix · sub-department prefix · daily sequence), and moves through a fixed state machine documented in Ticket Lifecycle.
Quick Start
Three calls to verify your integration is wired correctly. Replace localhost:5050 with the production host once provisioned.
Authenticate
Exchange username + password for a bearer token. The token embeds the user id, role, and a 24-hour expiry.
Read public data
Pull the departments visible to kiosk clients — no authentication required, ideal for a smoke test.
Subscribe to events
Connect to the SignalR hub to receive every ticket state change in real time. No polling.
Step 1 — Log in
# Returns: { user: { id, username, name, role, ... }, token: "<base64>" } curl -X POST http://localhost:5050/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}'
Step 2 — Read kiosk departments
# Public — returns only departments where isActive AND showOnKiosk are true
curl http://localhost:5050/api/departments/kiosk
Step 3 — Subscribe to real-time updates
SignalR cannot be exercised with cURL — it requires the official @microsoft/signalr client. The full reference is at SignalR Hub; the snippet below is the minimum to verify connectivity.
// npm i @microsoft/signalr import * as signalR from "@microsoft/signalr"; const connection = new signalR.HubConnectionBuilder() .withUrl("http://localhost:5050/queueHub") .withAutomaticReconnect() .build(); connection.on("TicketCreated", (ticket) => { console.log("New ticket:", ticket.displayNumber); }); await connection.start(); await connection.invoke("JoinDisplay");
Authentication
QueueBridge uses a bearer-token scheme. The token is opaque to the client — treat it as a string. Tokens last 24 hours from the moment of issue.
Token format
The server issues a Base64-encoded string of the form userId:role:expiryTicks — partners do not need to decode it. Just store it from the login response and send it back on each request:
Authorization: Bearer eyJ1c2VySWQiOi...<token>
Roles and permissions
QueueBridge has two roles — admin and user — plus six granular permission codes. Admins implicitly hold every permission. The default permission set assigned to new user accounts is transfer.execute + department.lock.
| Permission code | What it allows |
|---|---|
| transfer.execute | Transfer tickets between sub-departments. |
| department.lock | Temporarily close / reopen a sub-department. |
| ticket.view_without_call | Inspect a ticket without changing its state. |
| ticket.edit_number | Manually edit a ticket's sequence number. |
| voice.generate | Trigger voice-announcement endpoints (TTS). |
| user.manage | Create, update, and delete users. |
The current build returns tokens on login and exposes GET /api/auth/verify for validation, but per-endpoint authorization filters are not applied at the controller layer. Partners are encouraged to attach the Authorization header on every authenticated request — future versions will enforce it without API changes.
Login — full reference
See Auth & Users below for the endpoint signature. Expired tokens return 401 Unauthorized with the body { "error": "انتهت صلاحية الجلسة" } (English: "Session expired").
Ticket Lifecycle
Every ticket lives in exactly one of seven states. The diagram below is the canonical state machine — every REST endpoint that mutates a ticket is labelled on the transition it triggers.
State reference
| Enum | Numeric | Meaning |
|---|---|---|
Waiting | 0 | Ticket issued; waiting to be called. |
Called | 1 | An employee has called the ticket; customer is on their way to the counter. |
InProgress | 2 | Service has started; the customer is at the counter. |
Completed | 3 | Service finished. isSuccess distinguishes successful vs. failed outcomes. |
Transferred | 4 | Ticket moved to a different sub-department; a new ticket may have been issued at the destination. |
Postponed | 5 | Customer asked to wait; ticket can be reactivated later. |
Cancelled | 6 | Ticket cancelled by an operator. |
The status field is serialized as its integer value by default (e.g. 0 for Waiting). The reporting endpoints additionally emit a parallel statusAr field with the Arabic display label.
Core Workflows
Four end-to-end scenarios that cover ~95% of real integrations. Each diagram shows the wire-level interaction between client, REST API, and SignalR. The cURL playbook below each diagram lets you reproduce the workflow from the command line.
Workflow 1 — Customer takes a ticket
Playbook
# 1. Issue a ticket curl -X POST http://localhost:5050/api/tickets \ -H "Content-Type: application/json" \ -d '{"departmentId":"dept-123","subDepartmentId":"subdept-456"}' # Response 200: # { "ticket": { "id":"...", "displayNumber":"A-H-007", "status":0, ... }, # "companyName":"...", "departmentName":"...", "roomNumber":"12", "waitingBefore":6 }
Workflow 2 — Employee calls the next ticket
Playbook
# 1. Fetch the next waiting ticket(s) curl "http://localhost:5050/api/tickets/waiting?subDeptId=subdept-456" # 2. Call a specific ticket curl -X PUT http://localhost:5050/api/tickets/ticket-789/call \ -H "Content-Type: application/json" \ -d '{"userId":"user-1","userName":"Ahmed"}' # 3. (Optional) Re-announce curl -X PUT http://localhost:5050/api/tickets/ticket-789/recall \ -H "Content-Type: application/json" \ -d '{"userId":"user-1","userName":"Ahmed"}' # 4. Mark service started when the customer arrives curl -X PUT http://localhost:5050/api/tickets/ticket-789/start \ -H "Content-Type: application/json" -d '{}'
Workflow 3 — Transfer to another sub-department
{newDepartmentId, newSubDepartmentId, note, keepSameNumber} API->>API: Append TicketHistory entry
set transferredFromSubDeptId API->>HUB: emit "TicketTransferred" API->>HUB: emit "QueueUpdated" (source) API->>HUB: emit "QueueUpdated" (target) API-->>P: 200 {updated ticket}
Playbook
curl -X PUT http://localhost:5050/api/tickets/ticket-789/transfer \ -H "Content-Type: application/json" \ -d '{ "newDepartmentId":"dept-999", "newSubDepartmentId":"subdept-pharmacy-1", "note":"Needs prescription pickup", "keepSameNumber": false }'
Workflow 4 — Complete and report
Playbook
# 1. Complete with success curl -X PUT http://localhost:5050/api/tickets/ticket-789/complete \ -H "Content-Type: application/json" \ -d '{"isSuccess":true,"note":"Prescribed antibiotics"}' # 2. Pull today's KPIs curl http://localhost:5050/api/reports/daily-summary # 3. Drill down by sub-department curl "http://localhost:5050/api/reports/subdepartments?fromDate=2026-05-19&toDate=2026-05-19"
Auth & Users
Login, token verification, and user management. 7 endpoints.
Exchange username + password for a bearer token + user profile.
| Field | Type | Required | Notes |
|---|---|---|---|
username | string | REQUIRED | Case-sensitive. Default seed: admin. |
password | string | REQUIRED | Plain text over the wire — deploy behind HTTPS in production. |
curl -X POST http://localhost:5050/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}'
200 OK
{
"user": {
"id": "a3f1...",
"username": "admin",
"name": "System Admin",
"role": "admin",
"assignedSubDepartmentIds": [],
"permissions": []
},
"token": "YTNmMTpsYWRtaW46NjM4NDg..."
}
401 Unauthorized — invalid credentials
{ "error": "اسم المستخدم أو كلمة المرور غير صحيحة" }
Validate a bearer token and return the associated user.
| Header | Required | Format |
|---|---|---|
Authorization | REQUIRED | Bearer <token> |
curl http://localhost:5050/api/auth/verify \
-H "Authorization: Bearer YTNmMTpsYWRtaW46..."
200 OK Returns { user: QueueUser }
401 Unauthorized Missing, malformed, or expired token. Expired returns body { "error": "انتهت صلاحية الجلسة" }.
List all users. The response enriches assignedSubDepartmentNames from sub-department records for convenience.
curl http://localhost:5050/api/users
200 OK — QueueUser[]. See Data Models.
Get a single user by id.
| Param | Type | Notes |
|---|---|---|
id | string (GUID) | User id. |
200 OK QueueUser
404 Not Found
Create a new user. Returns the created user with a generated id.
Send a QueueUser — see data model. Minimum fields: username, password, name, role.
{
"username": "reception_1",
"password": "secret",
"name": "Reception Desk 1",
"role": "user",
"roomNumber": "R-12",
"assignedSubDepartmentIds": ["subdept-456"],
"permissions": ["transfer.execute", "department.lock"],
"isActive": true
}
curl -X POST http://localhost:5050/api/users \ -H "Content-Type: application/json" \ -d '{"username":"u1","password":"p","name":"User One","role":"user"}'
201 Created Created user with Location header.
400 Bad Request Invalid input — e.g. employee limit reached per license.
409 Conflict Duplicate username.
Update an existing user. Empty password preserves the existing one.
200 OK Updated user. 404. 409 Conflict duplicate username.
Delete a user. The system refuses to delete the last remaining admin.
204 No Content
400 Bad Request — { "error": "لا يمكن حذف آخر مدير" } ("Cannot delete the last admin").
Tickets
The core resource. 19 endpoints covering issue, query, call, complete, transfer, postpone, reactivate, and reporting.
Issue a new ticket against a sub-department. Increments the daily sequence and emits TicketCreated.
| Field | Type | Required | Notes |
|---|---|---|---|
departmentId | string | REQUIRED | Parent department id. |
subDepartmentId | string | REQUIRED | Sub-department to issue against. Rejected with 423 if closed. |
curl -X POST http://localhost:5050/api/tickets \ -H "Content-Type: application/json" \ -d '{"departmentId":"dept-123","subDepartmentId":"subdept-456"}'
200 OK Print-ready envelope:
{
"ticket": {
"id": "7d18...",
"referenceNumber": "QT-20260519-00000007",
"displayNumber": "A-H-007",
"sequenceNumber": 7,
"status": 0,
"createdAt": "2026-05-19T10:30:00Z"
// ... full Ticket model
},
"companyName": "Medical Center",
"slogan": "Your health, our priority",
"logo": "data:image/png;base64,...",
"departmentName": "Cardiology",
"subDepartmentName": "Dr. Ali",
"roomNumber": "12",
"waitingBefore": 6
}
423 Locked Sub-department is temporarily closed:
{
"error": "هذا القسم مغلق مؤقتاً",
"isClosed": true,
"closedUntil": "2026-05-19T11:15:00Z",
"remainingMinutes": 12,
"reason": "Lunch break"
}
400 Bad Request Validation error — e.g. unknown sub-department.
List every ticket ever issued. For large deployments prefer the filtered endpoints below.
curl http://localhost:5050/api/tickets
200 OK Ticket[]
Fetch a single ticket including its full history audit trail.
200 OK Ticket · 404
All tickets in Waiting state, optionally filtered by sub-department.
| Param | Type | Required | Notes |
|---|---|---|---|
subDeptId | string | OPTIONAL | When provided, returns only tickets for this sub-department. |
curl "http://localhost:5050/api/tickets/waiting?subDeptId=subdept-456"Lightweight count for kiosks — how many people are ahead.
{ "count": 14 }Tickets currently in Postponed state.
subDeptId optional — filter by sub-department.
Multi-sub-department waiting list for portals that serve several desks.
| Param | Type | Notes |
|---|---|---|
ids | string | Comma-separated list of sub-department ids. ?ids=a,b,c |
Same as /waiting/multi but for postponed tickets.
All tickets for a sub-department, regardless of state.
Call a ticket. Builds an Arabic audio-announcement string server-side and broadcasts TicketCalled to all listeners (including displays for voice playback).
| Field | Type | Notes |
|---|---|---|
userId | string | Employee calling the ticket. Used as fallback for room number. |
userName | string | Display name — recorded in history. |
curl -X PUT http://localhost:5050/api/tickets/<id>/call \ -H "Content-Type: application/json" \ -d '{"userId":"user-1","userName":"Ahmed"}'
200 OK Updated Ticket with status=1 and calledAt set. 404 ticket not found.
SignalR event emitted: TicketCalled with payload { ticket, roomNumber, audioSpeech }.
Re-announce a previously called ticket without changing its status. Useful when the customer didn't appear.
Same as /call: { userId, userName }.
Mark service as actively in progress (the customer arrived at the counter).
Optional { "note": "..." } or empty body.
Emits TicketStarted with the updated ticket.
Close the ticket. Sets completedAt, isSuccess, and completionNote.
| Field | Type | Notes |
|---|---|---|
isSuccess | bool | Default true. Set false for unsuccessful outcomes (counted separately in reports). |
note | string? | Free-form completion note. |
Emits TicketCompleted then QueueUpdated for the sub-department.
Move a ticket to a different sub-department. Records the source in transferredFromSubDeptId / transferredFromDeptId.
| Field | Type | Notes |
|---|---|---|
newDepartmentId | string | Destination department. |
newSubDepartmentId | string | Destination sub-department. |
note | string? | Reason for transfer. |
keepSameNumber | bool | Default false. When true, the ticket retains its existing displayNumber at the destination. |
Emits TicketTransferred, plus two QueueUpdated events (source + destination).
Manually override a ticket's sequence number. Requires permission ticket.edit_number.
{
"newSequenceNumber": 42,
"userId": "user-1",
"userName": "Ahmed"
}Emits TicketNumberUpdated and QueueUpdated.
A specialized transfer: completes the current ticket and creates a new one in the destination sub-department, preserving continuity for the customer. Returns both tickets in the response.
{
"newDepartmentId": "dept-pharmacy",
"newSubDepartmentId": "subdept-pharmacy-1",
"note": "Pickup prescription",
"userId": "user-1",
"userName": "Ahmed"
}{
"completed": { /* old ticket, status=3 */ },
"created": { /* new ticket, status=0 */ }
}Emits TicketCompleted, QueueUpdated, TicketCreated, QueueUpdated in sequence.
Move a ticket to Postponed state. Customer may return later and be reactivated.
Optional { "note": "..." }.
Emits TicketPostponed and QueueUpdated.
Return a postponed ticket to the Waiting queue.
Emits QueueUpdated for the sub-department.
Quick daily snapshot of tickets — lighter than the full Reports endpoints.
date optional — ISO date string. Defaults to today.
Departments
Top-level service categories. 8 endpoints.
All departments, active and inactive.
curl http://localhost:5050/api/departments
200 OK Department[]
Get a single department.
200 OK · 404
Filtered list for kiosks — only departments where isActive=true AND showOnKiosk=true. Recommended for public-facing clients.
Filtered list for display screens — only departments where isActive=true AND showOnDisplay=true.
Create a department. Body is a Department object (see model).
curl -X POST http://localhost:5050/api/departments \ -H "Content-Type: application/json" \ -d '{ "name":"Cardiology","nameEn":"Cardiology","prefix":"A", "color":"#2563eb","icon":"❤️", "isActive":true,"showOnKiosk":true,"showOnDisplay":true }'
201 Created
Update an existing department.
Remove a department. Cascade behavior on sub-departments is defined by the service layer.
204 No Content · 404
Manually reset the daily sequence counters for all departments and sub-departments. Normally invoked automatically at dailyResetTime in settings.
{ "message": "Daily sequences reset" }Sub-Departments
The actual service desks. Includes the temporary closure facility — closed sub-departments reject new ticket issuance with HTTP 423. 9 endpoints.
List sub-departments. Optionally filtered to one parent department.
deptId optional — return only sub-departments under this department.
Get a single sub-department, including closure status.
Kiosk-filtered list under a parent department (isActive=true AND showOnKiosk=true).
Create a sub-department.
curl -X POST http://localhost:5050/api/subdepartments \ -H "Content-Type: application/json" \ -d '{ "departmentId":"dept-123", "name":"Dr. Ali", "prefix":"H", "roomNumber":"12", "color":"#22c55e","icon":"👨⚕️", "isActive":true,"showOnKiosk":true,"showOnDisplay":true }'
Update a sub-department.
Delete a sub-department.
204 No Content · 404
Temporarily close a sub-department for N minutes. Requires permission department.lock. Emits SubDeptClosureChanged.
| Field | Type | Notes |
|---|---|---|
minutes | int | Default 15. Sets closedUntil = now + minutes. |
reason | string? | Shown on kiosks and displays. |
userId / userName | string? | Who closed it. |
curl -X PUT http://localhost:5050/api/subdepartments/<id>/close \ -H "Content-Type: application/json" \ -d '{"minutes":30,"reason":"Lunch","userId":"user-1","userName":"Ahmed"}'
Close several sub-departments at once. Useful for end-of-day or break-room patterns.
{
"ids": ["sub-1", "sub-2", "sub-3"],
"minutes": 60,
"reason": "Maintenance",
"userId": "admin",
"userName": "Admin"
}200 OK List of updated sub-departments. 400 empty ids array.
SignalR: emits a single SubDeptClosureBulkChanged with the list.
Reopen a closed sub-department immediately, ignoring closedUntil.
Emits SubDeptClosureChanged.
Settings
Global queue configuration. 3 endpoints.
Read the active QueueSettings document (single global record). See the data model for the full field list.
Replace the global settings. Send the full QueueSettings object — fields omitted will revert to defaults on disk.
Convenience endpoint — upload a logo image as Base64. Equivalent to PUTting settings with only logoBase64 changed.
{ "logoBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." }Display Settings
Configure which departments and sub-departments are shown on each physical display screen. Lets you run multiple displays with different filtering. 6 endpoints.
All display configurations.
A single display configuration.
Returns the resolved { departments, subDepartments } that should appear on this display, taking showAllDepartments and the filter arrays into account.
{
"departments": [ Department, ... ],
"subDepartments": [ SubDepartment, ... ]
}Create a display configuration.
{
"name": "Lobby East TV",
"departmentIds": ["dept-1", "dept-2"],
"subDepartmentIds": [],
"showAllDepartments": false
}Update a display configuration.
Delete a display configuration.
Reports
Aggregated analytics. All date parameters accept ISO 8601 strings (e.g. 2026-05-19). 4 endpoints.
Detailed ticket report with computed waiting + service times. Useful for exporting to BI tools.
| Param | Type | Notes |
|---|---|---|
fromDate | date | Inclusive. |
toDate | date | Inclusive. |
departmentId | string | |
subDepartmentId | string | |
status | string | Enum name — e.g. Completed. |
userId | string | Employee that handled the ticket. |
curl "http://localhost:5050/api/reports/tickets?fromDate=2026-05-01&toDate=2026-05-19&status=Completed"{
"id": "...",
"referenceNumber": "QT-20260519-00000007",
"displayNumber": "A-H-007",
"departmentName": "Cardiology",
"subDepartmentName": "Dr. Ali",
"status": "Completed",
"statusAr": "مكتمل",
"createdAt": "2026-05-19T10:30:00Z",
"calledAt": "2026-05-19T10:42:00Z",
"startedAt": "2026-05-19T10:43:00Z",
"completedAt": "2026-05-19T10:51:00Z",
"waitingTime": "12د 0ث", // formatted
"serviceTime": "9د 0ث",
"assignedUserId": "user-1",
"assignedUserName": "Ahmed",
"isSuccess": true,
"historyCount": 4
}The waitingTime and serviceTime strings are formatted with Arabic time units (س=hours, د=minutes, ث=seconds). Use the raw createdAt/calledAt/completedAt timestamps when computing in other locales.
Aggregate KPIs grouped by sub-department: counts, success rate, average waiting + service times.
fromDate, toDate (both optional).
{
"subDepartmentId": "...",
"subDepartmentName": "Dr. Ali",
"departmentId": "...",
"roomNumber": "12",
"totalTickets": 120,
"waitingTickets": 3,
"calledTickets": 1,
"inProgressTickets": 1,
"completedTickets": 110,
"transferredTickets": 3,
"cancelledTickets": 2,
"successRate": 97.3,
"avgWaitingTime": 8.4, // minutes
"avgServiceTime": 6.7 // minutes
}KPIs grouped by employee: handled count, success rate, average service time.
fromDate, toDate.
{
"userId": "user-1",
"userName": "Ahmed",
"totalHandled": 48,
"completedCount": 45,
"inProgressCount": 1,
"transferredCount": 2,
"successRate": 95.6,
"avgServiceTime": 5.2
}One-shot dashboard summary for a given day.
date optional — defaults to today.
{
"date": "2026-05-19T00:00:00",
"totalTickets": 340,
"waitingTickets": 14,
"calledTickets": 3,
"inProgressTickets": 2,
"completedTickets": 310,
"transferredTickets": 8,
"cancelledTickets": 3,
"successRate": 96.5,
"avgWaitingTime": 9.2,
"avgServiceTime": 5.8
}License
Hardware-bound activation and employee-seat enforcement. 5 endpoints.
Active license details: plan, expiry, max employees.
The server's hardware fingerprint — needed by Iraq Soft to issue an activation key.
{ "hardwareId": "7F-4C-..." }Quick boolean validity check, plus a human-readable message.
{ "isValid": true, "message": "License active" }Apply an activation key.
{ "licenseKey": "XXXX-XXXX-XXXX-XXXX" }200 OK { success:true, message, license }
400 Bad Request { success:false, message } — e.g. wrong hardware id, expired, or empty key.
How many employees are currently provisioned and how many more the license allows.
{
"currentCount": 12,
"canAddMore": true,
"remainingSlots": 8,
"maxEmployees": 20
}Text-to-Speech
Server-side proxy to Google Translate's TTS. Returns an MP3 stream. 1 endpoint.
Stream a generated MP3 for the given text.
| Param | Type | Required | Notes |
|---|---|---|---|
text | string | REQUIRED | URL-encoded source text. |
lang | string | OPTIONAL | Default ar. Accepts any code Google Translate supports. |
curl "http://localhost:5050/api/tts/speak?text=Welcome&lang=en" --output welcome.mp3200 OK — Content-Type: audio/mpeg with binary MP3 body. 500 if Google upstream is unreachable.
SignalR Hub
Real-time updates are delivered over a single SignalR hub. Every state-changing REST call emits one or more events on this hub so subscribed clients update without polling.
Connection
| Property | Value |
|---|---|
| Hub URL | http://<host>:5050/queueHub (also reachable over WebSocket as ws://<host>:5050/queueHub) |
| Client library | @microsoft/signalr (Node/JS), Microsoft.AspNetCore.SignalR.Client (.NET), official clients available for Java, Python, C++. |
| Transport | WebSockets preferred; server falls back to Server-Sent Events / Long Polling automatically. |
| Auth | Connection-level auth is not currently enforced. Future versions may require the bearer token via the accessTokenFactory option. |
// Reference connection setup with reconnect, error handling, and join import * as signalR from "@microsoft/signalr"; const conn = new signalR.HubConnectionBuilder() .withUrl("http://localhost:5050/queueHub") .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) .configureLogging(signalR.LogLevel.Information) .build(); conn.onreconnecting(err => console.warn("reconnecting", err)); conn.onreconnected(id => console.log("reconnected", id)); conn.onclose(err => console.error("closed", err)); // Subscribe to events BEFORE start() conn.on("TicketCreated", ticket => {/* ... */}); conn.on("TicketCalled", ({ ticket, roomNumber, audioSpeech }) => {/* ... */}); await conn.start(); await conn.invoke("JoinDisplay"); // or: await conn.invoke("JoinSubDepartment", "subdept-456");
Client → Server methods
These are invoked from the client via connection.invoke(name, args). They control which broadcast groups the connection belongs to.
| Method | Args | Effect |
|---|---|---|
JoinSubDepartment |
subDeptId: string |
Adds the connection to group subdept-{subDeptId}. (Currently informational — broadcasts are sent to Clients.All; groups exist for forward-compatible scoped delivery.) |
LeaveSubDepartment |
subDeptId: string |
Removes the connection from the sub-department group. |
JoinDisplay |
— | Adds the connection to the display group. |
Server → Client events
Subscribe with connection.on(name, handler). Every event below is currently broadcast to all connected clients.
| Event name | Fires when | Payload shape |
|---|---|---|
TicketCreated | POST /api/tickets succeeds, or the destination side of /request-service. | Ticket |
TicketCalled | PUT /{id}/call or PUT /{id}/recall. | { ticket: Ticket, roomNumber: string, audioSpeech: string } — audioSpeech is the pre-built Arabic announcement. |
TicketStarted | PUT /{id}/start. | Ticket |
TicketCompleted | PUT /{id}/complete, or the source side of /request-service. | Ticket |
TicketTransferred | PUT /{id}/transfer. | Ticket |
TicketNumberUpdated | PUT /{id}/update-number. | Ticket |
TicketPostponed | PUT /{id}/postpone. | Ticket |
SubDeptClosureChanged | PUT /{id}/close or /{id}/reopen. | SubDepartment (the updated one). |
SubDeptClosureBulkChanged | POST /subdepartments/close-multi. | SubDepartment[] |
QueueUpdated | After complete, transfer, request-service, update-number, postpone, reactivate. | string — the affected subDepartmentId. Listeners should re-query waiting lists for that sub-department. |
Several REST calls emit multiple SignalR events in sequence (e.g. /request-service emits four). Treat event handlers as idempotent re-renderers — they should derive UI state from the current ticket payload, not from event ordering.
Data Models
Schemas for every DTO returned by the API. All fields use camelCase. Nullable fields are marked ?.
Department
| Field | Type | Default | Notes |
|---|---|---|---|
id | string | auto GUID | Primary key. |
name | string | "" | Arabic (or primary) display name. |
nameEn | string | "" | English display name. |
prefix | string | "A" | Letter used in displayNumber. |
color | string | "#3b82f6" | Hex color for UI accents. |
icon | string | "🏥" | Emoji icon. Used when useImage=false. |
imageBase64 | string? | null | Optional PNG/JPG as data URL. |
useImage | bool | false | Prefer imageBase64 over icon. |
displayOrder | int | 0 | Sort key for UI. |
isActive | bool | true | Soft on/off switch. |
showOnKiosk | bool | true | Visibility on kiosks. |
showOnDisplay | bool | true | Visibility on display screens. |
currentSequence | int | 0 | Daily ticket counter. Resets at dailyResetTime. |
lastResetDate | datetime | today | Timestamp of the last reset. |
createdAt | datetime | UTC now | Audit. |
SubDepartment
| Field | Type | Default | Notes |
|---|---|---|---|
id | string | auto GUID | Primary key. |
departmentId | string | "" | Foreign key to Department. |
name / nameEn | string | "" | Display names. |
prefix | string | "1" | Sub-department letter/digit in displayNumber. |
color / icon / imageBase64 / useImage | — | — | Same semantics as Department. |
roomNumber | string | "" | Physical room / counter label. |
displayOrder | int | 0 | Sort key. |
isActive | bool | true | |
showOnKiosk / showOnDisplay | bool | true | |
currentSequence / lastResetDate | int / datetime | — | Per-sub-department daily counter. |
assignedUserIds | string[] | [] | Employees responsible for this sub-department. |
createdAt | datetime | UTC now | |
| Closure state | |||
isClosed | bool | false | true means new tickets are rejected with 423. |
closedUntil | datetime? | null | UTC time of automatic reopen. |
closureReason | string? | null | Free-form reason. |
closedByUserId / closedByUserName | string? | null | Audit. |
closedAt | datetime? | null | UTC time the closure was initiated. |
Ticket
| Field | Type | Default | Notes |
|---|---|---|---|
id | string | auto GUID | Primary key. |
referenceNumber | string | "" | Format QT-YYYYMMDD-NNNNNNNN. Globally unique across history. |
departmentId / subDepartmentId | string | — | Foreign keys. |
departmentName / subDepartmentName | string | — | Denormalized for immutable audit. |
departmentPrefix / subDepartmentPrefix | string | — | Denormalized prefixes used to build displayNumber. |
sequenceNumber | int | 0 | Daily sequence inside the sub-department. |
displayNumber | string | "" | Human label, format {deptPrefix}-{subPrefix}-{NNN}, e.g. A-H-001. |
status | int (enum) | 0 | See Ticket Lifecycle. |
createdAt | datetime | UTC now | Time issued. |
calledAt | datetime? | null | Set on /call. |
startedAt | datetime? | null | Set on /start. |
completedAt | datetime? | null | Set on /complete. |
assignedUserId / assignedUserName | string? | null | Last employee who acted. |
note | string? | null | Working note set on /start / /postpone. |
completionNote | string? | null | Note recorded on /complete. |
isSuccess | bool | true | Outcome flag. |
transferredFromDeptId / transferredFromSubDeptId | string? | null | Set on transfer. |
transferNote | string? | null | Reason for transfer. |
history | TicketHistory[] | [] | Embedded changelog. |
TicketHistory
| Field | Type | Notes |
|---|---|---|
timestamp | datetime | UTC. |
action | string | One of Created, Called, Started, Completed, Transferred, Postponed, Reactivated, NumberUpdated. |
userId / userName | string? | Actor. |
note | string? | Optional context. |
fromDeptId / toDeptId | string? | Populated on transfer. |
QueueUser
| Field | Type | Default | Notes |
|---|---|---|---|
id | string | auto GUID | Primary key. |
username | string | "" | Unique login. |
password | string | "" | Hashed on the server. Pass empty on update to keep existing. |
name | string | "" | Display name. |
role | string | "user" | One of admin, user. |
roomNumber | string | "" | Fallback room used by /call if the sub-department has none. |
assignedSubDepartmentIds | string[] | [] | Sub-departments this user can serve. |
assignedSubDepartmentNames | string[] | [] | Enriched by GET /api/users for display purposes. |
permissions | string[] | [] | Granular permission codes (see Authentication). |
isActive | bool | true | |
createdAt | datetime | UTC now |
QueueSettings
Single global record. All fields are present on every response.
| Field | Type | Default | Purpose |
|---|---|---|---|
| Branding | |||
companyName | string | "مركز طبي" | Shown on tickets and displays. |
companyNameEn | string | "Medical Center" | |
slogan | string | — | Subtitle line. |
logoBase64 | string | "" | Data URL for the logo. |
| Kiosk UI | |||
kioskSectionTitle | string | — | Top-level kiosk heading. |
kioskSectionSubtitle | string | — | Sub-title under it. |
kioskSubdeptTitle / kioskSubdeptSubtitle | string | — | Sub-department screen labels. |
kioskAutoResetSeconds | int | 10 | Auto-return to home after ticket print. 0 = instant. |
ticketLabel | string | "وصل" | Word used for "ticket" in announcements. |
| Printing | |||
receiptWidth | int | 80 | Receipt width in millimeters. |
receiptFontSize | string | "medium" | small / medium / large. |
| Display screen | |||
displayShowCompanyName | bool | true | |
displayShowLogo | bool | true | |
displayShowWaitingCount | bool | true | Show count of waiting tickets per sub-department. |
displayShowWaitingNames | bool | false | Show ticket numbers of those still waiting. |
displayScrollSpeed | int | 3 | 1 (slow) — 5 (fast). |
displayCallDuration | int | 5 | Seconds a called ticket stays highlighted. |
displayEnableSound / displayEnableVoiceAnnouncement | bool | true | Toggle audio cues. |
displayShowPostponedTickets | bool | true | Render postponed list. |
roomLabel | string | "الغرفة" | Word used for "room" in announcements (override to "Window", "Counter", etc.). |
| Voice engine | |||
displayVoiceEngine | string | "browser" | browser / files / google. |
displayVoiceLocale | string | "ar-SA" | BCP-47 tag. |
displayVoiceName | string? | null | Optional specific voice name in the browser. |
displayVoiceRate | double | 0.9 | 0.5 – 2.0. |
displayVoicePitch | double | 1.0 | 0.5 – 2.0. |
dailyResetTime | timespan | 00:00:00 | Wall-clock time at which daily sequences reset. |
DisplaySettings
| Field | Type | Default | Notes |
|---|---|---|---|
id | string | auto GUID | Primary key. |
name | string | "شاشة الاستقبال" | Friendly name. |
departmentIds | string[] | [] | Departments shown when showAllDepartments=false. |
subDepartmentIds | string[] | [] | Additional sub-departments shown. |
showAllDepartments | bool | true | Bypass filters and show everything. |
createdAt | datetime | UTC now |
Error Reference
QueueBridge uses standard HTTP status codes. Error bodies follow one of two shapes, depending on the controller:
// Shape 1 — single error message { "error": "<message, often in Arabic>" } // Shape 2 — used by License and a few others { "success": false, "message": "<message>" }
| Code | Meaning | When |
|---|---|---|
| 200 | OK | Successful GET / PUT / POST that returns content. |
| 201 | Created | Successful POST that creates a resource. Includes Location header. |
| 204 | No Content | Successful DELETE. |
| 400 | Bad Request | Validation failed, malformed JSON, or business-rule rejection (e.g. license limit reached, last admin deletion). |
| 401 | Unauthorized | Missing/invalid/expired token. Returned by /auth/login on bad credentials and /auth/verify on bad tokens. |
| 403 | Forbidden | Reserved — future use for permission enforcement. |
| 404 | Not Found | Resource (ticket, user, department, etc.) does not exist. |
| 409 | Conflict | Duplicate username on user create/update. |
| 423 | Locked | Sub-department temporarily closed. Returned by POST /api/tickets. Special payload — see below. |
| 500 | Server Error | Unhandled exception. Upstream failure for /api/tts/speak. |
The 423 closed-department payload
This response is unique and worth handling specially — it lets the client display a friendly "back in N minutes" message instead of a generic error.
{
"error": "هذا القسم مغلق مؤقتاً", // "This section is temporarily closed"
"isClosed": true,
"closedUntil": "2026-05-19T11:15:00Z", // UTC ISO 8601
"remainingMinutes": 12,
"reason": "Lunch break" // nullable
}
Many error messages are returned in Arabic because they're designed to be shown directly on kiosks. They're stable strings — partners may map them client-side. Examples: "اسم المستخدم أو كلمة المرور غير صحيحة" (invalid credentials), "انتهت صلاحية الجلسة" (session expired), "لا يمكن حذف آخر مدير" (cannot delete last admin), "هذا القسم مغلق مؤقتاً" (sub-department closed), "مفتاح الترخيص مطلوب" (license key required).
CORS, Rate Limits, Versioning
CORS
The server enables a permissive CORS policy that accepts any origin, method, and header, and allows credentials — this is required for SignalR over WebSockets to function across origins. Production deployments behind a reverse proxy should narrow the allowed origin list at the proxy layer.
Rate limits
No rate limiting is currently applied at the API layer. The intended deployment topology — a private LAN with a small number of trusted kiosks, portals, and displays — does not benefit from it. Partners exposing the API over public networks should add a rate-limit layer at their gateway.
Versioning
The current API uses unversioned paths (/api/...). Future breaking changes will be introduced under a versioned prefix (/api/v2/...). The v1 surface documented here will remain available for the duration of any partner agreement.
Pagination
List endpoints currently return full collections. For deployments that grow beyond ~10,000 tickets/day, contact us before integrating with bulk endpoints — pagination is on the roadmap.
Appendix
Glossary
| Term | Meaning |
|---|---|
| Department | Top-level service category. Holds the first letter of displayNumber. |
| Sub-Department | A specific service desk — usually mapped to a physical room or counter. Holds the second segment of displayNumber. |
| Display | A large-screen monitor in the waiting area showing called and waiting tickets. Configured via DisplaySettings. |
| Kiosk | A customer-facing touchscreen used to issue tickets. |
| Portal | The employee-facing UI used to call and complete tickets. |
displayNumber | The human-friendly ticket label, e.g. A-H-001 = department A, sub-department H, sequence 001. |
referenceNumber | The globally unique identifier across all history: QT-YYYYMMDD-NNNNNNNN. |
audioSpeech | Pre-built Arabic announcement string, e.g. "الوصل رقم سبعة يتفضل إلى الغرفة اثنا عشر". Generated server-side; consumed by displays. |
Default seed credentials
A fresh installation seeds two users. Rotate both passwords on first deploy.
admin / admin123 — role: admin
user1 / user123 — role: user
Storage notes
QueueBridge persists state in JSON files (tickets.json, users.json, departments.json, subdepartments.json, queue_settings.json, display_settings.json) next to the server binary. Backups are as simple as copying that folder. There is no external database to provision.
Contact & support
| Channel | Detail |
|---|---|
| WhatsApp (preferred for integration questions) | +964 772 228 4111 |
| Hotline | 6554 |
| Office | Iraq Soft — Baghdad, Electronic University |
| Web | QueueBridge product site |