Fetch, don't deliver: how QC-Phone Plus brings incoming SIP calls to a sleeping iPhone
By Robert Brandt, QuadChroma
Published . Status: design. QC-Phone Plus is in development and not yet available.
The problem
QC-Phone is a SIP phone app for iPhone and iPad. It works with the SIP account the user already has. An app like this cannot stay connected to the SIP provider in the background: iOS suspends it after a short time, and a suspended app cannot receive a SIP INVITE.
For calls to reach a sleeping phone, something on the network has to receive the call on the phone's behalf and wake the phone. That is the job of the optional QC-Phone Plus service.
The usual way is to deliver the call. A gateway receives it, wakes the app, waits until the app has reconnected or registered again, and then forwards the call to the app.
We are building it the other way round. Our server never delivers a call to the phone. It holds the call and only tells the phone who is calling. If the user answers, the phone fetches the call itself.
This article describes the design in enough detail to build it. We publish it so that anyone can use it.
The parts
- The SIP provider the user already has. Nothing changes there.
- The Plus server. It keeps the user's SIP account registered and holds incoming calls. It never handles audio.
- The push service of the operating system. On iOS these are VoIP pushes through Apple's push service (PushKit).
- Optionally, at home: a persistent connection that the operating system keeps open on Wi-Fi networks the user selects (on iOS: Local Push Connectivity).
- The QC-Phone app on iPhone or iPad.
Setup
- The user activates Plus in the app. Over HTTPS, the app sends the server:
- the provider's registrar and the SIP user name;
- a password hash (HA1), never the password itself;
- its push token;
- a public key generated on the device.
- The server tells the app once which server name to use. The app stores it. Failover works through DNS under that same name.
- The server registers the SIP account at the provider with its own contact address, over TLS where the provider supports it. There is one registration per SIP account, even if the subscription is used on several devices.
- The app keeps no registration and no connection with the server. This holds in the foreground and in the background. The app never tells the server whether it is in the foreground or the background.
- In Plus mode the app does not register at the provider either, so the provider only ever sees one registration. Outgoing calls go directly from the app to the provider and never through the server.
An incoming call, step by step
- The provider sends an INVITE to the server. The server answers provisionally (100 Trying, 180 Ringing), so the caller hears ringing, and holds the INVITE in memory. It holds at most one call per account at a time; a second call meanwhile is answered with 486 Busy Here.
- The server announces the call to each active device, exactly once per device. The announcement is encrypted end to end with the device's public key (HPKE, RFC 9180). It contains only what the phone needs to show the call: the caller's number, the caller's display name and the time it was sent. It contains no server address, no identifier of the call, no session description and no credentials. The push service only sees that a message went to a device.
- The phone rings. On iOS it reports the call to CallKit right away. Until the user answers, the app sends nothing to the network.
- The user taps Answer. Only now does the app connect over HTTPS to the server name it stored at setup and authenticate with its device key. It asks for the call held for its account, receives the provider's session description (the SDP offer) and sends back its SDP answer. Nothing from the announcement is needed for this: the server finds the call through the account.
- The server answers the held INVITE with 200 OK carrying the phone's SDP answer. The provider sends ACK.
- If the provider's INVITE had no SDP, the phone sends an offer instead. The server puts it into the 200 OK, and the provider's answer from the ACK reaches the phone over HTTPS.
- Audio flows directly between provider and phone, encrypted with SRTP where the provider supports it. The server passes the SDP through unchanged and never relays media. On the phone the call stays an incoming call from start to finish.
- Between phone and server there is no SIP at all, only HTTPS. The server alone keeps the SIP dialog with the provider.
During the call
- After answering, the phone keeps an HTTPS status request open (long-poll). It reports when the provider hangs up or sends a new offer.
- The phone uses short HTTPS requests:
- to hang up: the server sends BYE to the provider;
- to send a new SDP offer when it puts the call on hold or changes network: the server sends re-INVITE to the provider and returns the answer;
- to answer a new offer from the provider;
- to send DTMF as SIP INFO where needed. In-band DTMF (RFC 4733) travels with the audio anyway.
- The server handles session refreshes towards the provider.
- If the phone's status request stays away for more than 30 seconds, the server ends the call, so no call is left hanging.
- Requests that would extend the call, such as REFER, are refused.
When the call is not answered
- Decline: the app sends an HTTPS request, and the server rejects the held INVITE with 603 Decline.
- The caller hangs up, another device answers, or the time runs out: the server sends each device a second, silent message without any identifier. The phone stops ringing and does not contact the server.
- The user answers too late: the server replies that nothing is held for the account, and the phone closes the call screen.
- No answer: the server keeps the call as long as the provider keeps it ringing. The provider's own no-answer handling ends it with CANCEL, and its voicemail or forwarding takes over. The server sets no deadline of its own; a protection timer of a few minutes only cleans up and then rejects the call with 480.
- The push service reports an invalid device token: the server rejects the call at once, so the caller is not left waiting.
Several devices
All active devices of a subscription are told about the call. The first device on which the user answers gets the call. The other devices receive the silent message and stop ringing. A subscription can be active on a limited number of devices at the same time.
At home without a push service
On Wi-Fi networks the user selects, for example the home network, the phone can do without the push service:
- A network extension of the operating system (on iOS: Local Push Connectivity) keeps a persistent, encrypted connection to the server while the phone is on that network.
- The server announces the call over this connection instead of the push service, again with display data only.
- Everything else stays the same: the phone rings, and after the user answers it fetches the call over HTTPS.
- For each call and device exactly one channel is used: the persistent connection if it is up at that moment, otherwise the push service. Never both.
Inside the app
The app's existing SIP stack can stay unchanged. A small adapter inside the app presents the fetched call to it as a normal incoming SIP call over the loopback interface. The adapter turns the stack's answer, hang-up and new offers into the HTTPS requests above. Towards the server, only HTTPS leaves the phone.
Security and privacy
- The announcement is encrypted end to end with a key that exists only on the device.
- Every HTTPS request is authenticated with the device key. The server name comes from setup, never from a message.
- Signalling to the provider uses TLS where the provider supports it, audio uses SRTP where the provider supports it.
- The server never handles audio. It keeps everything in memory only and writes no call lists and no logs with phone numbers. Planned restarts hand the state over from memory to memory, so nothing is lost and nothing is written to disk. After an unexpected restart all setups are gone, and the app notices this and sets itself up again automatically.
Foreground and background are the same
The app is never registered at the server. A call reaches an open app on exactly the same path as a sleeping one: announcement, ringing, answer, fetch. There is one path to build, test and keep working.
Why this is reliable
- There is no connection between phone and server that has to be kept alive while idle, and none that can silently break. The optional home connection is kept by the operating system.
- The phone holds no registration on the server that could expire or go stale.
- Every incoming call takes the same path, whatever state the app is in, and with every provider: the provider only sees an ordinary answered call.
- The announcement carries nothing that the call depends on. Even if its content were lost, the user could still answer.
- If anything fails before the answer, the call ends cleanly at the provider, with voicemail or forwarding, instead of hanging.
Sequence: incoming call through the push service
Sequence: at home over the persistent connection
Variants
The design is part of this publication in all of the following variants, alone or in any combination.
- When the phone fetches:
- when the user answers (the default);
- right after waking, where the phone's request stays in an early state and a later request signals the answer;
- right after waking with a completed exchange, while the server keeps the provider side unanswered until the phone signals the answer;
- with the server holding the call for as long as the provider keeps it ringing (the default), or only up to a fixed time set by the server.
- How the phone fetches:
- HTTPS requests (the default), or WebSocket messages;
- the phone's own SIP INVITE over TLS, TCP, UDP or WebSocket. The server answers it and joins it to the held call as a signalling-only back-to-back user agent: with a delayed offer from the phone, the server's 200 OK carries the provider's offer and the phone's ACK carries the answer;
- WebRTC signalling;
- with or without an adapter inside the app that turns the app's SIP stack's messages into these requests.
- How the server finds the call:
- through the authenticated account or device (the default);
- through a one-time value carried in the announcement, or derived from the call identifiers with a secret server key;
- one value shared by all devices, or one per device.
- What the announcement carries:
- display data only (the default);
- additionally the session description, a reference to the call or the server address;
- encrypted with HPKE or another authenticated encryption scheme, or only an opaque reference.
- Channel for the announcement:
- the operating system's push service (the default), for example an Apple VoIP push (PushKit), an Apple alert or background push, Firebase Cloud Messaging with high priority, or any other operating-system push service;
- a persistent connection kept by an operating-system network extension on networks the user selects, for example the home Wi-Fi, a private mobile network or a mission-critical mobile service;
- exactly one channel per call and device, chosen by whether the persistent connection is up at that moment.
- Where the offer travels:
- fetched over HTTPS when the user answers (the default);
- in the encrypted announcement;
- the phone makes the offer, and the provider's answer reaches the phone over HTTPS;
- renegotiation at any time during the call.
- Status channel:
- none before the answer, with cancellation by a second silent message (the default);
- HTTPS long-poll, WebSocket, Server-Sent Events or repeated polling, before or after the answer;
- SIP SUBSCRIBE and NOTIFY.
- How calls reach the server:
- the server registers the SIP account with its own contact (the default);
- or the provider forwards calls to a SIP address of the server without registration (SIP trunk or call forwarding);
- one registration per account for several devices, with one announcement per device.
- Several servers:
- the phone uses a server name assigned at setup, with failover through DNS (the default);
- or the announcement or a lookup names the server that holds the call;
- or any server can answer through shared in-memory state.
- Device limit: a fixed number of device slots per subscription. A new device takes the slot of a device the user chooses, or of the least recently active device.
- Callback instead of fetching: the server ends the incoming call at once (for example with 480 or 486), so no call is left waiting, and sends an ordinary notification such as "X called – call back". If the user taps it, the phone places a new outgoing call to the caller directly through the provider, with the provider's normal encryption. The server is not contacted. This mode can be switched on per country or per device, for example as a fallback.
- Other platforms: the same design works on Android with Firebase Cloud Messaging and a ConnectionService instead of CallKit, and on desktop systems.
- Beyond phone calls: the same pattern works for video calls, push-to-talk, conferences and any other session request that a sleeping device has to accept.
Text licence: CC BY 4.0.
Changes
- 27 October 2026: Author line added. The technical content is unchanged.
- 30 October 2026: New header design. The text is unchanged.