Skip to content

Commit 3a3bd39

Browse files
committed
Update documentation for REST over Websocket
1 parent 7da5fd5 commit 3a3bd39

File tree

1 file changed

+92
-134
lines changed

1 file changed

+92
-134
lines changed

docs/Configuration/Interfaces/Asterisk-REST-Interface-ARI/ARI-REST-over-WebSocket.md

+92-134
Original file line numberDiff line numberDiff line change
@@ -2,90 +2,51 @@
22

33
Historically, using ARI required two communications channels... HTTP for making REST requests and getting their reponses, and Websocket for receiving events. Upcoming releases of Asterisk however, will allow you to make REST requests and receive their responses over the same Websocket you use to receive events.
44

5-
## The ASTSwaggerSocket Protocol
5+
## The Protocol
66

7-
There are several published protocols for request/response type communication over Websockets including [WAMP](https://wamp-proto.org), [JSON-RPC](https://www.jsonrpc.org), [XMPP](https://xmpp.org), [Socket.IO](https://socket.io), etc. but these are all fairly heavyweight and would require significant effort to implement. Instead we went with [SwaggerSocket](https://github.com/swagger-api/swagger-socket) which is a very lightweight JSON encapsulation of HTTP requests and responses and although that project itself is archived, the protocol itself is perfect for our use. We did have to modify it slightly to fit the existing Asterisk ARI implementation (hence "ASTSwaggerSocket") but the changes are trivial. See [Differences between SwaggerSocket and ASTSwaggerSocket](#differences-between-swaggersocket-and-astswaggersocket) below.
8-
9-
### Example Handshake
10-
11-
Assuming you've already established a Websocket connection to Asterisk, you'll need to start with a handshake before you can send requests.
12-
13-
Client sends:
14-
15-
```json
16-
{
17-
"type": "RESTHandshakeRequest",
18-
"protocol_version": "1.0",
19-
"protocol_name": "ASTSwaggerSocket"
20-
}
21-
```
22-
23-
Server responds with a standard ARI Event:
24-
25-
```json
26-
{
27-
"type": "RESTStatusResponse",
28-
"identity": "d1eb679c-166f-4278-8b28-0340088a3410",
29-
"status": {
30-
"status_code": 200,
31-
"reason_phrase": "OK"
32-
},
33-
"timestamp": "2025-03-14T15:37:21.208-0600",
34-
"asterisk_id": "e4:1d:2d:b8:3b:a0",
35-
"application": "mystasisapp"
36-
}
37-
```
38-
39-
The handshake only needs to be done once after you've established the websocket. You'll need to use the `identity` when making future requests. Speaking of which, you can now make as many requests as you wish.
7+
There are several published protocols for request/response type communication over Websockets including [WAMP](https://wamp-proto.org), [JSON-RPC](https://www.jsonrpc.org), [XMPP](https://xmpp.org), [Socket.IO](https://socket.io), etc. but these are all fairly heavyweight and would require significant effort to implement. Instead we went with a simple JSON wrapper loosely based on [SwaggerSocket](https://github.com/swagger-api/swagger-socket).
408

419
### Request/Response
4210

11+
Assuming you've already established a websocket to Asterisk, you can start sending requests without any further setup.
12+
4313
Let's say you received a StasisStart event for channel `ast-1741988825.0`. You can now issue a POST request to answer it...
4414

4515
Client sends:
4616

4717
```json
4818
{
4919
"type": "RESTRequest",
50-
"identity": "d1eb679c-166f-4278-8b28-0340088a3410",
51-
"requests": [
52-
{
53-
"uuid": "1e409d15-a35e-4946-b270-c460cc45b2c4",
54-
"method": "POST",
55-
"path": "channels/ast-1741988825.0/answer"
56-
}
57-
]
20+
"transaction_id": "d1eb679c-166f-4278-8b28-0340088a3410",
21+
"request_id": "1e409d15-a35e-4946-b270-c460cc45b2c4",
22+
"method": "POST",
23+
"uri": "channels/ast-1741988825.0/answer"
5824
}
5925
```
6026

6127
* `type` must be `RESTRequest`.
62-
* The `identity` property needs to be the same value that was returned in the `RESTStatusResponse` for the handshake.
63-
* Although the SwaggerSocket protocol supports sending multiple requests in a single message, our version only looks at the first request in the `requests` array.
64-
* `uuid` can actually be any string you like. It's returned in the response so you can correlate it to the request that generated it.
28+
* The `transaction_id` is optional and can be any valid string. It will be returned in the response. You could use this to correlate multiple requests together like creating a channel and adding it to a bridge.
29+
* `request_id` is also optional and can be any valid string. It will also be returned in the response. You should use this to tie a single request and response together.
6530
* `method` can be any HTTP method that's valid for the resource you're referencing.
66-
* `path` is the path to the resource.
31+
* `uri` is the URI for the resource with optional query parameters but without the leading `/`.
6732

6833
Server responds with a standard ARI Event:
6934

7035
```json
7136
{
72-
"type": "RESTResponseMsg",
73-
"identity": "d1eb679c-166f-4278-8b28-0340088a3410",
74-
"responses": [
75-
{
76-
"uuid": "1e409d15-a35e-4946-b270-c460cc45b2c4",
77-
"status_code": 204,
78-
"reason_phrase": "No Content",
79-
"path": "channels/ast-1741988825.0/answer"
80-
}
81-
],
37+
"type": "RESTResponse",
38+
"transaction_id": "d1eb679c-166f-4278-8b28-0340088a3410",
39+
"request_id": "1e409d15-a35e-4946-b270-c460cc45b2c4",
40+
"status_code": 204,
41+
"reason_phrase": "No Content",
42+
"uri": "channels/ast-1741988825.0/answer"
8243
"timestamp": "2025-03-17T08:32:35.709-0600",
8344
"asterisk_id": "e4:1d:2d:b8:3b:a0",
8445
"application": "mystasisapp"
8546
}
8647
```
8748

88-
The response is straightforward. The `uuid` will be whatever was specified in the request.
49+
The response is straightforward. The `transaction_id` and `request_id` will be whatever was specified in the request. Those properties will be present but empty strings if not specified in the request.
8950

9051
Let's do a GET on the channel now...
9152

@@ -94,112 +55,119 @@ Client sends:
9455
```json
9556
{
9657
"type": "RESTRequest",
97-
"identity": "d1eb679c-166f-4278-8b28-0340088a3410",
98-
"requests": [
99-
{
100-
"uuid": "1e409d15-a35e-4946-b270-c460cc45b2c4",
101-
"method": "GET",
102-
"path": "channels/ast-1741988825.0"
103-
}
104-
]
58+
"transaction_id": "d1eb679c-166f-4278-8b28-0340088a3410",
59+
"request_id": "469ee918-d315-435d-b40f-c8e84007d4d3",
60+
"method": "GET",
61+
"uri": "channels/ast-1741988825.0"
10562
}
10663
```
10764

10865
Server responds with:
10966

11067
```json
11168
{
112-
"type": "RESTResponseMsg",
113-
"identity": "d1eb679c-166f-4278-8b28-0340088a3410",
114-
"responses": [
69+
"type": "RESTResponse",
70+
"transaction_id": "d1eb679c-166f-4278-8b28-0340088a3410",
71+
"request_id": "469ee918-d315-435d-b40f-c8e84007d4d3",
72+
"status_code": 200,
73+
"reason_phrase": "OK",
74+
"uri": "channels/ast-1741988825.0",
75+
"headers": [
11576
{
116-
"uuid": "1e409d15-a35e-4946-b270-c460cc45b2c4",
117-
"status_code": 200,
118-
"reason_phrase": "OK",
119-
"path": "channels/ast-1741988825.0",
120-
"headers": [
121-
{
122-
"name": "Content-type",
123-
"value": "application/json"
124-
}
125-
],
126-
"message_body": "{\"id\":\"ast-1741990187.0\",\"name\":\"PJSIP/1171-00000000\",
127-
\"state\":\"Up\",\"protocol_id\":\"tqOjze4LWAiFZVNlsj4FJpE7H0VX1Yhm\",\"caller\":
128-
{\"name\": \"Alice Cooper\",\"number\":\"1171\"},\"connected\":{\"name\":\"\",
129-
\"number\":\"\"}, \"accountcode\":\"\",\"dialplan\":{\"context\":\"default\",\"exten\":
130-
\"1118\",\"priority\": 2,\"app_name\":\"Stasis\",\"app_data\":\"voicebot\"},
131-
\"creationtime\": \"2025-03-17T08:32:34.709-0600\",\"language\":\"en_US\"}"
77+
"name": "Content-type",
78+
"value": "application/json"
13279
}
13380
],
81+
"message_body": "{\"id\":\"ast-1741990187.0\",\"name\":\"PJSIP/1171-00000000\",
82+
\"state\":\"Up\",\"protocol_id\":\"tqOjze4LWAiFZVNlsj4FJpE7H0VX1Yhm\",\"caller\":
83+
{\"name\": \"Alice Cooper\",\"number\":\"1171\"},\"connected\":{\"name\":\"\",
84+
\"number\":\"\"}, \"accountcode\":\"\",\"dialplan\":{\"context\":\"default\",\"exten\":
85+
\"1118\",\"priority\": 2,\"app_name\":\"Stasis\",\"app_data\":\"voicebot\"},
86+
\"creationtime\": \"2025-03-17T08:32:34.709-0600\",\"language\":\"en_US\"}",
13487
"timestamp": "2025-03-17T08:32:38.709-0600",
13588
"asterisk_id": "e4:1d:2d:b8:3b:a0",
13689
"application": "mystasisapp"
13790
}
13891
```
13992

140-
You should be able to figure out the response for yourself. The `message_body` is exactly the same JSON response you'd have gotten if you made this GET request via HTTP. It's escaped of course since the response message itself is JSON but you can unmarshall that into its own JSON object using whatever JSON utilities are available for your application programming language.
93+
You should be able to figure out the response for yourself. The `message_body` is exactly the same JSON response you'd have gotten if you made this GET request via HTTP. It's wrapped for clarity and it's escaped since the response message itself is JSON. You can unmarshall that into its own object using whatever JSON utilities are available for your application programming language.
14194

14295
Now let's snoop on that channel...
14396

144-
This requires us to send parameters in a query string. The `path` parameter must not include a query string which is why it's named `path` and not `uri`. Instead, query strings are specified with the `query_strings` array.
97+
This requires us to send parameters to the resource. There are 4 methods for doing this and they are processed in the order below...
98+
99+
* Adding a query string to the `uri`;
145100

146101
```json
147102
{
148103
"type": "RESTRequest",
149-
"identity": "d1eb679c-166f-4278-8b28-0340088a3410",
150-
"requests": [
151-
{
152-
"uuid": "1e409d15-a35e-4946-b270-c460cc45b2c4",
153-
"method": "POST",
154-
"path": "channels/ast-12345678.0/snoop/snoop-channel1"
155-
"query_strings": [
156-
{
157-
"spy": "both",
158-
"whisper": "none",
159-
"app": "Record",
160-
"appArgs": "myfile.wav,5,60,q"
161-
}
162-
]
163-
}
104+
"transaction_id": "d1eb679c-166f-4278-8b28-0340088a3410",
105+
"request_id": "9a4d7b2b-d0f0-402f-bfbd-296d3e0e6e1e",
106+
"method": "POST",
107+
"uri": "channels/ast-12345678.0/snoop/snoop-channel1?spy=both&whisper=none&app=Record&appArgs=myfile.wav,5,60,q"
108+
}
109+
```
110+
111+
* Using the `query-strings` array parameter:
112+
113+
```json
114+
{
115+
"type": "RESTRequest",
116+
"transaction_id": "d1eb679c-166f-4278-8b28-0340088a3410",
117+
"request_id": "9a4d7b2b-d0f0-402f-bfbd-296d3e0e6e1e",
118+
"method": "POST",
119+
"uri": "channels/ast-12345678.0/snoop/snoop-channel1",
120+
"query_strings": [
121+
"spy": "both",
122+
"whisper": "none",
123+
"app": "Record",
124+
"appArgs": "myfile.wav,5,60,q"
164125
]
165126
}
166127
```
167128

168-
We used `query_strings` to specify the snoop arguments but we could also place them in the body of the request as JSON. This requires setting a `Content-Type` header...
129+
* Using an `application/x-www-form-urlencoded` message body:
169130

170131
```json
171-
"path": "channels/ast-12345678.0/snoop/snoop-channel1"
172-
"headers": [
173-
{
174-
"name": "Content-Type",
175-
"value": "application/json"
176-
}
177-
],
178-
"message_body": "{ \"spy\": \"both\", \"whisper\": \"none\", \"app\": \"Record\", \"appArgs\": \"myfile.wav,5,60,q\" }"
132+
{
133+
"type": "RESTRequest",
134+
"transaction_id": "d1eb679c-166f-4278-8b28-0340088a3410",
135+
"request_id": "9a4d7b2b-d0f0-402f-bfbd-296d3e0e6e1e",
136+
"method": "POST",
137+
"uri": "channels/ast-12345678.0/snoop/snoop-channel1",
138+
"content_type": "application/x-www-form-urlencoded",
139+
"message_body": "spy=both&whisper=none&app=Record&appArgs=myfile.wav,5,60,q"
140+
}
179141
```
180142

181-
We used JSON in the message body but an x-www-form-urlencoded string also works. Since `Content-Type` is a common header, you can specify `content_type` directly instead of in a `headers` array. Don't forget to urlencode the string if it contains a character from the following set `[?&=% ]`. This example is fine as is...
143+
* Using an `application/json` message body:
182144

183145
```json
184-
"path": "channels/ast-12345678.0/snoop/snoop-channel1"
185-
"content_type": "application/x-www-form-urlencoded",
186-
"message_body": "spy=both&whisper=none&app=Record&appArgs=myfile.wav,5,60,q"
146+
{
147+
"type": "RESTRequest",
148+
"transaction_id": "d1eb679c-166f-4278-8b28-0340088a3410",
149+
"request_id": "9a4d7b2b-d0f0-402f-bfbd-296d3e0e6e1e",
150+
"method": "POST",
151+
"uri": "channels/ast-12345678.0/snoop/snoop-channel1",
152+
"content_type": "application/json",
153+
"message_body": "{ \"spy\": \"both\", \"whisper\": \"none\", \"app\": \"Record\", \"appArgs\": \"myfile.wav,5,60,q\" }"
154+
}
187155
```
188156

157+
/// warning
158+
PICK A METHOD! Using more than one method to pass parameters to the resource is highly discouraged because the rules for duplicates are a bit tricky. Of the first 3 methods, the first occurence wins. However, if you also use the 4th method, it will overwrite any earlier values.
159+
///
160+
189161
Server responds with:
190162

191163
```json
192164
{
193-
"type": "RESTResponseMsg",
194-
"identity": "d1eb679c-166f-4278-8b28-0340088a3410",
195-
"responses": [
196-
{
197-
"uuid": "1e409d15-a35e-4946-b270-c460cc45b2c4",
198-
"status_code": 204,
199-
"reason_phrase": "No Content",
200-
"path": "channels/ast-12345678.0/snoop/snoop-channel1"
201-
}
202-
],
165+
"type": "RESTResponse",
166+
"transaction_id": "d1eb679c-166f-4278-8b28-0340088a3410",
167+
"request_id": "9a4d7b2b-d0f0-402f-bfbd-296d3e0e6e1e",
168+
"status_code": 204,
169+
"reason_phrase": "No Content",
170+
"path": "channels/ast-12345678.0/snoop/snoop-channel1"
203171
"timestamp": "2025-03-17T08:32:39.709-0600",
204172
"asterisk_id": "e4:1d:2d:b8:3b:a0",
205173
"application": "mystasisapp"
@@ -211,13 +179,3 @@ That's all there is to it.
211179
## Caveats
212180

213181
There's really only one... You can't get binary data like recordings via the websocket. The frames written to the underlying websocket use the TEXT opcode and the messages are all JSON and while there are ways we could send binary data, they're just too complicated and could interfere with getting asynchronous events. Attempting to retrieve binary data will result in a 406 "Not Acceptable. Use HTTP GET" response.
214-
215-
## Differences between SwaggerSocket and ASTSwaggerSocket
216-
217-
To fit within the existing ARI archictecture several modifications had to be made to the SwaggerSocket API:
218-
219-
* Requests must include a `type` parameter to allow the request to be routed properly.
220-
* SwaggerSocket uses "camelCase" for protocol parameter names but ARI requires parameter names to be "snake_case".
221-
* Responses are encapsulated in ARI events which will include the `type`, `timestamp`, `asterisk_id` and `application` parameters.
222-
* Although the SwaggerSocket protocol supports more than one request in a message, ASTSwaggerSocket will only process the first request in the array.
223-
* SwaggerSocket's `statusCode` parameter is renamed to `status_code` to comply with the "snake_case" ARI requirement and it's also a JSON number rather than a string. `reasonPhrase` is also renamed to `reason_phrase`.

0 commit comments

Comments
 (0)