Architecture
The Listo Public API has two integration surfaces — a push surface (Service Bus) and a pull surface (REST) — backed by the same canonical event store.
Walkthrough
Each labelled arrow above corresponds to a step in the lifecycle of one screening event.
1. Submit screening
The Listo iPad app submits a completed hearing screening to the Listo Backend API over HTTPS.
2. Persist
The backend writes the session to the Listo
database. The session is assigned a numeric id at this point. This is the
canonical store for the Sessions API.
3. Publish CloudEvent
In the same request that persists the session, the backend publishes a
CloudEvent of type com.auditdata.listo.screening.completed.v1 to the
Service Bus queue provisioned for the session's tenant — by default the
tenant's dedicated queue, or the shared queue carrying that tenant if
the integrating system opted into shared-queue mode for it at
onboarding. Persistence and publication happen together — every session
that exists in the database has an event on its queue.
The Service Bus message body is the full CloudEvent JSON in structured
binding mode. The CloudEvent's data field contains the full screening
payload.
4. Push to consumer
Service Bus delivers the message to your consumer using the listen-only SAS
connection string we issued at onboarding. Delivery is at-least-once; you
acknowledge with CompleteMessage once you've durably handled the event.
5. Sessions API
GET /v1/tenants/{tenantId}/screenings?from=…&to=… reads from the same
database that holds the canonical sessions, scoped to the single tenant
in the path. Each item is a screening object — the same fields the
Service Bus delivery carries inside its CloudEvent data payload,
returned here directly without an envelope wrapper.
Use the Sessions API when:
- Your consumer was offline during a delivery window.
- You need to reprocess a range after a downstream bug.
Why two transports
Service Bus is the primary path: it's lower latency, it scales to many consumers without us needing to add capacity, and it's the natural fit for event-driven processing on the integrating system side. We push there first.
The REST API exists because a queue is not a system of record. Once a message is acknowledged or its retention window passes, it's gone. The Sessions API gives you a deterministic way to recover, without depending on Service Bus state we don't control on your end.
Tenancy
By default each tenant has its own queue. If your integrating system is linked to N tenants under this default, you receive N queues and N listen-only SAS connection strings — one per queue, each carrying events for exactly one tenant.
At onboarding you can request that two or more of your linked tenants
route to a single shared queue instead — useful if you'd rather
process a subset of your tenants in one consumer rather than fan out.
A shared queue still receives only your tenants' events; consumers
identify the originating tenant per event by reading data.tenantId.
The OAuth/REST side is unchanged: tokens remain single-tenant
regardless of queue topology.
Your OAuth credentials are scoped the same way regardless of queue
topology: each tenant gets its own (client_id, client_secret) pair,
and the bearer token minted from that pair carries a single-UUID
tenant_id claim. The {tenantId} in
GET /v1/tenants/{tenantId}/screenings must equal that claim — a
different value returns 403 forbidden. To query a different tenant,
mint a token using that tenant's credentials.
You will never receive another integrating system's data, and a queue will never mix events from outside the specific tenant or tenants it was provisioned for.