Authentication
Every request carries a key. There are three kinds and the difference is not cosmetic — it decides where the key may travel and what it may call.
The three kinds#
| Prefix | Kind | Where it goes | What it may call |
|---|---|---|---|
kj_live_… | Live | Your server. Authorization: Bearer only. | Everything your plan includes. |
kj_test_… | Test | Your server, staging, CI. Authorization: Bearer only. | The same as live. Separate key, same quota — easy to revoke on its own. |
kj_pub_… | Publishable | A web page. It is checked against the browser's Origin and capped per day (below). May travel as ?key=…. | Every widget route; not batch matching, the transit scan, PDFs or embed_font on a chart. |
A key is its prefix plus 43 random base62 characters — about 256 bits. We store only its SHA-256 and the first twelve characters, so the dashboard can tell you which key is which and a stolen database dump cannot call the API. You see a key once, when you create it. Lose it and you make a new one.
Sending a key#
On a server, always the header:
curl -s https://api.kaaljyoti.com/v1/kundli \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $KAALJYOTI_API_KEY" \
-d '{"birth": {"datetime": "1990-05-14T10:30:00", "timezone": "Asia/Kolkata",
"latitude": 28.6139, "longitude": 77.2090}}'
From a browser, the publishable key may go in the query string, because a browser cannot keep a secret anyway:
await fetch(`https://api.kaaljyoti.com/v1/panchang?key=${PUBLISHABLE_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ latitude: 28.6139, longitude: 77.209, timezone: 'Asia/Kolkata' }),
});
A secret key sent as ?key=… is refused outright, even if it is otherwise
valid. A URL ends up in server logs, browser history, proxy caches and
referrer headers; a live key that has been in one is a live key that has
leaked. The API would rather fail your request than let that happen quietly.
Origins, and what they do and do not protect#
A publishable key is only accepted from an origin it lists. You give the list
when you create the key — https://example.com, https://app.example.com —
and the browser sends Origin on every request without being asked. An
origin that is not on the list gets 403 forbidden_origin.
The match is exact, and * is not a wildcard: a publishable key that worked
from anywhere would just be a secret published on purpose. Origins are matched
case-insensitively, and scheme and port are part of the origin, so
http://localhost:3000 and https://localhost:3000 are two different entries.
The origin check is a browser control, not authentication. A browser
cannot lie about Origin, so another website cannot use your key in its
pages. But a publishable key is in your page's source, and anyone who copies
it into a script can send it with whatever Origin header they like. Treat a
publishable key as public, and rely on its limits for what it can cost.
What limits the cost of a publishable key#
- A daily credit cap per key. By default a tenth of your plan's monthly
credits per UTC day; you can set your own number for each key on the
dashboard's Sites page. Over it the key gets
402 quota_exceeded— the message says it is the key's daily cap — until 00:00 UTC, and we e-mail you once that day. Your other keys and the rest of your credits are untouched. - No credit packs unless you allow them for that key on the Sites page. A publishable key spends the period's credits only, so a copied key cannot reach credits you paid for in advance.
- A per-minute limit per site, counted at our edge in front of every server, and a tighter one on the routes that cost five credits or more a call (readings and the kundli report, the horoscope, the month-long panchang and ephemeris, transit events, kundli events and sade-sati).
- An e-mail on unusual use: a key whose day is five times its average over the week before, and at least 1,000 credits.
- It cannot call the batch or the scan (
/v1/match/batch,/v1/transit/scan) or the PDFs, and cannot ask forembed_font: trueon a chart.
If you see traffic you do not recognise, revoke the key and create a new one with the same origins. The caps limit the damage; they do not stop someone spending the key's daily cap every day until you rotate it.
Rate limits are per site, not only per key#
A publishable key is on a page, and every visitor of that page is using it. So
a publishable key is measured twice: each Origin it is used from gets its
own per-minute allowance, and the key as a whole still cannot exceed your
plan's req_per_min.
One busy site can therefore no longer spend the whole key's minute and leave
your other sites with nothing — but the key-wide rate is still the ceiling, and
it is the one your plan quotes. Over either, the answer is the same
429 rate_limited with a Retry-After; the message says which of the two
refused. A publishable key that arrives without an Origin at all — a curl,
a native app — is refused as forbidden_origin before either bucket is
consulted. The per-site count is kept at our edge as well as on each server,
so it does not grow as the API scales out.
How many keys#
The free plan allows one key of each kind at a time — one live, one test, one publishable. Revoke one to make another. Paid plans allow up to fifty of each, which is more than anyone needs and exists only so a loop cannot fill a table.
Rotating and revoking#
Revoke a key in the dashboard and it stops working — within about a minute. The gateway caches a key's record for sixty seconds so that a customer looping on one key is not a database query per request; the trade is that a revoked key may answer for up to a minute after you revoke it. Plan a rotation as: create the new key, deploy it, then revoke the old one.
If a key has leaked, revoke first and deploy second. A minute of overlap is better than a day of exposure.
What the API never sees#
The key is the only credential. There is no account password, no OAuth dance
and no session: the API does not know who you are beyond which key you used
and which account it belongs to. And what you send it — a birth, a place, a
date — is used to compute an answer and never stored: bodies are not logged, and the response cache is
keyed on a hash. (The two GET lookups, /v1/timezone and /v1/places, take
their input in the URL, and URLs are in the access logs for up to 30 days.)
Errors you may see#
| Code | Status | Means |
|---|---|---|
invalid_key | 401 | No key, a key we do not know, or a secret key in the query string. |
key_revoked | 401 | The key existed and was revoked. |
forbidden_origin | 403 | A publishable key from an origin it does not list. |
plan_required | 403 | The key's kind does not cover this endpoint, or a PDF on Free. |
Each of them is described on the errors page, and none of them costs a credit.