Transports
WebSocket
One socket that carries any call by name, several at once, with the same JSON as REST.
Shape
wss://api.inorbit.hr/v1/ws is one connection for every route in the reference,
addressed by the RPC's full name instead of a path. Frames are JSON text, one object
each. The token goes on the upgrade request as Authorization: Bearer.
From the client:
| Frame | Meaning |
|---|---|
{"type":"call","id":"1","method":"iohr.<service>.v1.<Service>/<Method>","body":{…}} | start a call; body may be omitted for an empty request |
{"type":"cancel","id":"1"} | stop a call in flight |
To the client:
| Frame | Meaning |
|---|---|
{"type":"data","id":"1","body":{…}} | one answer; a unary call sends exactly one, a stream sends many |
{"type":"end","id":"1"} | the call is over and the id is free again |
{"type":"error","id":"1","code":…,"error":…,"details":[…]} | the call failed; the error envelope with the id it belongs to |
{"type":"error","code":…,"error":…,"details":[…]} | the frame itself was refused; no call is named and the socket stays open |
{"type":"call","id":"1","method":"iohr.accounts.v1.AccountsService/GetMe"}
{"type":"data","id":"1","body":{"…":"…"}}
{"type":"end","id":"1"}The method name is the operation's x-iohr-rpc in the
OpenAPI document: EventsService.StreamEvents in
the reference is iohr.events.v1.EventsService/StreamEvents on the socket. The whole
request message is the frame's body; path and query rules belong to REST and do not
apply here.
The frames are published as a JSON Schema at
https://api.inorbit.hr/frames.json, no token
needed, with the limits below in x-iohr-limits. The SDKs are built against it.
Scopes
A token or key limited to scopes opens the socket like a full
one, and each call is checked against its scopes the way REST checks each request: a
method is yours when its operation in the reference names one of your scopes. With
events:read you may call EventsService/ListEventTypes and EventsService/StreamEvents
and nothing else. Any other call ends with {"type":"error","id":…,"code":"forbidden",…}
and the socket stays open. A full API token may call every method.
Rules
idis yours, unique among the calls you have in flight, free again afterendorerror. Every call ends with exactly one of the two, cancellation included.- Calls are independent: a unary call answers while streams stay open. A refusal never closes the socket.
- At most 64 calls in flight per connection; the 65th is
rate_limiteduntil one ends. - A frame from the client is at most 256 KiB.
- A client that reads slowly stalls only its own calls; frames wait in one bounded queue per connection.
- The server sends a WebSocket ping every 15 seconds; your WebSocket library answers it.
A socket with no call in flight and no frame from you for 5 minutes is closed with
code 1000, reason
idle. - The socket counts as one of your open streams: a key, API token or person holds at
most 32 (event streams, sockets and MQTT connections together), an account 128. Past
that the upgrade is refused with
429 rate_limited.
How a socket ends
The token is checked once, at the upgrade, and the socket is not cut when that token
expires. The server ends it in two cases, each with one error frame that names no
call, then the close:
| Frame code | Why | What to do |
|---|---|---|
unauthenticated | the key or API token behind the socket was revoked | stop; a new key is a new decision |
unavailable | the socket reached its longest life, 24 hours | reconnect and send again the calls that had not ended |
After any close, a client reconnects with a fresh token and issues again only the calls that had not ended and are safe to repeat (reads and streams).