Errors
Every failure has the same shape, and it always names a code and links back to this page:
{
"status": "error",
"error": {
"code": "validation_error",
"message": "give either timezone or utc_offset, not both",
"field": "birth.utc_offset",
"docs": "https://kaaljyoti.com/api/docs/errors#validation_error"
}
}Branch on code, never on message. Messages are written for people and get clearer over time; codes are the contract and do not change. field appears when the API knows which part of the body was wrong, as a dotted path.
None of these costs a credit. The credits are reserved before the calculation and given back the moment a request is refused, so only answers are charged for — see Credits per API. Refusals do appear in your usage as errors, because your error rate is worth seeing.
Every response, successful or not, carries X-KJ-Request-Id. Quote it when you write to us: it lets us find the exact request without you having to tell us what was in it — we do not log bodies.
At a glance
| Code | HTTP | Means |
|---|---|---|
validation_error | 400 | The request body is not one this endpoint accepts. |
invalid_key | 401 | No key, or not a key we know. |
key_revoked | 401 | That key existed and was revoked. |
quota_exceeded | 402 | This period's credits are used up, or not enough are left for this request. |
pdf_quota_exceeded | 402 | This period's PDFs are used up. |
forbidden_origin | 403 | A publishable key, used from an origin it does not list. |
plan_required | 403 | This endpoint is not open to this key, or it is a PDF on the Free plan. |
not_found | 404 | No such path. |
not_computable | 422 | The request was valid, but no answer exists for it. |
rate_limited | 429 | Too fast. |
engine_error | 500 | Our fault. |
service_disabled | 503 | The API is deliberately turned off. |
validation_error — HTTP 400
The request body is not one this endpoint accepts.
A missing or misspelled field (bodies are strict, so an unknown key is an error rather than a silent default), a date that is not a real calendar date, a year outside 1800–2400, a latitude beyond ±89.9°, both timezone and utc_offset given at once, a window longer than 366 days or with to before from, an unknown ayanamsa slug, or a body over 8 KB. The response names the offending path in error.field.
Costs no credits.
invalid_key — HTTP 401
No key, or not a key we know.
The same answer is given for a missing key and a wrong one, on purpose. It is also what you get when a secret key (kj_live_… or kj_test_…) is sent in the query string: only publishable keys may travel in a URL.
Costs no credits.
key_revoked — HTTP 401
That key existed and was revoked.
Create a new one in the dashboard. A revoked key can keep answering for up to a minute — the gateway caches a key record for sixty seconds — so revoke before you finish a rotation, not after.
Costs no credits.
quota_exceeded — HTTP 402
This period's credits are used up, or not enough are left for this request.
The account has spent its monthly credits — the plan, anything added to the account, and any credit packs it can draw on — or has fewer left than this request costs; the message says which, and a cheaper request may still go through. It clears when the month turns, when you buy a credit pack, or immediately on an upgrade. Nothing is throttled silently and nothing extra is billed without you choosing it. A publishable key has two more causes: it has reached its own daily credit cap (the message says so; it clears at 00:00 UTC, and the owner can change the cap on the Sites page), or the period's credits are gone and the key is not allowed to draw on credit packs.
Costs no credits.
pdf_quota_exceeded — HTTP 402
This period's PDFs are used up.
The account has made every PDF its plan includes this period (POST /v1/pdf/*: Starter 50, Growth 200, Scale 500, Enterprise 2,500 or as the contract says). It clears on the account's billing day; a credit pack does not raise it. The same PDF asked for again within 24 hours still comes back — a cached PDF uses no allowance — and every other route keeps working: this is the PDF count, not the credits.
Costs no credits.
forbidden_origin — HTTP 403
A publishable key, used from an origin it does not list.
Add the origin to the key in the dashboard. The match is exact — scheme and port are part of an origin — and * is not a wildcard. This is also the code the edge returns for a request that did not come through it.
Costs no credits.
plan_required — HTTP 403
This endpoint is not open to this key, or it is a PDF on the Free plan.
No plan gates an API, with one exception: a PDF (/v1/pdf/*) on the Free plan — every paid plan has them. The other causes are per-request branding on a PDF below Enterprise, and a publishable key asking for something publishable keys never get: /v1/transit/scan, /v1/match/batch or a PDF on any plan, or embed_font: true on a chart. The key's kind is checked before the plan.
Costs no credits.
not_found — HTTP 404
No such path.
Check the version prefix — every endpoint lives under /v1 — and the reference list name on GET /v1/reference/{list}.
Costs no credits.
not_computable — HTTP 422
The request was valid, but no answer exists for it.
The arithmetic has no result for these inputs rather than a wrong one: a sunrise that does not occur at that latitude on that day is the usual case. The message says what could not be computed. Retrying will not help; changing the place or the date will.
Costs no credits.
rate_limited — HTTP 429
Too fast.
Your key's token bucket is empty. Retry-After says how many seconds to wait, and X-RateLimit-Remaining and X-RateLimit-Reset let you pace yourself before it happens. Heavy endpoints have their own ceiling of 10 a minute on top of the plan's rate. A publishable key is also limited per site, at our edge, and more tightly on the routes that cost five credits or more; that refusal says so, carries Retry-After: 60 and has no X-RateLimit-* headers.
Costs no credits.
engine_error — HTTP 500
Our fault.
Something failed inside the calculation that should not have. It is reported to us automatically with the request id; send us X-KJ-Request-Id from the response and we can find the exact request without you telling us what was in it.
Costs no credits.
service_disabled — HTTP 503
The API is deliberately turned off.
The kill switch is on — an incident, or maintenance. GET /v1/health keeps answering throughout, so it is the thing to poll. This is never a way of shedding load from one account.
Costs no credits.
Which of these are worth retrying
rate_limited— yes, afterRetry-After. With backoff, not in a tight loop.engine_error— once, then tell us. Twice in a row is not a transient.service_disabled— pollGET /v1/healthrather than the endpoint you wanted.- Everything else — no. The same request will be refused the same way; fix the request, the key or the plan.