Authentication
Almost all AstroWay API requests authenticate via the X-Api-Key header. Exceptions: the 14 /v1/reference/* endpoints (signs, planets, houses, aspects, …) are public - no key required, capped at 30 requests / hour per IP. When the calls come from your server, send the X-AstroWay-Site-URL header and the cap becomes 300 requests / hour per site: details.
No other methods - no OAuth, JWT, or session cookies at the API level. Reference dictionaries /v1/reference/* are public and free (canonical lookup tables).
Key format
Section titled “Key format”aw_live_aB3xY7pQ9rN2mK4jH8vC5tL6wZ1fD0eRaw_test_aB3xY7pQ9rN2mK4jH8vC5tL6wZ1fD0eRaw_live_: production, deducts credits from balanceaw_test_: sandbox, returns stub responses (or minimal real calculations), does not consume credits, tracked separately in dashboard- After the prefix: 32 base62 characters, cryptographically random
- Key is shown in plain text only once at creation. Dashboard shows only first and last 4 characters afterward.
Request header
Section titled “Request header”POST /v1/chart HTTP/1.1Host: api.astroway.infoX-Api-Key: aw_live_aB3xY7pQ9rN2mK4jH8vC5tL6wZ1fD0eRContent-Type: application/jsonPostman collection
Section titled “Postman collection”If Postman is more convenient than curl/SDK - there’s a ready-made collection with all 760 endpoints, grouped into 55 folders by OpenAPI tag, with example bodies pre-filled. Auth is configured at collection level: paste your key into the apiKey variable once and every request picks it up.
- Published docs: documenter.getpostman.com/view/54689779/2sBXqNmy3n
- Public workspace: postman.com/astroway-info/astroway-api
- Download JSON: astroway-api.json (≈ 5 MB) - import directly into Postman or Insomnia
The collection is auto-generated from the OpenAPI spec - refreshed on every deploy.
Creating and managing keys
Section titled “Creating and managing keys”Via dashboard:
- api.astroway.info/dashboard/keys
- Up to 10 active keys per account at once
- Each key has a
name(e.g.production-backend,ci-tests,local-dev)
Via API (for self-service integrations):
curl -X POST https://api.astroway.info/v1/keys \ -H "X-Api-Key: aw_live_master_key" \ -H "Content-Type: application/json" \ -d '{"name": "ci-tests", "mode": "test"}'// Equivalent via @astroway/sdk: aw.client.POST('/keys', { body: { name, mode } })const r = await fetch('https://api.astroway.info/v1/keys', { method: 'POST', headers: { 'X-Api-Key': process.env.ASTROWAY_MASTER_KEY!, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'ci-tests', mode: 'test' }),});
const { data: created } = await r.json();console.log(created.api_key); // shown once - store it now# Equivalent via `astroway` (PyPI): aw.post('/keys', body={'name': ..., 'mode': ...})import os, requests
r = requests.post( 'https://api.astroway.info/v1/keys', headers={'X-Api-Key': os.environ['ASTROWAY_MASTER_KEY']}, json={'name': 'ci-tests', 'mode': 'test'},)created = r.json()['data']print(created['api_key']) # shown once - store it now<?phpuse GuzzleHttp\Client;
$aw = new Client(['base_uri' => 'https://api.astroway.info/v1/']);$r = $aw->post('keys', [ 'headers' => ['X-Api-Key' => getenv('ASTROWAY_MASTER_KEY')], 'json' => ['name' => 'ci-tests', 'mode' => 'test'],]);
$created = json_decode($r->getBody(), true)['data'];echo $created['api_key']; // shown once - store it nowKeys for the browser
Section titled “Keys for the browser”A key is server-only by default: a request carrying an Origin or a
Referer header gets 403 ORIGIN_FORBIDDEN. That is deliberate, and it is what
catches a key left in a frontend by accident.
When a widget or a page has to call the API from the browser, create the key
with an origin policy from the start. The origin_restriction field on
POST /v1/keys:
type | What is allowed | When to use it |
|---|---|---|
server-only (default) | only requests with no Origin and no Referer | backend, cron, CI |
public | only the listed hosts, and the request must carry an origin | a widget or page on your own domain, a key in page source |
allowlist | the same as public | older spelling: still accepted, no longer listed in openapi.json |
curl -X POST https://api.astroway.info/v1/keys \ -H "X-Api-Key: aw_live_master_key" \ -H "Content-Type: application/json" \ -d '{ "name": "site-widget", "origin_restriction": { "type": "public", "allowed_origins": ["example.com", "*.example.com"] } }'Matching rules, checked against production:
- Up to 20 hosts per key. The scheme (
https://) and the port are ignored, and case does not matter. - A leading
www.is stripped from the incoming host, soexample.comalready coverswww.example.com. There is no need to add thewww.form separately; it changes nothing. *.example.comcovers subdomains (app.example.com) but notexample.comitself. List the apex on its own line, as in the example above.- A host that is not on the list gets
403 ORIGIN_NOT_ALLOWED, with the host itself inincoming_originso you can see which line is missing. Referercounts the same asOrigin: browsers send it whereOriginis absent.
Editing the list on a key that already exists
Section titled “Editing the list on a key that already exists”PATCH /v1/keys/{id} rewrites the list without touching the key itself. It takes the same access as POST /v1/keys above; in the dashboard it is “Edit origins” in the key’s menu.
curl -X PATCH https://api.astroway.info/v1/keys/433 \ -H "X-Api-Key: aw_live_master_key" \ -H "Content-Type: application/json" \ -d '{ "origin_restriction": { "type": "public", "allowed_origins": ["example.com", "*.example.com"] } }'The new list applies from the very next request: the cached key is dropped as the row is written, so there is no minute to wait out. The key string does not change, so nothing has to be reissued in your page or your backend. POST /v1/keys/{id}/rotate keeps the list, and the old key value stops working at once.
The class itself is not editable, because it is spelled in the key:
| Attempt | Answer |
|---|---|
turn a pk_ key into server-only | 400: the class is part of the key, revoke it and create a new one |
give a secret aw_ key a list | 400: a browser key is a different class, created as public |
leave a pk_ key with an empty list | 400: a key with no list would refuse every request |
A credit budget for one key
Section titled “A credit budget for one key”Credits pool across the account, so any one key can spend all of them. That is fine for a backend key and not for a key that sits in page source: the origin list says where it may be called from, and says nothing about what it may cost.
credits_cap_cycle on the same PATCH /v1/keys/{id} bounds that key alone:
curl -X PATCH https://api.astroway.info/v1/keys/433 \ -H "X-Api-Key: aw_live_master_key" \ -H "Content-Type: application/json" \ -d '{"credits_cap_cycle": 5000}'null clears it. 0 means a key that may spend nothing, which is a way to park a key without revoking it. The counter is the same one credits use, and it resets with the billing cycle.
A key that reaches its own budget gets 429 KEY_BUDGET_EXHAUSTED, even when the account still has credits, and even on a plan with overage: a budget you can overspend for money is not a budget. While a budget is set, every answer carries X-Key-Budget-Limit and X-Key-Budget-Remaining.
An endpoint scope for one key
Section titled “An endpoint scope for one key”A key with no scope may call everything its plan includes. allowed_endpoints narrows it to a list of paths, each either exact (chart) or a namespace (embed/*). It is the third and independent limit: the origin list says where a call may come from, the budget says what it may cost, the scope says what it may call.
curl -X PATCH https://api.astroway.info/v1/keys/433 \ -H "X-Api-Key: aw_live_master_key" \ -H "Content-Type: application/json" \ -d '{"allowed_endpoints": ["embed/*", "public/moon-phase"]}'null clears it. An empty list is refused, because it would read equally well as “everything” and as “nothing”. Paths are written relative to /v1, without a leading slash, and case does not matter.
A call outside the list gets 403 ENDPOINT_NOT_IN_SCOPE, and details names the list so you do not have to guess. The scope applies to both key classes, and to /v1/public/* and /v1/embed/* exactly as to everything else. Account reads are not exempt from it, unlike credits: a key narrowed to embed/* cannot read its own usage either. That is deliberate, because a key sitting in page source should not hand a visitor the account’s spending, and it does not get in the way of managing the key: the dashboard authenticates with a session, not with the key.
Best practices
Section titled “Best practices”1. Do not put a server-only key in the frontend
Section titled “1. Do not put a server-only key in the frontend”The default key is a backend-level secret. If it lands in JS that runs in the user’s browser - it’s visible in DevTools and will leak. That is exactly why such a key answers 403 ORIGIN_FORBIDDEN to any request carrying an Origin or a Referer.
When you do need to call from the browser, it is not done with an ordinary key but with one created as origin_restriction.type: "public": keys for the browser. That key is prefixed pk_ and is visible in DevTools too, and that is the point: what it is worth is bounded by the list of your own domains, and it refuses a request that carries no origin at all.
Correct: your backend holds the key, frontend calls your backend, your backend calls AstroWay API.
Incorrect:
// DO NOT DO THISfetch('https://api.astroway.info/v1/chart', { headers: { 'X-Api-Key': 'aw_live_...' }, // visible in browser});2. Environment variables, not hardcode
Section titled “2. Environment variables, not hardcode”# .env (in .gitignore)ASTROWAY_API_KEY=aw_live_...// in codeconst key = process.env.ASTROWAY_API_KEY;if (!key) throw new Error('ASTROWAY_API_KEY is not set');3. Separate keys per environment
Section titled “3. Separate keys per environment”production-backend: productionstaging-backend: stagingci-tests: CI/CD pipelinelocal-dev-alice: local development per developer
If one key is compromised - revoke just that one, others keep working.
4. Rotation
Section titled “4. Rotation”Recommended frequency - every 90 days. Or immediately on suspected leak.
Zero-downtime rotation:
- Create a new key with the same
name+-v2 - Deploy it to production (in parallel with old one)
- Confirm traffic flows to new key (dashboard)
- Revoke the old one
5. Monitor usage
Section titled “5. Monitor usage”Dashboard shows requests per key separately. If one key suddenly starts making 10× more requests than usual - investigate.
Set an alert: Settings → Notifications → Alert when daily credits > X.
Authentication errors
Section titled “Authentication errors”| Code | Meaning |
|---|---|
401 Unauthorized | X-Api-Key header missing or key invalid |
403 Forbidden | Key valid but lacks scope (e.g. regular key managing other keys) |
402 Payment Required | Credits exhausted (Free) or subscription inactive |
429 Too Many Requests | Rate limit exceeded (10/60/300 req/min per plan) |
Example 401:
{ "error": { "code": "invalid_api_key", "message": "The API key provided is invalid or has been revoked.", "request_id": "8aec43e9bc749b37e998a489bbad1757" }}Sandbox mode
Section titled “Sandbox mode”Keys with aw_test_ prefix run in sandbox:
- No credit consumption
- Return stub or minimal real responses
- Don’t affect production statistics
- All endpoints available
Useful for CI/CD, e2e tests, and local development. Created in dashboard with one click.
- Credits & Rate Limits: how billing and rate limiting work
- Errors: full error code reference