iOS ↔ Android — Shared Contract & Parity¶
Single source of truth for the two mobile apps. iOS (Mobile App/astradial-astracall, AstraCall/own engine) and Android (Mobile App/astradial-astracall-android, own engine) are two codebases that must behave identically against the same backend contracts. This page is where they correlate: shared API shapes, the feature parity matrix, and the rule for keeping them in sync.
Maintenance rule. When either app ships (or changes) a feature that touches a shared endpoint or a cross-platform behaviour, update this page in the same session — the contract table and the parity matrix. When one platform lands a feature first, add/refresh its handoff doc (e.g.
VOIP-PUSH-ON-OPEN-HANDOFF.mdin the android repo) and flip the matrix cell. Docs of tested behaviour still follow the astradial-platform Rule 6 (write after staging/prod confirms); the contract shape here is defined by merged API code and is safe to document immediately.
The apps do NOT share code — only these contracts. Both do trivial derived math (toggles, labels) locally; all heavy aggregation is server-side.
Shared backend API contracts (mobile)¶
Bundle id per platform (fixed, not free-text): iOS com.astradial.astracall, Android com.astradial.phone — the editor Mobile Push dialog derives it from platform.
| Purpose | Endpoint | Notes for both apps |
|---|---|---|
| Login → platform JWT | POST /api/v1/auth/user-login {firebase_token} → {token, user} | Firebase ID token → org-scoped JWT (24 h). All other calls use Authorization: Bearer <jwt>. |
| Org SIP users (picker) | GET /api/v1/users | Filter to sip_port == 'wss' — only Mobile-App (WSS) lines can register; never attempt a non-wss line. See Mobile App Access. |
| SIP registration | wss://<sip-host>:8089/ws, subprotocol sip, digest auth | Register over WSS (RFC 7118). Prod devsip.astradial.com, staging stagesip.astradial.com. The port 8089 is open worldwide (not India-geo-filtered like 5060/5080). |
| Register VoIP/FCM push | POST /api/v1/users/voip-push-token {asterisk_endpoint, token, environment, bundle_id, platform} | platform:"ios" → APNs, "android" → FCM. Keyed by asterisk_endpoint. |
| Push already registered? | GET /api/v1/users/voip-push-status?asterisk_endpoint=&token= → {registered, token_matches, environment, platform, updated_at} | Skip a redundant POST on open; token_matches:false → re-register. Never returns the raw token. 404 / error → treat as "not registered". |
| Self-deregister push | DELETE /api/v1/users/voip-push-token?asterisk_endpoint= | Settings → Revoke Notification Access. Clears the token; wake CURL drops on next regen. |
| Push delivery ack | POST /api/v1/users/voip-push-ack {push_id} | Fire FIRST in the push handler (gateway alive-proof), no auth (push_id is the capability). |
| Analytics (Pulse) | GET /api/v1/analytics/pulse → see contract below | Pre-aggregated; fetch + render, do NOT pull raw /calls and aggregate locally. |
| Tickets (recovery metrics) | GET /api/v1/tickets | Recovery rate / unrecovered count derived client-side from tickets. |
| Raw call list (Recents) | GET /api/v1/calls?date_from=&date_to=&limit=&offset= (limit cap 200) | Recents/call-log only. NOT for analytics (use /analytics/pulse). |
Feature parity matrix¶
| Feature | Shared contract | iOS | Android | Handoff / notes |
|---|---|---|---|---|
| Login (Firebase → JWT) + server picker | auth/user-login, GET /users | ✅ | ✅ | Server dropdown (prod/staging/custom) at login. |
| SIP register over WSS + 2-way call | wss://…:8089/ws | ✅ | ✅ | alaw only (Tata trunk); no FEC → cellular choppy. |
| wss-only line gating (picker) | GET /users filter sip_port=='wss' | ✅ | spec'd | Non-wss line can't register → "keeps loading" bug. |
| VoIP push register on app open (strict, permission-gated) | voip-push-status GET + voip-push-token POST | ✅ | TODO | Android handoff: VOIP-PUSH-ON-OPEN-HANDOFF.md. iOS PushKitManager.ensureRegisteredOnOpen(). |
| Settings → Revoke Notification Access | DELETE voip-push-token + open OS settings | ✅ | TODO | Same handoff. |
| Caller extension/ID in the in-call UI | (client-side, from SIP From / dialed number) | ✅ | TODO | Android handoff: CALLER-ID-IN-CALL-UI-HANDOFF.md. |
| Analytics Pulse (PSTN-only, server-aggregated) | GET /analytics/pulse (+ GET /tickets) | in progress | stub | Endpoint live on staging (PR #529); iOS client + Android Pulse build against the contract below. |
| VoIP push wake (killed app → CallKit/ConnectionService) | push payload {caller, push-id} + voip-push-ack | ✅ | ✅ | See VoIP Push Wake. |
Legend: ✅ shipped · TODO speced, not built · stub = placeholder screen.
Analytics: GET /api/v1/analytics/pulse¶
Pre-aggregated, PSTN-only (internal ext-to-ext excluded), sessions deduped by linkedid in SQL, bucketed in the DB/business timezone (IST). Clients fetch once + render; derive toggles (7D/30D), answer-rate, week-over-week, last-7 sums, and insight strings locally (both apps must implement the same derived math — port iOS PulseSnapshot's derived section). Recovery metrics come from a separate GET /api/v1/tickets.
{
"generated_at": "2026-07-11T…",
"tz": "Asia/Kolkata",
"today": { "date": "YYYY-MM-DD", "inbound": 42, "answered": 39, "missed": 3 },
"hourly": {
"today": [ { "hour": 0, "total": 0, "missed": 0 }, "… hours 0..current" ],
"yesterday": [ { "hour": 0, "total": 2, "missed": 1 }, "… full 24" ]
},
"daily": [ { "date": "YYYY-MM-DD", "inbound": 31, "missed": 4, "outbound": 7, "new_callers": 3 }, "… 30, oldest→today" ]
}
Semantics both apps must honour: - PSTN-only everywhere (internal excluded). Matches /api/v1/calls' filters (incl. spam_blocked / dst!='s' carve-outs) so numbers reconcile with the raw call list. - answered = inbound − missed for today (missed = inbound with no answered leg). - Session bucket = arrival time (MIN(calldate)) — a minor divergence from the raw list's answered-leg time for retried calls near an hour/day boundary. Accepted; render as-is. - new_callers = first-seen caller number (inbound) within the 30-day window (window-relative). - Zero-filled server-side (30 daily slots, hourly today 0..now / yesterday 0..23) — clients never bucket, just map arrays to charts by the returned date/hour strings (no local re-bucketing / tz math).
Web dashboard uses a separate endpoint GET /api/v1/calls/stats (keeps internal breakdown + pickup-time series the mobile Pulse doesn't need); both reuse api/src/lib/callDirection.js predicates. Do not point mobile at /calls/stats or web at /analytics/pulse.
Keeping the two apps in sync — workflow¶
- Design/ship a mobile feature → define/confirm the shared endpoint contract here first.
- Implement on the lead platform; write/refresh the other platform's handoff doc in that app's repo (root
*-HANDOFF.md), with the exact contract + test cases. - Flip the parity matrix cell here and cross-link the handoff.
- When the second platform ships, mark ✅ and reconcile behaviour (numbers/timings must match — verify side-by-side).
Related¶
- Mobile App Access (WSS softphone line) — sip_port='wss', the picker gate, push-on-open + revoke.
- MD Analytics (in-app dashboard) — the Pulse UI surface.
- VoIP Push Wake — killed-app wake pipeline.
- Android — Status · iOS App.