The SkySolver API
Plate solve a photo of the night sky, or a list of its stars, and draw its annotation, as SVG to lay over the photo or as JSON to draw yourself: the same solver and the same annotations as the playground.
Overview
- Base URL
https://skysolver.app/api/v1. Requests and answers are JSON (UTF-8); the annotation can also come as SVG. - Three endpoints: solve an image sent as base64, solve star positions you measured yourself, and annotate a solved image.
- CORS is open: you can call the API from a web page.
- Versions: breaking changes get a new path (
/api/v2);v1only gains optional fields. The OpenAPI document describes every model. - Fair use: the service is free and runs on a small server. For large batches, solve star lists you detect yourself, or ask at [email protected].
Quick start
Solve a photo, then get its annotation as an SVG of the photo's size, to lay over it:
# 1. solve: the body is {"image": "<the file in base64>"}
base64 < photo.jpg | jq -Rs '{image: .}' \
| curl -s https://skysolver.app/api/v1/solve/image \
-H 'Content-Type: application/json' --data-binary @- > solution.json
# 2. annotate: the whole solution goes in as the sky model
jq '{solution: ., format: "svg"}' solution.json \
| curl -s https://skysolver.app/api/v1/annotate \
-H 'Content-Type: application/json' --data-binary @- > annotation.svgThe same in JavaScript (Node 18 or later; in a browser only reading the file differs):
import { readFile } from "node:fs/promises";
const post = (path, body) => fetch("https://skysolver.app/api/v1" + path, {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
});
// in a browser: FileReader's readAsDataURL(file) gives a data: URL, which works as it is
const image = (await readFile("photo.jpg")).toString("base64");
const solution = await (await post("/solve/image", { image })).json();
if (solution.solved) {
const answer = await post("/annotate", { solution, time: "2024-10-12T21:34:05+02:00" });
const svg = await answer.text();
}And in Python:
import base64, json, urllib.request
API = "https://skysolver.app/api/v1"
def post(path, body):
req = urllib.request.Request(API + path, data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as r:
return r.read()
with open("photo.jpg", "rb") as f:
image = base64.b64encode(f.read()).decode()
solution = json.loads(post("/solve/image", {"image": image}))
if solution["solved"]:
print(solution["center"], solution["scale_arcsec"])
svg = post("/annotate", {"solution": solution, "format": "svg"})Limits
Each client address has a bucket of requests per endpoint: up to burst at once, refilled at a steady rate. A request beyond it is answered at once with 429 and Retry-After. Every answer carries RateLimit-Limit (the burst), RateLimit-Remaining, RateLimit-Reset (seconds until the bucket is full) and RateLimit-Policy.
| Endpoint | Burst | Refill | Request body |
|---|---|---|---|
POST /solve/image | 10 | 1 a minute | 28 MB (a 20 MB file in base64) |
POST /solve/stars | 30 | 30 a minute | 4 MB |
POST /annotate | 30 | 30 a minute | 2 MB |
GET /health | 60 | 60 a minute | – |
- Images: at most 20 MB and 120 megapixels. The solver searches for at most
time_limit_ms(1–30 s, 10 s by default). - Star lists: 5 to 10,000 stars; the 3,000 brightest are used.
- Busy: the server runs a few solves at a time (
max_concurrentin health). Beyond that a request is answered at once with429,error: "busy"andRetry-After: wait that long, plus a random second or so, and send it again. GET /healthreturns the limits in force.
Conventions
- Sky coordinates are degrees in the ICRS (J2000): RA 0–360, Dec −90–90.
- Pixels count from 0 at the centre of the top-left pixel; x grows to the right and y down. The image's top-left corner is therefore (−0.5, −0.5) and its centre (width/2 − 0.5, height/2 − 0.5). FITS counts from 1: star lists can say so (
pixel_origin), andfits_headeruses FITS's own convention. - Times are ISO 8601 with a UTC offset or
Z:2024-10-12T21:34:05+02:00. A time without one is refused, never guessed. - Errors are
{"error": code, "message": text}, withdetailsfor invalid fields: see Errors.
Solve an image
Detects the stars of an image and finds where in the sky it points: blind, with no hint of position, scale or rotation, for fields from a few arcminutes to all-sky panoramas. The image is decoded and solved in memory and not kept.
data:image/…;base64, prefix is fine). At most 20 MB decoded and 120 megapixels. Formats: JPEG, PNG (8/16-bit), TIFF (8/16/32-bit and float; uncompressed, LZW, ZIP, PackBits), FITS (integer or float; grey, RGB or a colour camera's raw mosaic), XISF (uncompressed or zlib), BMP, GIF, TGA and PSD. Linear data (stacks, FITS, XISF) are stretched before detection.10000Allowed 1000–30000{
"image": "/9j/4AAQSkZJRgABAQEASABIAAD…",
"time_limit_ms": 10000
}The answer is the solution, plus image (format, width, height) and capture: when and where the photo was taken, as its metadata says (EXIF, XMP, IPTC, FITS headers, XISF properties).
+02:00; null when unknown.EXIF DateTimeOriginal, FITS DATE-OBS, XISF Observation:Time:Start…{lat, lon, height, source}, degrees east and north, metres.{
"api_version": "1.0",
"solved": true,
"width": 2048,
"height": 1366,
"wcs": {
"ctype": "RA---TAN-SIP",
"crval": [57.56884791, 30.441625],
"crpix": [1023.5, 682.5],
"cd": [[0.0004834903827, 0.008644326623], [-0.008637593318, 0.0004834749004]],
"sip": { "order": 3, "a": [[0, 2, -1.585329748e-7], …], "b": […], "inv_order": 4, "ap": […], "bp": […] }
},
"projection": null,
"center": { "ra": 57.56884791, "dec": 30.441625 },
"corners": [{ "name": "top-left", "x": -0.5, "y": -0.5, "ra": 49.44900035, "dec": 38.65911116 }, …],
"fov": { "width_deg": 17.6401047, "height_deg": 11.79908067 },
"scale_arcsec": 31.15610913,
"rotation_deg": 266.7987974,
"parity": "normal",
"stats": { "stars": 3294, "matches": 972, "rms_px": 0.3723350842, "log_odds": 3198.350966, … },
"fits_header": "SIMPLE = T …",
"matched": [[1988.17, 1290.68, 63.70586, 22.302973, 8.59], …],
"timing_ms": { "solve": 153.9, "detect": 40.3, "decode": 507.7, "server": 844.4 },
"image": { "format": "FITS", "width": 2048, "height": 1366 },
"capture": {
"time": { "utc": "2025-08-16T21:00:00.000Z", "local": "2025-08-16T23:00:00.000", "utc_offset": "+02:00",
"source": "FITS DATE-OBS", "exposure_s": 60 },
"location": { "lat": 52.2297, "lon": 21.0122, "height": 110, "source": "FITS SITELAT/SITELONG" },
"other_dates": []
}
}Solve star positions
Solves from star positions you measured yourself (SExtractor, SEP, photutils, PixInsight's StarDetector, your own centroids): the image never leaves your machine, and the request is a few kilobytes. This is how the playground solves.
8–1000008–100000[x, y], [x, y, flux] or {"x": …, "y": …, "flux": …}. flux is any positive brightness measure (sum, peak, …): give it for every star or for none, and without it list the stars brightest first. 30 to 300 stars is plenty; the 3,000 brightest are used. Positions must lie in the image.5–10000 items0: the centre of the first pixel is (0, 0), as in numpy and OpenCV. 1: it is (1, 1), as in FITS, SExtractor, astrometry.net and DS9. Answers are always 0-based.0Allowed 0 · 110000Allowed 1000–30000{
"width": 6000,
"height": 4000,
"stars": [
[3735.37, 69.14, 51230],
[2120.04, 1987.51, 48810],
{ "x": 812.6, "y": 3020.9, "flux": 47002 },
…
]
}The answer is the solution.
The solution
Both solve endpoints answer with this object.
200 answer, with solved: false and an error.{ra, dec} in degrees. null when the middle of an all-sky image is not on the sky.top-left, top-right, bottom-right, bottom-left: each {name, x, y, ra, dec}, with ra and dec null where a corner is off the sky.{width_deg, height_deg}: the angular length of the image's horizontal and vertical middle lines.normal · mirroredcrval (RA, Dec of the reference pixel), crpix (the reference pixel, 0-based), cd (degrees per pixel) and sip (distortion polynomials, terms [p, q, coefficient]). null for projection solutions.stars (detected or given), matches, rms_px (how far the matched stars are from the model), log_odds (the evidence for the match, a natural logarithm: a solution needs 25 and 10 matched stars), and details of the search.[x, y, ra, dec, magnitude] each, the magnitude Gaia's G.decode (reading the file), detect, solve, and the server's total for the request, in milliseconds.Annotate a solved image
Draws the coordinate grid, constellations, deep-sky objects (15,000 of them: Messier, NGC, IC, Sharpless, Barnard, Lynds, van den Bergh, Caldwell) and, given a time, the Sun, the Moon and the planets, as the playground's Save image draws them: the same objects, labels and styles. You send the sky model, not the image. The annotation is not clipped to the sky: the playground finds the horizon in the photo itself.
corners, wcs and projection; taken from solution or the FITS header's NAXIS1/NAXIS2 otherwise.8–100000svg: an SVG document of the image's size, transparent, to lay over the photo. json: the objects, bodies, lines and placed labels as data (JSON output)."svg"Allowed svg · jsonZ). With it, the Sun, the Moon, the planets and Pluto are drawn where they were (within 1″ for 1850–2150).-90–90-180–180-500–100001400Allowed 200–80000Allowed 0–1{
"solution": { "width": 2048, "height": 1366, "wcs": { … }, … },
"format": "svg",
"time": "2025-08-16T23:00:00+02:00",
"location": { "lat": 52.23, "lon": 21.01 },
"view": { "group_by": "catalog", "grid": { "on": false }, "catalogs": { "solar": { "color": "#ff8800", "size": 16 } } },
"filter": { "catalogs": { "ldn": false, "lbn": false }, "magnitude": [-2, 10] },
"edits": { "hidden": ["Mars"], "names": { "NGC 1499": "California" } }
}With format: "svg" the answer is image/svg+xml: <svg width="2048" height="1366" viewBox="0 0 2048 1366">…, in the photo's pixels. A warning about the sky model, if any, comes in the SkySolver-Warning header.
Sky models
Send the one you have; all describe how the image's pixels map to the sky.
{ra, dec}: center, top_left, top_right, bottom_right, bottom_left. A gnomonic (TAN) projection is fitted to them; for wide-angle lenses that distort, the answer warns how far the corners are from it. Every corner must be less than 87° from the centre: wider views need the solver's projection.crval [RA, Dec], crpix [x, y] (0-based), cd [[CD1_1, CD1_2], [CD2_1, CD2_2]] in degrees per pixel, and optionally sip {order, a, b, inv_order, ap, bp} with terms [p, q, coefficient].CTYPE1 = 'RA---TAN' or 'RA---TAN-SIP', CRVAL, CRPIX (1-based, as FITS has it), and CD, or PC with CDELT, or CDELT with CROTA2; SIP's A_*, B_*, AP_*, BP_*. What astrometry.net, ASTAP, PixInsight and this solver write./solve returns it./solve answer: its size and its wcs or projection are used.JSON output
With format: "json": what the annotation draws, in image pixels (0-based, y down), and the labels as placed.
center {ra, dec}, scale_arcsec (at the centre), diagonal_deg (null for all-sky views).width (the layout_width used) and scale: image pixels per layout pixel, which sizes and widths were multiplied by.M 42, NGC 1977), as edits takes it, and the label drawn (M42).Orion Nebula, or the one given in edits) and the catalogue's own.emission…) and every catalogue group it is in.["Mel 22", "LBN 770"].[x0, y0, x1, y1, …] in pixels (a traced outline, or the catalogue's ellipse); closed is false when the shape leaves an all-sky view's domain and only pieces of it are drawn.edits.hidden hides it.time: the Sun, the Moon and the planets in the frame: id, name, kind, ra, dec, mag, diameter_arcsec, distance_au, phase (lit fraction), elongation_deg, topocentric (placed as seen from location), x, y, radius_px, outline, shown.id, name, lines (polylines in pixels), stars, label_at.kind (ra or dec), value (degrees), label, lines.text, of (the object), kind, x, y (the text's baseline anchor), size, anchor (start, middle, end), color, opacity.Options
All optional, and partial: what you leave out keeps the playground's defaults. Values out of range, and keys that do not exist, are refused with 422 rather than ignored.
types or catalogs below then apply."type"Allowed type · catalogon and a line style (default #96beff, dashed).on and a line style.on (off by default), color, opacity, blur (0–16 px), distance (0–12 px).truetrue{"emission": {"color": "#ff99cc"}}.#rrggbb0–10.3–6solid · dashed · dotted · dashdot6–32group_by: "type"): {"dark": false}. All are on by default.group_by: "catalog").auto: Messier objects always, others by how prominent they are in the frame (size, or a magnitude limit that follows the image scale). A range: objects with a magnitude inside it; those without one (most nebulae) by size."auto"Allowed "auto" · [brightest, faintest] within -2–18objects[].id gives it: ["NGC 1999", "Mars"].{"NGC 1499": "California"}; an empty string labels an object by its designation alone.Groups and styles
The keys of view.types, filter.types, view.catalogs and filter.catalogs, with their default style (colour, opacity, width, line, font size):
| types key | Group | Default |
|---|---|---|
galaxy | Galaxies | #ffb35c0.8 · 1 px · solid · 10.5 px |
open | Open clusters | #7dd3fc0.8 · 1 px · solid · 10.5 px |
globular | Globular clusters | #fde68a0.85 · 1 px · solid · 10.5 px |
emission | Emission nebulae | #d8b4fe0.85 · 1 px · solid · 10.5 px |
reflection | Reflection nebulae | #93c5fd0.8 · 1 px · solid · 10.5 px |
planetary | Planetary nebulae | #6ee7b70.85 · 1 px · solid · 10.5 px |
snr | Supernova remnants | #fda4af0.8 · 1 px · solid · 10.5 px |
dark | Dark nebulae | #cbd5e10.5 · 1 px · solid · 10.5 px |
solar | Solar system | #d9f99d0.9 · 1.2 px · solid · 12 px |
other | Other | #e2e8f00.7 · 1 px · solid · 10.5 px |
| catalogs key | Group | Default |
|---|---|---|
messier | Messier | #facc150.9 · 1 px · solid · 12 px |
ngc | NGC | #7dd3fc0.8 · 1 px · solid · 10.5 px |
ic | IC | #a5b4fc0.8 · 1 px · solid · 10.5 px |
sharpless | Sharpless (Sh2) | #d8b4fe0.85 · 1 px · solid · 10.5 px |
barnard | Barnard (B) | #cbd5e10.55 · 1 px · solid · 10.5 px |
ldn | Lynds dark (LDN) | #94a3b80.5 · 1 px · solid · 10.5 px |
lbn | Lynds bright (LBN) | #f9a8d40.8 · 1 px · solid · 10.5 px |
vdb | van den Bergh (vdB, vdBH) | #93c5fd0.8 · 1 px · solid · 10.5 px |
caldwell | Caldwell | #6ee7b70.8 · 1 px · solid · 10.5 px |
solar | Solar system | #d9f99d0.9 · 1.2 px · solid · 12 px |
other | Other | #e2e8f00.7 · 1 px · solid · 10.5 px |
Errors
Every error answer has the same shape: a code to act on, and a message to show. Invalid requests list each problem:
{
"error": "invalid_request",
"message": "view.grid.color: String should match pattern '^#[0-9a-fA-F]{6}$'",
"details": [
{ "field": "view.grid.color", "problem": "String should match pattern '^#[0-9a-fA-F]{6}$'" },
{ "field": "filter.magnitude", "problem": "magnitude must be \"auto\" or [brightest, faintest] within -2..18" }
]
}| Status | error | When |
|---|---|---|
| 400 | invalid_image | The image is not valid base64, or cannot be read. |
| 400 | unsupported_format | The file is not one of the formats above. |
| 413 | payload_too_large | The request body, the file or its pixel count is over the limit. |
| 422 | invalid_request | A field is missing, of the wrong type, out of range or unknown; details lists each. |
| 429 | rate_limited | Your client address used up its requests; retry_after says when to try again. |
| 429 | busy | The server is solving as much as it can; retry after Retry-After. |
| 500 | internal_error | Something failed on our side. |
| 503 | unavailable | The solver or the annotator is not ready. |
An image that cannot be solved is not an error: the answer is 200 with solved: false.
Health
The service's status, the star indexes and annotator workers loaded, and the limits in force.
{
"status": "ok",
"version": "1.0",
"uptime_s": 86400.2,
"solver": { "indexes": 4, "max_concurrent": 4, "in_flight": 0 },
"annotator": { "ok": true, "workers": 2, "objects": 15333 },
"limits": {
"rates": {
"image": { "burst": 10, "per_minute": 1 },
"stars": { "burst": 30, "per_minute": 30 },
"annotate": { "burst": 30, "per_minute": 30 },
"light": { "burst": 60, "per_minute": 60 }
},
"image_mb": 20, "megapixels": 120, "stars": 10000, "time_limit_ms": 30000
}
}Changelog
- 1.0 (16 October 2026): solve an image (base64) or star positions, annotate as SVG or JSON, with the Sun, Moon and planets.
Questions, a bug, a larger quota? [email protected]. By using the API you accept the terms; what it does with your data is in the privacy page.