Skip to content

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.md in 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

  1. Design/ship a mobile feature → define/confirm the shared endpoint contract here first.
  2. 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.
  3. Flip the parity matrix cell here and cross-link the handoff.
  4. When the second platform ships, mark ✅ and reconcile behaviour (numbers/timings must match — verify side-by-side).