Mobile apps and the backend proxy
Anything inside an app bundle can be extracted, so an sk_ key shipped in a React Native or Flutter app is a leaked key. The fix is a small endpoint on your own backend that trades your user’s session for a client token: a credential that lasts at most 15 minutes, can call only the endpoints you list and can carry its own credit cap.
App ──(your session)──▶ Your backend ──(sk_live_)──▶ POST /v1/tokensApp ◀──── ct_ token ──── Your backendApp ──(Bearer ct_...)─────────────────────────────▶ https://api.navapatra.com/v1/kundaliYour backend decides who gets a token, and the API enforces the token’s limits. Birth data goes straight from the app to the API and never passes through your server.
1. Issue tokens from your backend
Section titled “1. Issue tokens from your backend”Only sk_live_ keys can create tokens. Authenticate the user with your own session first, then ask for the narrowest scopes the app needs.
// A web-standard handler: works in Hono, Next.js route handlers, Bun, Deno and Workers.export async function issueToken(req: Request): Promise<Response> { const user = await getSessionUser(req); // your own auth check if (!user) return new Response('Unauthorized', { status: 401 });
const res = await fetch('https://api.navapatra.com/v1/tokens', { method: 'POST', headers: { Authorization: `Bearer ${process.env.NAVAPATRA_API_KEY}`, // sk_live_..., server only 'Content-Type': 'application/json', }, body: JSON.stringify({ scopes: ['kundali', 'charts.svg', 'places.search'], ttlSeconds: 900, creditCap: 10, }), }); if (!res.ok) return new Response('Token service unavailable', { status: 502 });
const { token, expiresAt } = await res.json(); return Response.json({ token, expiresAt });}# Flask (pip install flask requests)import osimport requestsfrom flask import Flask, abort, jsonify
app = Flask(__name__)
@app.post("/api/astro-token")def issue_token(): user = get_session_user() # your own auth check if user is None: abort(401)
res = requests.post( "https://api.navapatra.com/v1/tokens", headers={"Authorization": f"Bearer {os.environ['NAVAPATRA_API_KEY']}"}, json={ "scopes": ["kundali", "charts.svg", "places.search"], "ttlSeconds": 900, "creditCap": 10, }, timeout=10, ) if not res.ok: abort(502) body = res.json() return jsonify(token=body["token"], expiresAt=body["expiresAt"])<?php// Any PHP framework; Guzzle for the outbound call (composer require guzzlehttp/guzzle).require 'vendor/autoload.php';
$user = get_session_user(); // your own auth checkif ($user === null) { http_response_code(401); exit;}
$client = new GuzzleHttp\Client(['timeout' => 10]);$res = $client->post('https://api.navapatra.com/v1/tokens', [ 'headers' => ['Authorization' => 'Bearer ' . getenv('NAVAPATRA_API_KEY')], 'json' => [ 'scopes' => ['kundali', 'charts.svg', 'places.search'], 'ttlSeconds' => 900, 'creditCap' => 10, ],]);
$body = json_decode((string) $res->getBody(), true);header('Content-Type: application/json');echo json_encode(['token' => $body['token'], 'expiresAt' => $body['expiresAt']]);The response from /v1/tokens:
{ "token": "ct_eyJ2IjoxLCJqdGkiOiJkOTcxYjg3NS01MGZlLTRk...", "expiresAt": "2026-10-10T15:57:53Z", "scopes": ["kundali", "charts.svg", "places.search"], "creditCap": 10}Rules worth knowing:
ttlSecondsis 60 to 900; the default and the maximum is 900.creditCaplimits the token’s whole lifetime. Once reached, calls return402 credit_cap_reached.- Credits come from your organization’s balance, like any other call.
- The token uses the rate limit of the key that created it, counted per token, and the API version that key was pinned to when the token was issued.
- Creating a token costs no credits, but it does count toward the issuing key’s rate limit.
2. Use the token in the app
Section titled “2. Use the token in the app”Keep the token in memory, reuse it until shortly before expiresAt, and fetch a fresh one when the API answers 401 with code: "token_expired".
type Cached = { token: string; expiresAt: number };let cached: Cached | null = null;
async function clientToken(force = false): Promise<string> { if (!force && cached && cached.expiresAt - Date.now() > 60_000) return cached.token; const res = await fetch('https://your-backend.example.com/api/astro-token', { method: 'POST', credentials: 'include', // or your own session header }); if (!res.ok) throw new Error(`token: ${res.status}`); const body = await res.json(); cached = { token: body.token, expiresAt: Date.parse(body.expiresAt) }; return cached.token;}
export async function callApi(path: string, body: unknown, retried = false): Promise<any> { const res = await fetch(`https://api.navapatra.com/v1${path}`, { method: 'POST', headers: { Authorization: `Bearer ${await clientToken(retried)}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); const json = await res.json(); if (res.status === 401 && json.code === 'token_expired' && !retried) return callApi(path, body, true); if (!res.ok) throw new Error(`${json.code}: ${json.detail}`); return json;}
// const kundali = await callApi('/kundali', { birth: { ... }, lang: 'ne' });// api.dart (dependencies: http)import 'dart:convert';import 'package:http/http.dart' as http;
class AstroApi { String? _token; DateTime _expiresAt = DateTime.fromMillisecondsSinceEpoch(0);
Future<String> _clientToken({bool force = false}) async { final fresh = _expiresAt.difference(DateTime.now()) > const Duration(minutes: 1); if (!force && _token != null && fresh) return _token!; final res = await http.post(Uri.parse('https://your-backend.example.com/api/astro-token')); if (res.statusCode != 200) throw Exception('token: ${res.statusCode}'); final body = jsonDecode(res.body) as Map<String, dynamic>; _token = body['token'] as String; _expiresAt = DateTime.parse(body['expiresAt'] as String); return _token!; }
Future<Map<String, dynamic>> post(String path, Map<String, dynamic> body, {bool retried = false}) async { final res = await http.post( Uri.parse('https://api.navapatra.com/v1$path'), headers: { 'Authorization': 'Bearer ${await _clientToken(force: retried)}', 'Content-Type': 'application/json', }, body: jsonEncode(body), ); final json = jsonDecode(utf8.decode(res.bodyBytes)) as Map<String, dynamic>; if (res.statusCode == 401 && json['code'] == 'token_expired' && !retried) { return post(path, body, retried: true); } if (res.statusCode >= 400) throw Exception('${json['code']}: ${json['detail']}'); return json; }}Protect the token endpoint
Section titled “Protect the token endpoint”Your token endpoint spends your credits, so treat it like any other paid action:
- Authenticate every request with your own session. Never issue tokens to anonymous callers unless that is a product decision.
- Scope narrowly. A chart screen needs
kundaliand maybecharts.svg, notmatching.ashtakoota.batch. - Cap credits per token with
creditCap, and rate-limit the endpoint per user on your side. - Keep the
sk_live_key in server configuration, never in source control. Roll it in the dashboard if it leaks.
Web apps, widgets and the SDK
Section titled “Web apps, widgets and the SDK”The same pattern works in the browser:
- Widgets take a client token directly. Put it in the element’s
publishable-keyattribute and the widget calls the API itself; when the token expires, set a fresh one and it refetches. See Widgets. - The TypeScript SDK accepts a token as its
apiKey(new Navapatra({ apiKey: token })), and your server can mint tokens withclient.tokens.create(...). See TypeScript SDK.