HTTP API

/health, /calls and /sip/originate.

Three endpoints, served on SIP_HTTP_PORT (default 8082). Only /sip/originate is authenticated.

MethodPathAuth
GET/healthnone
GET/callsnone
POST/sip/originateAuthorization: Bearer <ulai api key>

GET /health

curl -s http://148.113.58.51:8082/health
{ "status": "ok", "service": "sip-sfu-gateway" }

Always 200 while the process is serving. It says nothing about the SIP leg or the routing store — see observability for a check that covers both.

GET /calls

Every call currently bridged, oldest first.

curl -s http://148.113.58.51:8082/calls
{
  "live": [
    {
      "to_number": "+919363399639",
      "from_number": "+13187184515",
      "name": "sip-module",
      "sip_call_id": "3e114ad4-a9ad-4a0b-97b9-5cf11190bdd8",
      "session_id": "527f78a3262dbe8b0d0a678a9e98ac29",
      "project_id": "fa3f23f8-a75d-44a4-84f0-6a1c828f579f",
      "direction": "outbound",
      "source": "originate",
      "started_at": "2026-09-21T10:14:52.118Z"
    }
  ]
}
FieldMeaning
to_number, from_numberNormalised E.164
nameDisplay name in the room roster
sip_call_idThe SIP Call-ID — the join key between logs, events and carrier CDRs
session_idThe Ulai room
project_idOwning project; may be empty inbound if the host lookup missed
directioninbound or outbound
sourceoriginate, resolver, or the session header’s name when a room was supplied on the INVITE
started_atWhen the bridge started, RFC 3339

The list is sorted rather than map-ordered, so polling it does not reshuffle rows on every request.

POST /sip/originate

Places one outbound call. Authenticates with a control-plane API key, and the room opened for the call belongs to the project that key identifies — which is how one gateway serves several projects without per-project configuration.

curl -X POST http://148.113.58.51:8082/sip/originate \
  -H 'Authorization: Bearer ulai_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' \
  -H 'Content-Type: application/json' \
  -d '{
    "to_number":        "+919363399639",
    "trunk_id":         "74958094-8c76-419f-aa42-d4eac6768ec2",
    "project_id":       "fa3f23f8-a75d-44a4-84f0-6a1c828f579f",
    "from_number":      "+13187184515",
    "name":             "sip-module",
    "max_participants": 8,
    "offer_srtp":       true,
    "require_srtp":     true,
    "dial_verbatim":    false,
    "session_id":       "527f78a3262dbe8b0d0a678a9e98ac29"
  }'

The smallest request that works — the gateway creates the room and infers SRTP policy from the trunk’s transport:

curl -X POST http://148.113.58.51:8082/sip/originate \
  -H "Authorization: Bearer $ULAI_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "to_number": "+919363399639",
    "trunk_id":  "74958094-8c76-419f-aa42-d4eac6768ec2",
    "project_id":"fa3f23f8-a75d-44a4-84f0-6a1c828f579f"
  }'

Request body

FieldTypeRequiredDescription
to_numberstringyesDestination, E.164. Normalised before dialling.
trunk_idstringyesOutbound trunk within the project. The resolver turns it into an address, a transport and credentials — none of which are ever sent on the wire.
project_idstringunless ULAI_PROJECT_ID is setScopes trunk_id.
session_idstringnoDial into a room that already exists. Omit it and the gateway creates one and returns its id.
from_numberstringnoCaller ID presented to the trunk.
namestringnoDisplay name in the room roster. Defaults to to_number.
agent_idstringnoRecorded in session metadata and visible on the control plane’s discovery feed, so an orchestrator can tell which agent the room is waiting for. Unused by the gateway otherwise.
max_participantsintnoSizes a newly created room. Ignored when session_id is given. Defaults to ULAI_MAX_PARTICIPANTS.
offer_srtpboolnoOffer RTP/SAVP with an a=crypto line. Defaults to true on a TLS trunk, false otherwise.
require_srtpboolnoFail the call if the answer carries no crypto. Implies offer_srtp.
dial_verbatimboolnoSend to_number without a leading +. For self-hosted or CUSTOM trunks whose dialplan matches literal digit patterns; hosted ITSPs want E.164-with-plus.

Bodies are capped at 8 KiB. Validation reports every problem at once, so a dialler does not get one complaint per round trip.

202 Accepted

{
  "status": "originating",
  "to_number": "+919363399639",
  "session_id": "527f78a3262dbe8b0d0a678a9e98ac29",
  "created": false,
  "name": "sip-module",
  "trunk": "Main SBC"
}

created says whether the gateway made the room (and is therefore responsible for tearing it down). The response returns as soon as the trunk is resolved and the room exists — the phone is still ringing. Point an agent or a browser at session_id while it does; the gateway itself joins the room only once the callee answers.

Errors

StatusCause
400Malformed JSON, failed validation, or no project_id and no ULAI_PROJECT_ID
401Missing or malformed Authorization: Bearer
404No such trunk for that project, or no SIP config for the project
502Room creation failed, or the stored trunk address is malformed
503The gateway is shutting down
{ "error": "to_number is required\ntrunk_id is required" }

A 404 means the request asked for something that is not there; a 502 means an upstream failed. Only one of those is worth retrying.

What happens next

Nothing else comes back over HTTP. The call’s progress shows up in three places:

  • The log, prefixed with the number.
  • GET /calls, once the bridge starts.
  • Telecom eventsCALL_ANSWERED then CALL_HANGUP, published to the platform’s stream. See routing.

If the callee never answers, the room created for the call is terminated rather than left running.

Last modified September 21, 2026: sip module (0ee6a81)