Running a SIP service with your own mobile apps has plenty of challenges. One shows up on day one: how do you call a phone that isn't connected?
In SIP, a device has to be registered before you can send it anything, whether that's an INVITE or a CANCEL. Registering means telling the server “I'm reachable at this address”. Mobile operating systems don't care about this. When the app we built for SpecialCall goes to the background, iOS and Android soon close its WebSocket, and the registration is left pointing at a socket that no longer exists. If we send an INVITE at that moment, nothing answers.
The fix is well known: send the phone a push notification, wait for the app to wake up and register again, and only then send the INVITE. The hard part is the waiting. This post explains how we do it.
Where we started
At first our backend handled all of this. When it wanted to ring an interpreter, it sent the push notification itself and then kept trying to start the call through Asterisk until the device registered, giving up after 60 seconds. It worked, but it was a polling loop spread across two systems. The push and the call didn't know about each other, every retry cost time, and the backend had to understand both SIP state and APNs/FCM.
So we made a big change to our SIP setup: we put a SIP proxy in front of the apps. A proxy can hold an INVITE and wait for a registration on its own. We no longer need the retry loop.
OpenSIPS
The proxy we chose is OpenSIPS, a mature open source SIP server that has been in production use for close to twenty years. It isn't a PBX like Asterisk. It doesn't mix audio, play prompts or bridge channels. Its job is to route SIP messages, and it does that very fast and with a lot of flexibility.
You program OpenSIPS with its own scripting language. The config file is a set of routes, which are blocks of logic that run for every request, reply, failure or internal event. Features come from modules that you load as needed. For example, registrar and usrloc act as a registrar and keep a location table, tm handles stateful transactions, rest_client makes HTTP calls, and auth_db handles digest authentication.
What made it a good fit for us is that it supports RFC 8599 natively. RFC 8599 defines how a SIP device includes its push details in the Contact of its REGISTER. The registrar module understands those parameters. When an INVITE arrives for a device whose socket is gone, OpenSIPS can hold the INVITE, fire an event so a push gets sent, and release the INVITE once the device registers again.
In our setup OpenSIPS is also the registrar. The apps connect to it over secure WebSockets (WSS) and register there, and Asterisk sends their calls to OpenSIPS over a plain UDP trunk. As a bonus, OpenSIPS checks app credentials directly against Asterisk's PJSIP auth table, so we didn't have to migrate any credentials.
Gorush
OpenSIPS doesn't send push notifications itself. It tells you when one is needed and leaves the sending to you. We didn't want to write another APNs/FCM client, so we used another open source project: Gorush.
Gorush is a push notification server written in Go. It's a single binary with one HTTP endpoint (POST /api/push) that accepts a JSON list of notifications and delivers them through APNs (iOS) or FCM (Android). It handles the provider connections, auth keys and retries, and it exposes a health check and Prometheus metrics.
It runs on the same machine as OpenSIPS and listens only on 127.0.0.1. Only OpenSIPS ever talks to it. We run it in sync mode, so each HTTP request waits until Apple or Google replies and returns the result for every token. That gives us one log line per call saying whether the wake-up push went out.
How the apps register
The apps register with the RFC 8599 parameters on their Contact URI:
Contact: <sip:interpreter@...;transport=ws; pn-provider=apns; pn-param=<TEAM_ID>.example.voip; pn-prid=<PushKit token>>pn-providerisapnson iOS andfcmon Android.pn-paramidentifies the app: the APNs team and bundle ID with the.voipsuffix, or the Firebase project.pn-pridis the device's push token. On iOS this is the PushKit VoIP token, not the normal remote notification token. Only VoIP pushes can wake an app that is completely closed and show the native call screen through CallKit.
We use a registration expiry of one week. Interpreters need to stay reachable for a long time even if they haven't opened the app, and therefore haven't registered again, in days. The WebSocket behind the registration will be dead long before the week is up. That's fine, because what we need to keep is the contact and its push parameters. As long as OpenSIPS has those, it can wake the phone.
Life of a call
- SIP
- push / HTTP
- call control (ARI)
- INVITE held
Backend. Decides who to call: which interpreters speak the right language, are available and aren't already on a call. Then it asks Asterisk to start a call to each of them.
Asterisk. Creates the channel and sends the INVITE to the OpenSIPS trunk.
OpenSIPS. Sees that the INVITE is for one of its registered users and looks the user up in the location table. There are three possible outcomes:
- The device is connected. The INVITE is relayed right away and no push is sent.
- The device has a push-enabled contact. The registrar holds the INVITE and fires an
E_UL_CONTACT_REFRESHevent. Our event route builds the Gorush request and POSTs it. When the app registers again, OpenSIPS releases the held INVITE to the new connection. - The device isn't registered at all. There is nothing to wake up, so OpenSIPS rejects the INVITE right away and the backend moves on.
Gorush. Delivers the notification through APNs or FCM.
App. Wakes up, shows the incoming call (CallKit on iOS), registers, and receives the INVITE.
Result
The backend no longer knows push notifications exist. It asks Asterisk to start a call and Asterisk sends an INVITE, the same as calling a desk phone. Waking the device, waiting for it and handling a race with another interpreter all happen in one place, inside a single SIP transaction, using two open source components and a few hundred lines of OpenSIPS config.



