Skip to content

HTTP transport ​

The HTTP transport is the REST interface of the Janus API. A client POSTs a JSON request to a URL and reads the JSON reply. For events it keeps a GET open on the session (a long poll). Browsers use it through janus.js, scripts use it through curl. The Admin API has its own HTTP listener, on another port and path.

Enable and configure ​

The transport reads janus.transport.http.jcfg from the configs folder. With no file, or with http and admin_http both false, it binds nothing.

ini
general: {
	json = "indented"        # indented, plain or compact (the default)
	base_path = "/janus"     # where the Janus API lives
	http = true              # turn the plain HTTP listener on
	port = 8088
}
admin: {
	admin_base_path = "/admin"
	admin_http = true        # the Admin API is off unless you say so
	admin_port = 7088
}
cors: {
	allow_origin = "*"       # see "CORS" below
}

The full list of keys (HTTPS, interface and IP binding, acl, mhd_connection_limit, certificates) is in the transport files reference. janus.jcfg decides api_secret and admin_secret.

Bind to one address

Without ip the listener binds on every address, IPv6 included. For a test machine set ip = "127.0.0.1", and admin_ip = "127.0.0.1" for the Admin API.

Try it ​

Every reply below came from the Rust binary. The ports in the config above are the standard ones; the recorded run used free ports, which do not show in a path.

A session from start to finish ​

GET /janus/info and GET /janus/ping need no body. Everything else is a POST of a JSON body. The ids of the session and the handle go in the path.

jsonc
// Server information. The lists of plugins, transports and loggers are shortened here.

// → GET /janus/info

// ← 200 OK
// access-control-allow-origin: *
// access-control-allow-credentials: true
// access-control-max-age: 86400
// vary: Origin
// content-type: application/json
{
  "janus": "server_info",
  "transaction": "up8o8wWzNmv",
  "name": "Janus WebRTC Server",
  "version": 1404,
  "version_string": "1.4.4",
  "author": "Meetecho s.r.l.",
  "commit-hash": "not-a-git-repo",
  "compile-time": "Wed Nov  4 14:40:20 UTC 2026",
  "log-to-stdout": true,
  "log-to-file": false,
  "data_channels": true,
  "accepting-new-sessions": true,
  "session-timeout": 60,
  "reclaim-session-timeout": 0,
  "candidates-timeout": 45,
  "server-name": "MyJanusInstance",
  "local-ip": "192.0.2.10",
  "ipv6": false,
  "ice-lite": false,
  "ice-tcp": false,
  "ice-nomination": "aggressive",
  "ice-consent-freshness": false,
  "ice-keepalive-conncheck": false,
  "hangup-on-failed": false,
  "full-trickle": false,
  "mdns-enabled": true,
  "min-nack-queue": 200,
  "nack-optimizations": false,
  "twcc-period": 200,
  "dtls-mtu": 1200,
  "static-event-loops": 0,
  "api_secret": false,
  "auth_token": false,
  "event_handlers": false,
  "opaqueid_in_api": false,
  "dependencies": {},
  "transports": {
    "janus.transport.http": {
      "name": "JANUS REST (HTTP/HTTPS) transport plugin",
      "author": "Meetecho s.r.l.",
      "description": "This transport plugin adds REST (HTTP/HTTPS) support to the Janus API via libmicrohttpd.",
      "version_string": "0.0.2",
      "version": 2
    },
    "…": "1 more"
  },
  "events": {},
  "loggers": {
    "janus.logger.jsonlog": {
      "name": "JANUS JSON logger plugin",
      "author": "Meetecho s.r.l.",
      "description": "This is a trivial sample logger plugin for Janus, which saves log lines to a local JSON file.",
      "version_string": "0.0.1",
      "version": 1
    }
  },
  "plugins": {
    "janus.plugin.audiobridge": {
      "name": "JANUS AudioBridge plugin",
      "author": "Meetecho s.r.l.",
      "description": "This is a plugin implementing an audio conference bridge for Janus, mixing Opus streams.",
      "version_string": "0.0.13",
      "version": 13
    },
    "…": "10 more"
  }
}

