Skip to content

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/tokens
App ◀──── ct_ token ──── Your backend
App ──(Bearer ct_...)─────────────────────────────▶ https://api.navapatra.com/v1/kundali

Your 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.

POST/v1/tokens0 creditsReference

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 });
}

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:

  • ttlSeconds is 60 to 900; the default and the maximum is 900.
  • creditCap limits the token’s whole lifetime. Once reached, calls return 402 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.

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".

api.ts
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' });

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 kundali and maybe charts.svg, not matching.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.

The same pattern works in the browser:

  • Widgets take a client token directly. Put it in the element’s publishable-key attribute 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 with client.tokens.create(...). See TypeScript SDK.