freeastroapi / sdk
Official PHP SDK for FreeAstroAPI: Western, Vedic and Chinese astrology, numerology, horoscopes and ephemeris over a typed REST client.
Requires
- php: >=8.1
- ext-json: *
- php-http/discovery: ^1.19
- psr/http-client: ^1.0
- psr/http-client-implementation: ^1.0
- psr/http-factory: ^1.0
- psr/http-factory-implementation: ^1.0
- psr/http-message: ^1.1 || ^2.0
Requires (Dev)
- guzzlehttp/guzzle: ^7.8
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpunit/phpunit: ^10.5 || ^11.5
- squizlabs/php_codesniffer: ^3.10
- symfony/http-client: ^6.4 || ^7.0
Suggests
- guzzlehttp/guzzle: Default PSR-18 HTTP client; the SDK applies its timeout option to it automatically.
- symfony/http-client: Alternative PSR-18 HTTP client (with nyholm/psr7); the SDK applies its timeout option to it automatically.
Provides
None
Conflicts
None
Replaces
None
README
Official PHP client for FreeAstroAPI: Western, Vedic and Chinese astrology, numerology, horoscopes, Moon data and ephemeris, with a typed method for every public endpoint.
- PHP 8.1 or newer.
- Any PSR-18 HTTP client (Guzzle, Symfony HttpClient, ...), found automatically with
php-http/discovery. - Request and response shapes generated from the OpenAPI document as PHPStan/Psalm array shapes.
Install
composer require freeastroapi/sdk guzzlehttp/guzzle
Guzzle is the suggested client. Any PSR-18 client with PSR-17 factories works instead, for example composer require freeastroapi/sdk symfony/http-client nyholm/psr7.
Quick start
<?php require 'vendor/autoload.php'; use FreeAstroApi\Sdk\FreeAstro; $client = new FreeAstro(apiKey: 'YOUR_API_KEY'); // or set FREEASTRO_API_KEY and call new FreeAstro() $cities = $client->geo->search(['q' => 'Paris', 'limit' => 3]); echo $cities['results'][0]['timezone'];
Keep the API key on the server; never send it to browsers.
Examples
Natal chart
// POST /api/v1/natal/calculate?include_minor=true $chart = $client->natal->calculate( ['year' => 1990, 'month' => 6, 'day' => 15, 'hour' => 14, 'minute' => 30, 'city' => 'Paris'], ['include_minor' => true], );
Vedic panchang
// POST /api/v2/vedic/panchang $panchang = $client->vedic->panchang([ 'year' => 2026, 'month' => 1, 'day' => 14, 'hour' => 6, 'minute' => 0, 'city' => 'Delhi', ]);
Geo search
// GET /api/v2/geo/search?q=Lyon&limit=5 $result = $client->geo->search(['q' => 'Lyon', 'limit' => 5]); foreach ($result['results'] as $city) { printf("%s (%s) %s\n", $city['name'], $city['country'], $city['timezone']); }
Resources
Every operation in the OpenAPI document is a method on a resource property. Resources follow the API paths, exactly like the TypeScript and Python SDKs:
$client->natal->calculate($birth); // POST /api/v1/natal/calculate $client->western->natal->insights($request); // POST /api/v1/western/natal/insights $client->western->progressions->secondaryExactAspects($request); // POST /api/v1/western/progressions/secondary/exact-aspects $client->chinese->calendar('2026-02-17', ['lang' => 'en']); // GET /api/v1/chinese/calendar/{date} $client->horoscope->daily->sign(['sign' => 'aries']); // GET /api/v3/horoscope/daily/sign
Arguments are, in order: path parameters (as scalars), the JSON body (an array keyed by the API's field names), the query parameters (an array; null values are left out), and optional RequestOptions.
When an operation exists in several API versions, the plain method name calls the latest one and older versions carry a suffix: $client->vedic->chart() calls /api/v2/vedic/chart and $client->vedic->chartV1() calls /api/v1/vedic/chart; $client->horoscope->daily->signV1() / signV2() reach the older daily horoscopes. Deprecated operations are marked @deprecated.
Resource groups: ai, chinese, ephemeris, geo, horoscope, moon, natal, numerology, skyEvents, space, transits, vedic and western (with astrocartography, directions, electional, natal, progressions, signs, transits and more). FreeAstroApi\Sdk\Operations::ALL maps every operationId to its method.
Types
Bodies and responses are plain PHP arrays at runtime: responses are decoded JSON and are not validated, so new API fields never break your code. Their shapes are documented as type aliases in src/Schemas.php, which PHPStan, Psalm and PhpStorm use for checking and completion:
use FreeAstroApi\Sdk\Schemas; /** * @phpstan-import-type PanchangRequestV2 from Schemas */ final class DailyPanchang { /** @param PanchangRequestV2 $birth */ public function today(array $birth): string { /* ... */ } }
Every operation has a typed response: JSON operations return their response shape (for example MoonPhaseResponse), image and Markdown operations return string. The shapes describe what the API documents; the SDK never checks them at runtime, so a response with a missing field, a null, an extra field or a new enum value is returned unchanged instead of throwing.
A top-level empty body [] is sent as {}. PHP cannot tell an empty list from an empty object, so to send an empty JSON object inside a body use new \stdClass().
Images, Markdown and PDF
Operations that return another content type return the body as a string: SVG or Markdown text, or raw bytes for PNG and PDF.
$svg = $client->vedic->kpRender($birth + ['format' => 'svg']); file_put_contents('chart.svg', $svg);
Other endpoints
Call anything else with the generic methods. request() decodes JSON and returns other bodies as strings; requestRaw() returns the PSR-7 response:
$data = $client->request('POST', '/api/v1/example', query: ['lang' => 'fr'], body: ['example' => true]); $response = $client->requestRaw('GET', '/api/v1/example', accept: 'image/png');
Configuration
use FreeAstroApi\Sdk\FreeAstro; $client = new FreeAstro( apiKey: 'YOUR_API_KEY', // default: FREEASTRO_API_KEY baseUrl: 'https://api.freeastroapi.com', // default: FREEASTRO_BASE_URL, then this URL timeout: 20.0, // seconds maxRetries: 2, headers: ['X-Request-Source' => 'my-app'], );
The timeout is applied when the SDK creates the HTTP client itself (Guzzle, or Symfony HttpClient when Guzzle is absent). To pass your own client, configure its timeout there:
$client = new FreeAstro( apiKey: 'YOUR_API_KEY', httpClient: new \GuzzleHttp\Client(['timeout' => 30, 'proxy' => 'http://proxy:8080']), );
PSR-17 factories are discovered too; pass requestFactory: and streamFactory: to choose them.
Every request sends User-Agent: freeastroapi-php/<version> and the key in the x-api-key header.
Retries
Failed requests are retried automatically, up to maxRetries times (default 2), with exponential backoff and full jitter: a random wait of up to 0.5 s, then up to 1 s, 2 s, ... capped at 8 s.
- 429 Too Many Requests is retried for every operation, since the API rejected the request before running it.
- 408, 500, 502, 503, 504, network errors and timeouts are retried for every operation except the side-effecting ones listed in
FreeAstroApi\Sdk\RetryPolicy::SIDE_EFFECTING_OPERATIONS:v1_natal_calculate(can create a paid report job),v1_ai_generate,v2_vedic_chat,v2_western_chat,v2_vedic_qa,v1_western_transits_timelineandv1_western_transits_insights. Those are retried only when you pass an idempotency key (sent as theIdempotency-Keyheader) or opt in withretry: true. - Other 4xx statuses (400, 401, 403, 404, 409, 422, ...) are never retried.
When the API says how long to wait, with a Retry-After header (seconds or an HTTP date) or retry_after_ms in the error body, the SDK waits exactly that long instead of the backoff. If that is more than 30 seconds it does not wait: it throws right away (RateLimitException for a 429) and getRetryAfter() returns the requested delay in seconds, so you can schedule the retry yourself.
Per-request settings override the defaults:
use FreeAstroApi\Sdk\RequestOptions; $client->western->chat($message, new RequestOptions(idempotencyKey: 'chat-2026-01-14-001')); // retried safely $client->natal->calculate($birth, options: new RequestOptions(retry: true)); // retry transient errors anyway $client->geo->search(['q' => 'Paris'], new RequestOptions(retry: false)); // no retries at all, 429 included $client->vedic->panchang($birth, new RequestOptions(maxRetries: 5));
Generic request() calls follow the same rules, matching side-effecting operations by method and path.
Errors
Every exception implements FreeAstroApi\Sdk\Exception\FreeAstroException. HTTP errors extend ApiException, which carries the status, raw body, decoded body (getDetails(), usually the spec's ErrorResponse: detail, error, retry_after_ms, ...), error code and request id. getErrorCode() returns the stable code from error, detail.error or code:
| Status | Exception |
|---|---|
| 400, 422 | ValidationException (getErrors() lists FastAPI field errors) |
| 401 | AuthenticationException |
| 403 | PermissionDeniedException |
| 404 | NotFoundException |
| 409 | ConflictException |
| 429 | RateLimitException (getRetryAfter(): Retry-After or retry_after_ms, in seconds) |
| 5xx | ServerException |
| other | ApiException |
NetworkException means no response arrived (DNS, connection, TLS, timeout); ConfigurationException means no API key or HTTP client was found.
use FreeAstroApi\Sdk\Exception\ApiException; use FreeAstroApi\Sdk\Exception\RateLimitException; use FreeAstroApi\Sdk\Exception\ValidationException; try { $chart = $client->natal->calculate($birth); } catch (ValidationException $e) { foreach ($e->getErrors() as $error) { echo implode('.', $error['loc']), ': ', $error['msg'], "\n"; } } catch (RateLimitException $e) { retryLater($e->getRetryAfter()); } catch (ApiException $e) { error_log(sprintf('%d %s (request %s)', $e->getStatusCode(), $e->getMessage(), $e->getRequestId() ?? '-')); }
WordPress
The SDK works in WordPress plugins and themes. Install it with Composer in your plugin together with Guzzle or Symfony HttpClient (composer require freeastroapi/sdk guzzlehttp/guzzle), load vendor/autoload.php from the plugin, and keep the key in an option or wp-config.php constant rather than in front-end code. If other plugins ship different Guzzle versions, prefix your dependencies with a tool such as PHP-Scoper or Strauss, or pass any PSR-18 client you already bundle via httpClient:.
Development
The repository is the library plus an in-repo generator. src/Resources/, src/Schemas.php, src/Operations.php and src/ClientResources.php are generated from spec/freeastro.json by generator/generate.php (plain PHP 8.1, no dependencies); everything else is hand-written.
composer install composer check # generated code is current, PSR-12, PHPStan (level max), PHPUnit composer test:contract # every operation against a Prism mock (Node.js and Python 3)
Without a local Composer, run the same with Docker:
docker run --rm -v "$PWD":/app -w /app composer:2.10.3 composer install docker run --rm -v "$PWD":/app -w /app php:8.1-cli vendor/bin/phpunit
To update the spec and regenerate:
python3 scripts/spec_sync.py fetch # writes the normalised production spec to spec/freeastro.json
composer generate
The Sync spec workflow does this daily and opens a spec-sync/auto pull request when the API changes; Live smoke test calls production daily when the FREEASTRO_TEST_API_KEY secret is set. scripts/spec_sync.py, scripts/with_prism.py and tests/fixtures/synthetic-spec.json are shared with the other FreeAstroAPI SDKs.
Releasing
Bump FreeAstro::VERSION in src/FreeAstro.php, update CHANGELOG.md, then push a matching tag (git tag v0.1.0 && git push origin v0.1.0). The Release workflow runs the checks and creates the GitHub release; Packagist publishes the tag through its GitHub webhook.
Issues and contributions are welcome on GitHub.
Licensed under the Apache License 2.0.