// Ping the core with a GET (an HTTP-only shortcut; the JSON `ping` request works too).

// → GET /janus/ping

// ← 200 OK
{
  "janus": "pong",
  "transaction": "Plr5FExQsIp"
}

// Create a session. POST the JSON request to the base path.

// → POST /janus
{
  "janus": "create",
  "transaction": "create-1"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "create-1",
  "data": {
    "id": 1549595476763990
  }
}

// Attach a plugin handle. The session id goes in the path.

// → POST /janus/1549595476763990
{
  "janus": "attach",
  "plugin": "janus.plugin.echotest",
  "transaction": "attach-1"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 1549595476763990,
  "transaction": "attach-1",
  "data": {
    "id": 8893193682997906
  }
}

// Send a plugin message. The session and handle ids go in the path. The reply is an ack; the plugin's answer arrives on the long poll.

// → POST /janus/1549595476763990/8893193682997906
{
  "janus": "message",
  "transaction": "msg-1",
  "body": {
    "audio": true
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 1549595476763990,
  "transaction": "msg-1",
  "hint": "I'm taking my time!"
}

// Long poll: GET the session path. It returns as soon as an event is waiting.

// → GET /janus/1549595476763990?maxev=5

// ← 200 OK
[
  {
    "janus": "event",
    "session_id": 1549595476763990,
    "transaction": "msg-1",
    "sender": 8893193682997906,
    "plugindata": {
      "plugin": "janus.plugin.echotest",
      "data": {
        "echotest": "event",
        "result": "ok"
      }
    }
  }
]

// Keep the session alive (a long poll counts too).

// → POST /janus/1549595476763990
{
  "janus": "keepalive",
  "transaction": "ka-1"
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 1549595476763990,
  "transaction": "ka-1"
}

// Detach and destroy.

// → POST /janus/1549595476763990/8893193682997906
{
  "janus": "detach",
  "transaction": "detach-1"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 1549595476763990,
  "transaction": "detach-1"
}

// → POST /janus/1549595476763990
{
  "janus": "destroy",
  "transaction": "destroy-1"
}

// ← 200 OK
{
  "janus": "success",
  "session_id": 1549595476763990,
  "transaction": "destroy-1"
}

The message request returned an ack straight away: the plugin works on it in the background. Its answer arrived as an event on the long poll.

The long poll ​

A client keeps one GET /janus/<session> open. Janus answers it when an event is ready.

jsonc
// Session 3963298522899894 and handle 3441631099868049 are already set up. Two messages queue two events.

// → POST /janus/3963298522899894/3441631099868049
{
  "janus": "message",
  "transaction": "m1",
  "body": {
    "audio": true
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 3963298522899894,
  "transaction": "m1",
  "hint": "I'm taking my time!"
}

// → POST /janus/3963298522899894/3441631099868049
{
  "janus": "message",
  "transaction": "m2",
  "body": {
    "audio": false
  }
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 3963298522899894,
  "transaction": "m2",
  "hint": "I'm taking my time!"
}

// maxev=1 (the default) returns one event, as a bare object.

// → GET /janus/3963298522899894?maxev=1

// ← 200 OK
{
  "janus": "event",
  "session_id": 3963298522899894,
  "transaction": "m1",
  "sender": 3441631099868049,
  "plugindata": {
    "plugin": "janus.plugin.echotest",
    "data": {
      "echotest": "event",
      "result": "ok"
    }
  }
}

// maxev greater than 1 returns an array, here with the one event that is left.

// → GET /janus/3963298522899894?maxev=5

// ← 200 OK
[
  {
    "janus": "event",
    "session_id": 3963298522899894,
    "transaction": "m2",
    "sender": 3441631099868049,
    "plugindata": {
      "plugin": "janus.plugin.echotest",
      "data": {
        "echotest": "event",
        "result": "ok"
      }
    }
  }
]

// Nothing queued: the poll waits 30 seconds, then answers with a keepalive so the client can poll again.

// → GET /janus/3963298522899894

// ← 200 OK (after 30 s)
{
  "janus": "keepalive"
}

// That poll took 30.0 s.

// A poll for a session that does not exist is a bare 404.

// → GET /janus/123456

// ← 404 Not Found

// A GET on the handle path (session and handle) is redirected to the session path, where the long poll lives.

// → GET /janus/3963298522899894/3441631099868049

// ← 302 Found
// location: /janus/3963298522899894

Errors ​

jsonc
// Errors in the request are JSON replies with HTTP status 200. The `error.code` is the Janus error code.

// → POST /janus
{

// ← 200 OK
{
  "janus": "error",
  "error": {
    "code": 454,
    "reason": "JSON error: on line 1: string or '}' expected near end of file"
  }
}

// → POST /janus
[
  1
]

// ← 200 OK
{
  "janus": "error",
  "error": {
    "code": 455,
    "reason": "JSON error: not an object"
  }
}

// → POST /janus
{
  "janus": "create"
}

// ← 200 OK
{
  "janus": "error",
  "error": {
    "code": 456,
    "reason": "Missing mandatory element (transaction)"
  }
}

// → POST /janus
{
  "transaction": "x"
}

// ← 200 OK
{
  "janus": "error",
  "error": {
    "code": 456,
    "reason": "Missing mandatory element (janus)"
  }
}

// → POST /janus
{
  "janus": "bogus",
  "transaction": "x"
}

// ← 200 OK
{
  "janus": "error",
  "transaction": "x",
  "error": {
    "code": 457,
    "reason": "Unhandled request 'bogus' at this path"
  }
}

// → POST /janus/123456
{
  "janus": "keepalive",
  "transaction": "k"
}

// ← 200 OK
{
  "janus": "error",
  "session_id": 123456,
  "transaction": "k",
  "error": {
    "code": 458,
    "reason": "No such session 123456"
  }
}

// → POST /janus/7068532727520757/123456
{
  "janus": "message",
  "transaction": "m",
  "body": {}
}

// ← 200 OK
{
  "janus": "error",
  "session_id": 7068532727520757,
  "transaction": "m",
  "error": {
    "code": 459,
    "reason": "No such handle 123456 in session 7068532727520757"
  }
}

// Problems with the path or the HTTP method use HTTP status codes, with an empty body.

// → GET /janus/not-a-session

// ← 404 Not Found

// → GET /elsewhere

// ← 404 Not Found

// → POST /janus/info
{}

// ← 400 Bad Request

// DELETE gets a Janus error. The Janus API has no other methods: PUT and PATCH, an empty POST body and a path with more than session and handle get no reply at all, and the server closes the connection.

// → DELETE /janus

// ← 200 OK
{
  "janus": "error",
  "error": {
    "code": 450,
    "reason": "Unsupported method DELETE"
  }
}

// → PUT /janus
{
  "janus": "ping",
  "transaction": "p"
}

// ← connection closed by the server, no reply (UND_ERR_SOCKET)

// → POST /janus

// ← connection closed by the server, no reply (UND_ERR_SOCKET)

CORS ​

A browser on another origin sends an OPTIONS preflight first. Janus answers it with the headers the browser asks for.

jsonc
// 1. No [cors] section: the wildcard.

// → OPTIONS /janus
// Origin: https://app.example
// Access-Control-Request-Method: POST
// Access-Control-Request-Headers: content-type

// ← 200 OK
// access-control-allow-origin: *
// access-control-allow-methods: POST
// access-control-allow-headers: content-type
// access-control-max-age: 86400

// → POST /janus
// Origin: https://app.example
{
  "janus": "ping",
  "transaction": "p"
}

// ← 200 OK
// access-control-allow-origin: *
// access-control-max-age: 86400
{
  "janus": "pong",
  "transaction": "p"
}

// 2. allow_origin = "https://app.example": that value, plus Allow-Credentials and Vary.

// → OPTIONS /janus
// Origin: https://app.example
// Access-Control-Request-Method: POST
// Access-Control-Request-Headers: content-type

// ← 200 OK
// access-control-allow-origin: https://app.example
// access-control-allow-credentials: true
// access-control-allow-methods: POST
// access-control-allow-headers: content-type
// access-control-max-age: 86400
// vary: Origin

// 3. Also enforce_cors = true: a request from another origin gets 403, with no body.

// → POST /janus
// Origin: https://app.example
{
  "janus": "ping",
  "transaction": "ok"
}

// ← 200 OK
// access-control-allow-origin: https://app.example
// access-control-allow-credentials: true
// access-control-max-age: 86400
// vary: Origin
{
  "janus": "pong",
  "transaction": "ok"
}

// → POST /janus
// Origin: https://evil.example
{
  "janus": "ping",
  "transaction": "no"
}

// ← 403 Forbidden
// access-control-allow-origin: https://app.example
// access-control-allow-credentials: true
// access-control-max-age: 86400
// vary: Origin

// → POST /janus
{
  "janus": "ping",
  "transaction": "none"
}

// ← 403 Forbidden (no Origin header)
// access-control-allow-origin: https://app.example
// access-control-allow-credentials: true
// access-control-max-age: 86400
// vary: Origin

Another base path, and the Admin API ​

jsonc
// Config: base_path = "/gateway", admin_base_path = "/ctl". The old paths are gone.

// → GET /janus/info

// ← 404 Not Found

// → POST /gateway
{
  "janus": "create",
  "transaction": "c1"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "c1",
  "data": {
    "id": 2433102275000559
  }
}

// → GET /gateway/ping

// ← 200 OK
{
  "janus": "pong",
  "transaction": "qA2ETEhvRh5"
}

// → GET /ctl/ping

// ← 200 OK
{
  "janus": "pong",
  "transaction": "TFtg2QicDep"
}

// → GET /admin/ping

// ← 404 Not Found
jsonc
// The Admin API listens on its own port and path. `info` and `ping` need no secret (unless restrict_info_api is set).

// → GET /admin/ping

// ← 200 OK
{
  "janus": "pong",
  "transaction": "TcoDdG98eef"
}

// → GET /admin/info

// ← 200 OK
{
  "janus": "server_info",
  "name": "Janus WebRTC Server",
  "version_string": "1.4.4",
  "…": "same body as the Janus API info"
}

// Everything else needs `admin_secret`.

// → POST /admin
{
  "janus": "list_sessions",
  "transaction": "l1"
}

// ← 200 OK
{
  "janus": "error",
  "transaction": "l1",
  "error": {
    "code": 403,
    "reason": "Unauthorized request (wrong or missing secret/token)"
  }
}

// → POST /admin
{
  "janus": "list_sessions",
  "transaction": "l2",
  "admin_secret": "janusoverlord"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "l2",
  "sessions": []
}

// A session created on the Janus API port shows up:

// → POST /admin
{
  "janus": "list_sessions",
  "transaction": "l3",
  "admin_secret": "janusoverlord"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "l3",
  "sessions": [
    3092723475851060
  ]
}

// The two APIs do not mix: Admin requests on the Janus port, and Janus requests on the Admin port, are refused.

// → POST /janus
{
  "janus": "list_sessions",
  "transaction": "x1",
  "admin_secret": "janusoverlord"
}

// ← 200 OK
{
  "janus": "error",
  "transaction": "x1",
  "error": {
    "code": 457,
    "reason": "Unhandled request 'list_sessions' at this path"
  }
}

// → POST /admin
{
  "janus": "create",
  "transaction": "x2",
  "admin_secret": "janusoverlord"
}

// ← 200 OK
{
  "janus": "error",
  "transaction": "x2",
  "error": {
    "code": 457,
    "reason": "Unhandled request 'create' at this path"
  }
}

The API secret ​

jsonc
// With `api_secret` set in janus.jcfg, every request except `info` and `ping` needs the secret, in the body (`apisecret`) or, for the long poll, in the query.

// → POST /janus
{
  "janus": "create",
  "transaction": "no-secret"
}

// ← 200 OK
{
  "janus": "error",
  "transaction": "no-secret",
  "error": {
    "code": 403,
    "reason": "Unauthorized request (wrong or missing secret/token)"
  }
}

// → POST /janus
{
  "janus": "create",
  "transaction": "with-secret",
  "apisecret": "docs-secret"
}

// ← 200 OK
{
  "janus": "success",
  "transaction": "with-secret",
  "data": {
    "id": 7028426702966774
  }
}

// → GET /janus/7028426702966774

// ← 403 Forbidden (without the secret)

// → POST /janus/7028426702966774
{
  "janus": "keepalive",
  "transaction": "k",
  "apisecret": "docs-secret"
}

// ← 200 OK
{
  "janus": "ack",
  "session_id": 7028426702966774,
  "transaction": "k"
}

// → POST /janus
{
  "janus": "ping",
  "transaction": "ping-no-secret"
}

// ← 200 OK
{
  "janus": "pong",
  "transaction": "ping-no-secret"
}

// → GET /janus/info

// ← 200 OK
{
  "janus": "server_info",
  "name": "Janus WebRTC Server",
  "api_secret": true,
  "…": "rest of the info body"
}

// The long poll takes the secret as a query parameter. Janus answers at once when something is queued; here nothing is, so the poll waits 30 s and returns a keepalive.

// → GET /janus/7028426702966774?apisecret=docs-secret&maxev=1

// ← 200 OK
{
  "janus": "keepalive"
}

Output format, and the access list ​

jsonc
// → json = "indented", GET /janus/ping

// ← 200 OK
{
   "janus": "pong",
   "transaction": "abc123def45"
}

// → json = "plain", GET /janus/ping

// ← 200 OK
{"janus": "pong", "transaction": "abc123def45"}

// → json = "compact", GET /janus/ping

// ← 200 OK
{"janus":"pong","transaction":"abc123def45"}

// Access list: admin_acl = "10." allows only addresses that start with 10. A client from 127.0.0.1 is cut off without a reply; the Janus API port, with no acl, still answers.

// → GET /janus/ping

// ← 200 OK
{
  "janus": "pong",
  "…": ""
}

// → GET /admin/ping

// ← connection closed by the server, no reply (ECONNRESET)

HTTPS ​

jsonc
// Config: https = true, secure_port, and a [certificates] block with cert_pem and cert_key. The certificate is self-signed (openssl req -x509 ... -addext subjectAltName=...), so curl needs -k.

// → curl -k -i https://localhost:8089/janus/ping

// ← HTTP/1.1 200 OK
// content-type: application/json
// access-control-allow-origin: *
// access-control-allow-credentials: true
// vary: Origin
// access-control-max-age: 86400
// transfer-encoding: chunked
{
   "janus": "pong",
   "transaction": "abc123def45"
}

Reference ​

Janus API URLs ​

base is base_path (/janus by default). The paths are case sensitive.

Method and pathWhat it does
GET base/infoServer information, as an info request. No secret needed unless restrict_info_api is set.
GET base/pingA ping, answered with pong.
POST baseA request that needs no session: create, info, ping.
POST base/<session>A request for a session: attach, keepalive, destroy, claim.
POST base/<session>/<handle>A request for a handle: message, trickle, detach, hangup.
GET base/<session>The long poll: returns the next events of the session.
GET base/<session>/<handle>Redirects (302) to base/<session>.
OPTIONS on any path under baseThe CORS preflight. Always 200.

The session id can also be in the JSON body of a POST to base (session_id). If it is in both and they differ, the reply is error 457 Conflicting session ID (payload and path). A path with more components than <session>/<handle> gets no reply on POST and 404 on GET (see below).

The server does not look at Content-Type: text/plain, a form type and application/json were all accepted.

Query parameters of the long poll ​

ParameterTypeMeaning
maxevintegerThe most events to return. The default is 1, which returns one event as a bare object. A number above 1 returns an array. An invalid value falls back to 1.
apisecretstringThe API secret, when api_secret is set. A POST carries it in the body (apisecret); a GET has no body, so it goes here.
tokenstringThe token, when token authentication is on.

A poll with no event pending waits 30 seconds and returns {"janus": "keepalive"}. Start the next poll at once. In the C source a long poll also counts as a keepalive for the session, so a client that always has a poll open does not need to send keepalive. I did not time that out against the session timeout here.

Status codes ​

CodeWhen
200A Janus reply, including a Janus error. The error is in the JSON (janus: "error").
302GET on a handle path.
400POST to info or ping paths. These two are GET only.
403The long poll with a wrong or missing secret or token. With enforce_cors, a request from the wrong origin.
404Outside base_path, a long poll for a session that is not there, a GET on base with no session, a GET with too many path components. Empty body.
no replyThe server closes the connection, with no response: PUT or PATCH, an empty POST body, a POST with too many path components, an OPTIONS outside base_path.

DELETE is the odd one: it gets a Janus error, code 450 Unsupported method DELETE, with status 200.

CORS ​

Janus adds Access-Control-Allow-Origin and Access-Control-Max-Age: 86400 to every reply. On a preflight it echoes Access-Control-Request-Method as Access-Control-Allow-Methods and Access-Control-Request-Headers as Access-Control-Allow-Headers.

SettingEffect
no [cors] sectionAccess-Control-Allow-Origin: *.
allow_origin = "https://app.example"That value, plus Access-Control-Allow-Credentials: true and Vary: Origin. Needed for requests with withCredentials.
allow_origin and enforce_cors = trueJanus checks Origin (or Referer) itself and answers 403 to a request that does not start with allow_origin. A request with no Origin is refused too.

Setting allow_origin = "*" is not the same as leaving it out: the credentials and Vary headers appear. The dev config of Janus Bench does this.

The Admin API listener ​

The Admin API listens on admin_port under admin_base_path. It has the same URLs for GET info, GET ping and POST; the POST body is an Admin request with admin_secret. It does not accept Janus requests, and the Janus listener does not accept Admin requests: both answer error 457 Unhandled request. admin_acl, admin_ip, admin_interface, admin_https and the certificates work as their Janus counterparts.

Access list ​

acl and admin_acl take a comma separated list of address prefixes ("127.,192.168.0."). A client that does not match is cut off with no reply. With acl_forwarded = true the X-Forwarded-For header is checked as well; leave it off unless a trusted proxy sets that header.

Behaviour you may not expect ​

  • verified A GET of a session that does not exist is a bare 404, not a JSON error. A POST for the same session is a 200 with error 458.
  • verified The long poll waits 30 s, not forever. That is why a client sees keepalive events with no session id: it is the poll timing out.
  • verified maxev=1 returns an object, maxev=5 returns an array even when it holds one event.
  • verified Setting json in the file changes only the format of the bytes, never the fields. All three forms parse to the same value.
  • Two long polls open on the same session: the first one is not cut when the second arrives. Each waits its own 30 s. I did not check what the C server does here.

Known gaps ​

  • gap query_transport is not available: {"janus": "query_transport", "transport": "janus.transport.http", ...} answers error 490. The HTTP crate has the connection-count code, but the binary does not wire the Admin request to it.
  • gap HTTPS needs an X.509 version 3 certificate. A version 1 certificate (the kind openssl req -x509 of LibreSSL on macOS makes without -addext) is refused at start with UnsupportedCertVersion, and the HTTPS listener does not start. Add any -addext extension, for example subjectAltName.
  • gap general.interface resolves by name to the first IPv4 address of a running interface. This was checked in the transport crate, not run here.
  • gap mhd_debug has no effect: there is no libmicrohttpd.
  • The Rust listener is hyper, not libmicrohttpd. The differential harness compares status codes, bodies and the CORS headers with the C server's in its http-edge, http-cors, http-longpoll and base path scenarios. It does not compare header order, Date or Server.
  • parity/status.json lists the open items for this component: the MHD behaviour after a second reply to one request, and connections counts for HTTPS listeners.

Test it in Janus Bench ​

The Bench uses HTTP by default. core.info.http, core.session.http, core.errors and core.longpoll on the Run all checks page exercise the paths above. The Raw console page sends any request over HTTP.

GPL-3.0-or-later, like the Janus gateway it ports.