AstroCollab is a draft API specification for shooting one target with dozens of other astrophotographers: a 600-hour H-alpha mosaic of the Veil, or the faint outer halo of M31. At dusk your rig asks what to shoot and gets tonight's panels, filter and sub count, cut to its own field of view. It reports what it shot, and every good night counts toward an image no single backyard could build.
This is a spec, not an implementation. It defines an API for capture software to build in, and for servers that host projects. Starfront's app and server already speak it; other software, such as N.I.N.A., could build it in. This site holds the spec, plus an example server and a conformance tester.
DRAFT 0.2.0-draft.1 The specification may change incompatibly.
M31 / H-ALPHA / 412 OF 600 HOURSTONIGHT
For software authors Build it into your capture app17 REST calls Sign in, ask, reportJSON Schema For every payloadRead the REST reference →
HOW YOU TAKE PART
Join once. Shoot when it's clear.
You sign your capture software in and join a project once. On each clear night it asks the server what to shoot, and the server deals panels where the stack is thinnest and your rig has been least. You report what you shot.
Each rig tiles the project with its own camera: a 135 mm lens may take M31 in one frame, a 2000 mm SCT in dozens of panels. Each night the server deals panels where the stack is thinnest and nobody else is shooting, in one filter: H-alpha or SII under a bright Moon, OIII or LRGB when it is dark.
Nothing is reserved. The server spreads rigs across the field so every patch sees several telescopes, and good data from anyone counts.
⌁
Every good night counts
Each night is checked for star size, guiding, filter and Moon, in arcseconds so every rig is judged alike. A night counts once, however often it is reported.
↗
Your rig, your call
The server says where the stack needs data. Your capture software decides when to slew, and your safety limits always win.
This is a draft specification for capture software, such as Starfront or
N.I.N.A., to build in. Starfront already speaks it. This page follows one
telescope from getting its token to having its subs counted, and is written for
people adding the API to their software. Every snippet is a real payload,
captured from Starfront's server; full files are in examples/.
A project is one deep image that many astrophotographers shoot together, such as
a 10-hour-per-filter narrowband mosaic of M31's halo. It names a region of sky,
the depth wanted in each filter, and the rules data must meet. Each telescope
takes a share, tiles the region with its own camera, and every night asks which
panels to shoot. It reports what it shot; the server checks the numbers and
counts the good nights toward the project.
The server never moves your mount and never reserves sky.
The flow
Step
Request
Token
1. Get a telescope token
POST /api/v1/auth/login, then POST /api/v1/agents; or POST /api/v1/pair
None, then a person's
2. Describe the rig
POST /api/v1/agent/hello
The telescope's
3. Find a project
GET /api/v1/agent/projects
The telescope's
4. Join it
POST /api/v1/agent/projects/{id}/join
The telescope's
5. Ask what to shoot tonight
GET /api/v1/agent/task?night=…&moon=…&moonUp=…
The telescope's
6. Shoot
Your own software
—
7. Report what you shot
POST /api/v1/agent/report
The telescope's
Steps 1 to 4 happen once. Steps 5 to 7 repeat each night; programs typically ask
every ten minutes, which also keeps the telescope shown as online. The examples
use two shell variables:
SERVER=https://collab.example # the server's address
TOKEN=... # the telescope's token, from step 1
1. Get a telescope token
There are two ways; a server's GET /api/v1/health lists which it offers in
features.
Sign in, then enrol. The program asks for a login code and opens the page it
gives in the browser:
The person signs in there. The program polls GET /api/v1/auth/poll?code=…
until state is done, which hands over the person's token once. With that, it
enrols the telescope:
The reply holds the telescope's own token, shown once. Store it in the system
credential store.
Pairing. On servers that offer it, the person issues a code on the server's
web pages, and the program trades it for the same reply:
POST /api/v1/pair with {"code": "…", "name": "Vega 530"}.
The two tokens never stand in for each other: a person token cannot fetch work,
and a telescope token cannot enrol. See Authentication.
2. Describe the rig
Say hello with what the program already knows: optics, sensor, filters with
their bandpasses, the sub length each filter is shot at, and what the rig
usually achieves.
Star size and guiding are in arcseconds, never pixels. The sub lengths are the
ones your darks are built for: work is dealt at those lengths. presence is
optional and shows the group where you point; GET /api/v1/presence shows
everybody.
Each open project comes with its region, goals in hours per filter, rules, the
hours collected so far, and whether your rig can help, rule by rule:
{ "name": "M51 in LRGB", "kind": "single", "goals": { "L": 20.0, "R": 5.0, "G": 5.0, "B": 5.0 },
"compatibility": { "ok": false,
"summary": "cannot contribute: 530 mm is longer than the 400 mm the project wants; can shoot L — but no R; no G; no B" } }
Show this to the operator before they spend a night on a project they cannot
help. Each listing also gives progress per filter: how much of the field is at
the goal depth, the average, and the thinnest point. For a picture of where the
stack is deep and where it is thin, ask for the project's depth map:
The server checks the rules against your rig and hands back your share: the whole
region tiled with your camera's field, at your own sub lengths. Joining is your
consent, so the share arrives accepted. Joining again returns the same share.
5. Ask what to shoot tonight
Name the night, and say how bright tonight's Moon is at your site and how much of
the dark hours it is up. The server cannot know where you are.
That is one share from the reply, which lists all of yours. Tonight this rig
shoots six of its nine cells (share indexes into cells), all in OIII, 11 subs
of 300 s on each. The Moon is thin, so it is an OIII night; under a bright Moon
the server would send H-alpha or SII, which shoot through moonlight.
The list holds for the whole night: ask again and you get the same panels and
the same version, however many frames other rigs send meanwhile. The first time
you ask in the next night, it is dealt again, sending you where the stack is
thinnest and where you have been least. A version you already have means
nothing new.
Filter names come back as one letter: L, R, G, B, H, O, S. Your
"OIII" and the server's O are the same filter.
6. Shoot
Run the panels in order with your own sequencer. The list is advice: your
software decides when and whether to slew, and your safety limits always win.
Keep the share and panel number with each frame.
7. Report what you shot
Send one record per night, filter and panel, with the solved footprint and what
you measured:
Accepted nights add depth where their footprints lie, which steers everyone's
next night. A measurement you did not send cannot pass a rule; it shows under
unverified. Verdicts are advisory: a project's coordinator can overrule one.
Send every panel you have not yet reported on each poll, and mark a panel
reported only when the server has recorded it. Sending the same panel again is
safe: the larger figure stands, and the reply says duplicate.
Rules for every request
Rule
What to do
Address
Paths start with /api/v1 on the server's address.
Tokens
Authorization: Bearer <token>. Never put a token in a URL or log.
Bodies
JSON. Send numbers as numbers and unknown values as null. Keep or ignore fields you do not know.
Retries
Every call is safe to repeat as is; see the protocol.
Errors
{"detail": ...}. Act on the HTTP status; show detail to the operator.
Status
Meaning
What to do
400
Asked too early, such as joining before saying hello with the rig's optics.
Do what detail says first.
401
Missing, unknown or revoked token.
Ask the person to sign in or pair again.
403
Not yours to do.
Show the error; do not retry.
404
No such thing, or not yours.
Check the ID.
409
Conflicts with how things stand, such as a rig that cannot meet a project.
Show detail.
422
The body does not match its type; detail lists the bad fields.
Fix the program.
503
Not available here, such as sign-in on a server without it.
Use the other way in, or try later.
Next
REST reference: every route, its body, replies and examples.
Version 0.2.0-draft.1. Paths are relative to the server's address, for example https://collab.example.
Send Authorization: Bearer <token> where a token is needed: a telescope's agent
token on telescope routes, a person's token on account routes. Bodies are JSON.
Errors are {"detail": ...}: words for the operator on most errors, a list of
bad fields on 422. Objects are open: keep or ignore fields you do not know. Every
type links to a standalone JSON Schema, so you can
validate payloads without OpenAPI tools. The protocol gives the
rules behind each route.
The other way to get a telescope token, for servers that list the pairing feature. The person issues a single-use code on the server's web pages and types it into the program. Takes no token. Never retry on your own: a lost reply needs a new code.
Takes a share of a project, checked against the profile the rig last sent. The share is the whole region tiled with this rig's own camera, at its own sub lengths. Joining twice returns the share already held. Joining is consent: the share arrives accepted. Sending the night and the Moon deals tonight's list at once, as tonight would. A rig that has not described its optics gets 400; one that cannot meet the rules gets 409.
{
"detail": "cannot contribute: 1000 mm is longer than the 400 mm the project wants; can shoot L — but no R; no G; no B"
}
Ask what to shoot tonight
GET /api/v1/agent/task
Token: The telescope's agent token.
Tonight's work: each share with tonight's panels in share and tonight's filter and frames in visit. A list holds for the whole night the rig names and is dealt afresh the first time it asks in the next one. moon and moonUp describe the rig's own sky tonight and decide whether it is a narrowband night. A version the rig already has means nothing new.
Hands back what was shot, one record per night, filter and panel, and hears whether each counts. Reporting the same night, filter and panel again keeps the larger figure.
There are two kinds of token, and neither can do the other's job:
Token
Belongs to
Can
Person token
Somebody signed in
Enrol and list their own telescopes. Nothing else.
Telescope token
One telescope
Say hello, browse and join projects, fetch tonight's work, report frames. Never change a project.
A telescope token lives in a settings file on an observatory PC, so it must never
be able to rewrite a project. A person's sign-in must never be able to drive a
mount. Keeping them apart guarantees both.
A program gets a telescope token one of two ways. GET /api/v1/health lists
what the server offers in features: signin, pairing or both.
Signing in, then enrolling
The device flow, because capture software is a desktop program with no web page
of its own.
Open url in the browser. The person signs in there with whatever the
server uses. Starfront servers use Discord, and check the person belongs to
the community's Discord server. The program never sees a password.
Poll every few seconds until the person is done:
sh
curl "$SERVER/api/v1/auth/poll?code=EXAMPLE_ONLY_TOKEN_01_xxxxxxxx"
pending means keep waiting. done hands over the person's token once,
with their name and whether they may start projects
(example). claimed means the
token was already handed over; expired means start again.
4. Enrol each telescope with the person token:
sh
curl -X POST $SERVER/api/v1/agents \
-H "Authorization: Bearer $PERSON" -H "Content-Type: application/json" \
-d '{"name": "Vega 530"}'
The reply holds the telescope's token, shown once
(example). One person can enrol
several telescopes; each gets its own token.
GET /api/v1/auth says whether sign-in is on; POST /api/v1/auth/login gives
503 where it is not. GET /api/v1/auth/me says who is signed in, and
POST /api/v1/auth/logout signs out. GET /api/v1/agents lists the person's
telescopes, without their tokens.
Pairing
For servers that list pairing. Nothing to sign into from the program:
The person issues a pairing code on the server's web pages.
They type it into the program.
The program trades it for a telescope token, with no Authorization header:
sh
curl -X POST $SERVER/api/v1/pair \
-H "Content-Type: application/json" \
-d '{"code": "EXAMPLE-ONLY-PAIRING-CODE", "name": "Vega 530"}'
The reply is the same as enrolling: the telescope and its token, shown once.
A code works once and expires within an hour. An unknown, used or expired code
gets 401. Never retry pairing on your own: if the reply was lost, ask the
person for a new code.
Keeping tokens safe
Store tokens in the system credential store, not a plain settings file where
you can avoid it.
Send them only in the Authorization header, only over HTTPS, and only to the
server's own address.
Never write them to URLs, logs or reports.
Enrol or pair each telescope separately, so one can be revoked alone.
On 401, stop and ask the person to sign in or pair again. Do not retry in a
loop.
For servers
Offer at least one of sign-in and pairing, and list it in features.
Keep person and telescope tokens apart: answer 401 to either kind on the
other's routes.
Store tokens as hashes where you can, and let people revoke a telescope.
Pairing codes: single use, at most an hour, at least 128 bits of randomness,
and 429 after repeated bad codes.
This page goes through every example payload in order. They were captured from
Starfront's server driving one night, by tools/capture_starfront_examples.py,
so they are what a real server sends. For a shorter tour with requests, read
How a night works. The example index
maps each file to its type.
Discover the server
Health gives the protocol number, 1, and the
server's build. Sign-in status says
sign-in is on; this server signs people in with Discord.
The hello describes "Vega 530": a 530 mm
telescope with a 6248×4176 mono camera of 3.76 µm pixels, about 1.46″ per pixel
and a field of 2.54°×1.70°. It carries 7 nm H-alpha, OIII and SII filters and a
luminance filter of unknown bandpass, shoots 300 s narrowband and 120 s
luminance subs to match its darks, usually reaches 2.4″ stars and 0.62″ guiding,
and gives six hours a night between 21:30 and 04:30. It shares where it points.
The reply gives its ID and the server's time.
M51 in LRGB, a single project wanting 20 h of L and 5 h each of R, G and
B. Vega 530 cannot help: it is longer than the project's 400 mm limit and has
no colour filters. The listing says so, rule by rule.
M31 halo in narrowband, a 7°×4.5° mosaic turned to 35°, wanting 10 h of
H and of O at every point, with stars under 3.5″, subs of 120–600 s, 30° from
the Moon and above 30° altitude. Vega 530 can help.
The join sends the rig's own sub lengths,
and tonight's night and Moon so the first list is dealt at once. The
reply is an accepted share: the
whole mosaic tiled with nine cells of Vega 530's field, turned to 35°, at 300 s
in H and O.
Tonight
The program asks for tonight with the night's
name and a thin Moon (12% lit, up 30% of the dark hours). The
reply deals six of the nine cells, in order,
all in OIII, 11 subs of 300 s each: a dark night goes to the filter that cannot
be shot under a bright Moon. Asking again tonight returns the same list.
Shares a coordinator pushes arrive offered; the program
accepts one and gets it
back. A share from joining needs no
answer.
Report
The report gives two panels of OIII, each 11
frames of 300 s, with the solved footprint, 1.46″ per pixel, stars of 2.3″ and
2.5″, 0.58″ guiding and a 7 nm bandpass. Both are
accepted.
The next night's report has stars of
4.98″, and is rejected against the
project's 3.5″ limit, with the reason in words.
Depth and progress
The depth map cuts the M31 region into
a 16×11 grid and gives everybody's accepted seconds on each cell, per filter.
After the first report, OIII depth sits where the two panels landed and H-alpha
is still empty. The same data gives each project's progress in
the listing: per filter, the share of
the field at the goal, the average depth against it, and the thinnest point.
Presence
Who is on the sky: the rig is online,
pointing at M31, under the name it gave in its presence, "Vega 530 narrowband";
enrolledAs keeps the name it was enrolled under.
Errors
A request with an unknown token
gets 401. A 1000 mm rig that tries to join M51 gets
409 with the reasons.
MUST and MUST NOT mark requirements. SHOULD marks a recommendation; MAY marks
an option. The OpenAPI contract and the
JSON Schemas define payloads. This document defines the
behaviour they cannot.
This version adopts the protocol that Starfront
already speaks, with its paths unchanged. Starfront's capture app and
collaboration server are its first implementation.
1. What the API covers
A project is one image that many astrophotographers build together: a region of
sky, the depth wanted in each filter, and the rules data must meet. The API is
what a telescope's software calls to take part:
Get a telescope token, by signing a person in and enrolling the telescope, or
by pairing.
Say hello: describe the rig.
Browse open projects and join one.
Ask what to shoot tonight.
Report what was shot, and hear whether it counts.
Running projects belongs to each server and its own tools: starting and editing
projects, pushing work to a telescope by hand, overruling verdicts. This protocol
says how a server behaves toward telescopes, not how coordinators run it.
The telescope always asks; the server never reaches into an observatory. So a
rig behind a home router works, and a server going down means "no new work",
not "the night stops". The server never moves a mount, and never reserves sky.
2. Conventions
Paths. Routes live under /api/v1 on the server's address.
GET /api/v1/health is public and gives the protocol version, an integer (this
draft is protocol 1), and, optionally, features: the optional parts the server
offers, such as signin, pairing or files.
Bodies. Requests and responses are JSON. Objects are open: a receiver keeps
or ignores fields it does not know, so a newer program is never refused for
sending one field more. Senders send numbers as numbers and unknown values as
null. Receivers SHOULD read a blank or unreadable number as unknown rather than
refuse the request, as Starfront does.
Identifiers are 12 lowercase hexadecimal characters, made by the server.
Times are seconds since 1970 UTC, as numbers. A night is named by the
rig, such as 2026-10-05.
Units.
Quantity
Unit
Region and footprint centres
Degrees of RA and declination. RA is degrees, not hours.
Region width and height
Degrees of sky, not degrees of the RA coordinate.
Presence RA
Hours, as the rig's own chart shows it.
Star size (HFR) and guiding error
Arcseconds, never pixels.
Image scale
Arcseconds per pixel.
Focal length, pixel size
Millimetres, microns (unbinned).
Bandpass
Nanometres, full width.
Sub length, integration
Seconds. Depth goals are hours.
Moon illumination
0 to 1.
Filter names. Every filter name is folded to one letter wherever it enters:
Letter
Filter
Also written as
L
Luminance
Lum, Luminance, Clear, UV/IR cut, None
R, G, B
Red, green, blue
Red, Green, Blue
H
Hydrogen-alpha
Ha, H-alpha, Halpha, Ha 3nm
O
Oxygen III
OIII, O3, Oxygen
S
Sulphur II
SII, S2, Sulphur
Case, spaces, hyphens and a trailing bandpass ("Ha 3nm") do not matter. A name
that is none of these, such as a dual-band filter, is kept as written. Servers
MUST compare filters by these letters.
Errors are {"detail": ...}. On most errors detail is a sentence to show
the operator. On 422, detail lists each bad field. Clients act on the HTTP
status:
Status
Meaning
What to do
400
The request cannot be served as asked, such as joining before describing the rig.
Do what detail says first.
401
Missing, unknown or revoked token.
Stop and ask the person to sign in or pair again.
403
Not this caller's to do.
Show detail; do not retry.
404
No such thing, or not this caller's.
Check the ID.
409
Conflicts with how things stand, such as a rig that cannot meet a project, or a newer protocol.
Show detail.
422
The body does not match its type.
Fix the program.
503
Not available here, such as sign-in on a server without it.
Use the other way, or try later.
3. Tokens
There are two kinds of token, and neither can do the other's job:
A person token belongs to somebody signed in. It enrols and lists that
person's telescopes. It cannot fetch work or report frames.
A telescope token (agent token) belongs to one telescope. It says hello,
fetches work and reports frames, and nothing else. It never changes a project.
Both travel as Authorization: Bearer <token>.
A telescope gets its token one of two ways. A server MUST offer at least one, and
SHOULD list which in features. A server that sends no features offers sign-in
when GET /api/v1/auth says so; programs treat pairing as offered only when
features lists it.
Sign in, then enrol (signin). The device flow, because capture software is
a desktop program:
The program calls POST /api/v1/auth/login and gets a code and a url.
It opens url in the browser. The person signs in there, with whatever the
server uses; Starfront servers use Discord.
The program polls GET /api/v1/auth/poll?code=... until state is done,
which hands over the person's token once.
With the person token, the program enrols each telescope with
POST /api/v1/agents and gets that telescope's token, shown once.
GET /api/v1/auth says whether sign-in is on. GET /api/v1/auth/me and
POST /api/v1/auth/logout do what they say. A server MAY sign a person out
elsewhere when they sign in on a second machine.
Pairing (pairing). The person issues a single-use code on the server's web
pages and types it into the program, which calls POST /api/v1/pair with the
code and a name for the telescope. It gets the same reply as enrolling. A code
works once and expires within an hour; repeated bad codes MAY get 429.
Programs MUST NOT retry pairing on their own; if the reply is lost, the person
issues a new code.
Programs keep tokens in the system credential store and send them only in the
Authorization header, over HTTPS, to the server's own origin. Servers SHOULD
store only hashes of tokens and SHOULD let people revoke a telescope.
4. Hello
A telescope says what it is with POST /api/v1/agent/hello, before asking for
work and whenever the rig changes. The hello is also its heartbeat: a telescope
counts as online for 25 minutes after its last call.
The rig profile gives:
Optics and sensor: focal length, pixel size, unbinned sensor size,
binning. From these the server works out image scale and field of view.
Filters: each name with its bandpass, or null where it is not known.
Colour: a one-shot colour camera is broadband RGB, whatever is in front of
it.
Rotation: the camera's fixed position angle, or null when a rotator can
set any angle. A fixed camera's tiling follows its real angle.
What it achieves: typical star size and guiding error, in arcseconds.
Sub length per filter: what its darks are built for. Work is dealt at these
lengths, so every frame can be calibrated.
Time: hours per night it gives, and its local window, such as 21:00 to
03:00.
A hello MAY carry presence: where the telescope points, what it is doing, and
what to call it on the group's chart, such as its equipment profile's name.
GET /api/v1/presence shows every telescope seen in the last day, with what it
chose to share, so the group can see who is on the sky.
A hello whose protocol is newer than the server's gets 409.
5. Projects
A project is a region of sky, of one of two kinds:
mosaic: an area to cover. Each telescope tiles the whole region with its
own camera, so telescopes with different fields need not agree on a grid. Each
cell MUST fit the rig's field at the angle it shoots, and the cells SHOULD
cover the region; the exact layout is the server's choice. A fixed camera's
cells follow its reported angle. A camera turned by more than 2° (a half-turn
counts as none) gets its cells cut again at once, because the old ones no
longer fit what it sees. Smaller changes past a plate solve's wobble, a fifth
of a degree, wait for the next night.
single: one object. Every telescope frames it whole, at whatever field it
has; nobody tiles it.
Its goals are depth wanted at every point of the region, per filter, in
hours. Depth is integration time at a point on the sky, which means the same on
a 300 mm refractor and a 2000 mm reflector. Its requirements are the rules
data must meet: focal length or scale, colour cameras allowed or not, star size
and guiding, sub length, filters with the widest bandpass accepted, Moon
illumination and separation, lowest altitude, calibration, and the fewest frames
a visit to one panel is worth.
GET /api/v1/agent/projects lists every open project with whether this
telescope can help, checked against the profile it last sent, rule by rule.
Programs SHOULD show the operator why a rig cannot help before a night is spent
on it. Each listing also shows the accepted hours so far, who is taking part,
and progress per filter: the share of the region at the goal depth, the average
depth against the goal, and the thinnest point.
GET /api/v1/agent/projects/{id}/depth gives the depth map behind those figures:
the region cut into a fine grid, with everybody's accepted seconds on each cell,
per filter. A project is done in a filter when the whole region is at its goal.
POST /api/v1/agent/projects/{id}/join takes a share. A rig that has not said
hello with its focal length, sensor size and pixel size gets 400: the server
cannot cut cells without them. Otherwise the server checks the requirements
against the rig's profile itself and against its sub lengths, and answers 409
with the reason if they are not met. A join MAY carry the night, moon and
moonUp, as asking for tonight does, so the share's first list is tonight's and
made with the Moon in mind. A rig need carry only some of the filters
a project wants, each within its bandpass limit; its share and every night's list
use only those. A rig with none of them cannot help. The share is the whole region tiled with this
rig's camera, at its own sub lengths. Joining is consent: the share arrives
accepted. Joining twice returns the share already held.
6. Tonight
GET /api/v1/agent/task returns every share the telescope holds, each dealt for
tonight:
cells: the rig's tiling of the region.
share: which cells to shoot tonight, in order.
visit: tonight's filter and frames per panel.
version: rises whenever the share changes. A version the program already has
means nothing new.
The program says which night it is in with night, and describes its own sky
with moon (how much of the Moon is lit) and moonUp (the fraction of its dark
hours the Moon is up). The server cannot know where a rig is, so the rig tells it.
Rules for dealing a night:
A list MUST hold for the whole night it was dealt for. Other rigs' frames
arriving at 2 a.m. must not move panels under a rig that is shooting them. The
list is dealt afresh the first time the rig asks in the next night. A rig that
never names its night gets a list held for 20 hours. Nothing moves a list
within its night: changed hours, a newly reported Moon, newcomers, frames and
a changed tiling rule are all considered when the night turns. So a program
can read the deal once at dusk and trust it. The one exception is a fixed
camera turned by more than 2°, whose cells must be cut again (section 5).
A visit to a panel MUST NOT be shorter than the project's minFramesPerVisit
in any filter, because a rig stacks its own frames first.
A rig is dealt only filters it carries.
On a mosaic, a rig SHOULD shoot one filter a night: every panel gets a stack in
that filter, the wheel never turns between panels, and one set of flats
serves the night.
Which filter: under a bright Moon, the red narrowband lines (H, S), which shoot
through moonlight; on a dark night, what cannot be shot any other time (L, R,
G, B, O). Among those, the filter with the most depth still wanted once what
other rigs are putting in tonight is counted.
Which panels, in this order: not where somebody else is tonight; where this rig
has been least, so no patch is one camera's alone; where the field is thinnest.
Panels already at full depth are skipped. The night holds as many visits as
fit.
The reply also carries each project's requirements, so the program can judge its
own data before reporting it.
A share a coordinator pushed by hand arrives offered and stays so until the
program accepts it with POST /api/v1/agent/task/{id}. That route also declines
or finishes a share. Shares from joining need no answer.
Tonight's list is advice. The program and its operator decide when and whether
to shoot it, and the rig's own safety limits always win.
7. Reports and credit
After shooting, the program reports with POST /api/v1/agent/report: one record
per night, filter and panel. Each record gives frames, seconds and sub length,
the panel's solved footprint (where the telescope really pointed), image
scale, focal length, mean star size and guiding error over the night, Moon
illumination and separation, whether the frames are calibrated, the filter's
bandpass, and whether the camera is colour.
The server judges each record against the project's rules and answers with a
verdict per record:
Filter: one the project wants, and no wider than its limit.
Colour camera: allowed, and under the colour Moon limit.
Focal length and scale within limits.
Star size and guiding within limits, in arcseconds.
Sub length within limits.
Moon illumination and separation within limits.
Calibration, when required.
A star-size, guiding or image-scale rule that cannot be checked because the
measurement is missing is listed under unverified: a missing measurement is
not a pass. Other rules are judged only on what the record gives. Verdicts are advisory: a
project's coordinator can overrule one, with the server's own tools, because a
night the numbers reject may be the only data anybody has on that patch of sky.
Accepted records build the project's depth map: each record adds its seconds,
per filter, to the sky its footprint covers. That map drives the next night's
dealing for every rig.
A record for the same telescope, share, night, filter and panel replaces the
earlier one at the larger figure, so sending a report again never counts hours
twice. Programs SHOULD send every unreported panel on each poll and mark a panel
reported only when the server has recorded it.
8. Files (optional)
Core credit is the night report: the server judges the numbers a rig measured
and keeps no images. A project that needs pixels can require files through an
optional extension, offered by servers that list files. It will carry over the
upload, stacked-master and shared-folder design from draft 0.1. It is not part of
this draft's routes yet.
9. Retries
Every call is safe to repeat:
Call
Why a repeat is safe
Hello
It replaces the rig's profile and presence.
Tonight
It returns the list already dealt for that night.
Join
Joining twice returns the share already held.
Report
The same night, filter and panel keep the larger figure.
Accept, decline, finish
Setting the same state again leaves the share in that state.
Sign-in poll
It hands the token over once, then says claimed.
Pair
Never repeated automatically; see section 3.
10. Out of scope
This draft leaves out equipment control, coordinator tools, the depth map as a
picture, federation between servers, payments and image upload (see section 8).
Servers publish their sign-in rules, terms and retention policies themselves.
A small AstroCollab 0.2 server and an example telescope program, for testing
software against the protocol and for reading. Starfront's collaboration server
is the first implementation of 0.2; this is a second one, written from the
protocol alone, so the two can check each other.
Do not run it for real projects. It keeps everything in memory, serves plain
HTTP and has no accounts beyond what its tools make.
Run it
Use Python 3.12 or later with requirements-dev.txt installed. From the
repository root:
python -m reference.server --port 8080
It prints a person token, a pairing code and the sample projects:
AstroCollab reference server on http://127.0.0.1:8080
person token _cwX0HL1l1lLSQDKjaaIB-Y3zJ6mtwE96zKzC-2NpkE
pairing code 9HyJnIk4T5DLbh15
project 11ba95f0c9c5 M31 halo in narrowband
project a0c42f301767 M51 in LRGB
In another terminal, run one night with the example program:
The program pairs, says hello as a 530 mm refractor with narrowband filters,
browses, joins the first project it can help, asks for tonight's list, reports
two panels and reads who is on the sky. It prints each request and its status.
To sign in the way a desktop program does, call POST /api/v1/auth/login,
open the url it returns, press Approve, and poll /api/v1/auth/poll.
Sample projects
M31 halo in narrowband: a 5° × 3° mosaic wanting 10 hours each of H and O
at every point, 7 nm filters or narrower, stars of 3.5″ or better, subs of
120–600 s.
M51 in LRGB: one object wanting 20 hours of L and 5 each of R, G and B,
from focal lengths of 800 mm and up.
From Python
import threading
from reference.server import serve
httpd = serve(port=0) # sample=False starts empty
threading.Thread(target=httpd.serve_forever, daemon=True).start()
base = httpd.base_url # such as http://127.0.0.1:53211
code = httpd.tools.issue_pairing_code("Observer")
httpd.tools does what a real server's web pages and coordinators do:
Tool
Does
sign_in(name)
Returns a person token for name.
approve_login(code, name)
Finishes a device sign-in, as the sign-in page does.
issue_pairing_code(name)
Returns a single-use pairing code for name, good for an hour.
httpd.sample_project_ids maps each sample project's name to its ID.
httpd.state.now can be replaced to move the clock in tests.
How it decides
The rules are in rules.py, each a plain function:
Filter names fold to one letter (L R G B H O S), as the protocol says.
Tiling. A rig tiles a mosaic with its own frame, at the angle it shoots:
its fixed angle, or the project's when a rotator can turn it. The grid is laid
along the camera's axes, large enough to cover the north-up region, with 10%
overlap. A single target is one frame centred on the object. A fixed camera
keeps its cells when its angle moves by 0.2° or less, or by a half-turn,
which frames the same rectangle; a larger turn cuts them again.
Depth is integration time at a point. A cell's depth is the mean, over 25
points spread across it, of the seconds of every accepted footprint covering
each point.
The depth map cuts the region into a north-up grid, the longer side into
16 cells; a single target is one cell. Each accepted record adds its seconds
to each cell in proportion to how much of the cell its footprint covers. A
project whose region has no size gets 409.
Progress per filter, in the depth map and the project listing: the share
of cells at 90% of the goal or more, the mean depth against the goal (each
cell counted up to the goal), and the thinnest cell. A project with no goals
has none.
Presence shows the name a rig gives in its presence, if any, and the name
it was enrolled under as enrolledAs. Project listings name rigs the same
way.
A night's list holds for the night the rig names, or 20 hours if it names
none. A mosaic night is one filter: under a bright Moon (lit fraction times
the fraction of the night it is up, 0.2 or more) H or S, otherwise the rest;
then the filter with the most depth still wanted after what other rigs hold
tonight. Panels are taken least claimed by other rigs tonight first, then
least shot by this rig in any filter, then thinnest. The night holds as many
visits as fit, with 90 s per visit for the slew, each at least
minFramesPerVisit frames and no longer than a panel's full depth. A rig that
has not said how long its night is gets 6 hours.
A single target's night spreads over all its filters, by the depth each
still wants.
Joining needs the rig's focal length, pixel size and sensor size (400
without them) and at least one filter the project wants, within its bandpass
limit (409 with the reason otherwise). The share and every night's list use
only the wanted filters the rig carries.
Judging checks every rule the project sets against what the record gives.
A star-size, guiding or image-scale rule whose measurement is missing goes
under unverified; other rules with nothing to check against are skipped.
Reports for the same telescope, share, night, filter and panel keep the
larger figure.
Pairing answers 429 after five bad codes from one address in a minute.
A night's list never moves once dealt. New hours, a Moon reported for the
first time and other rigs' reports all wait for the next night.
Blank or unreadable numbers in a request are read as unknown, as the protocol
recommends. Everything else is checked against schemas/, and a body that does
not match gets 422 with a list of the bad fields.
The tests check every reply against the schema the contract gives for that
route and status, and cover tokens, sign-in, pairing, hello, presence, joining,
dealing, the held night, judging, duplicate reports, the depth map, progress
and the example program.
These tools check an AstroCollab 0.2 server, or a client, against the
protocol and the contract.
Every body is checked twice: against the OpenAPI document and against the
standalone JSON Schemas. They need Python 3.12 or later
and the packages in requirements-dev.txt.
Test a server
Use a test server, not a live one: the suite enrols telescopes, joins projects
and reports frames.
Before you run it, make two open projects with the server's own tools:
a mosaic that wants a red narrowband filter (H or S) and a dark-night
filter (O, L, R, G or B), sets maxHfr, and covers a region several times the
width of a 530 mm field (about 2.5° by 1.7°);
optionally, a single target.
Then give the suite a way to get two telescope tokens: a signed-in person's
token, which it uses to enrol them, or two unused pairing codes.
Add one more --pairing-code with a person token to check pairing as well.
Write tokens and codes after =, as above: they can start with -.
The suite reads each project's requirements and builds telescopes to fit them.
It names its filters as people do ("Ha", "OIII", "Lum") to check that the
server folds them to letters. It prints one line per check:
PASS bright_moon_deals_red_narrowband [Moon]: dealt H
FAIL a_mosaic_night_is_one_filter [Dealing]: a mosaic night deals ['H', 'O'] filters, not one
SKIP pairing_code_works_once [Tokens]: the server does not list pairing
The bracket names the row in the conformance scenarios.
A check fails if the server answers with the wrong status, or if any response
breaks the contract. The suite exits with status 1 if any check fails.
What the server suite checks
Row
Checks
Discovery
Health says ok and protocol 1. Sign-in status answers; starting sign-in gives a code that polls as pending, or 503 where sign-in is off. A server without features offers sign-in when its status says so.
Tokens
Enrolling or pairing gives each telescope its own token. Where features lists pairing, a used code gets 401. A made-up or missing token gets 401. A person's token cannot fetch work, and a telescope's token cannot enrol or list telescopes.
Browsing
Open projects are listed with kind and compatibility, and with progress for each goal filter, each figure between 0 and 1. A project's depth map covers its region, with one figure per cell for each goal filter; an accepted report raises it; an unknown project gets 404.
Hello
A bare rig cannot help; the same rig described properly can. A newer protocol gets 409.
Joining
A share arrives accepted, tiled with the rig's own field, at the rig's own sub lengths. Joining twice returns the same share. A rig with no optics gets 400; one with none of the wanted filters gets 409 with a reason. A rig with only some of them joins, and is dealt only those, even on a night that would favour another. A single target is one frame, centred on it. A fixed camera's cells follow its angle.
Tonight
Every share comes back. The list holds for the night, even after another rig reports, the rig gives new hours, or the Moon changes. The next night moves the rig to panels it has not shot. Accepting a share keeps it accepted.
Dealing
A mosaic night is one filter. Each visit has at least minFramesPerVisit frames. Two rigs on one mosaic get different panels.
Moon
A bright Moon deals H or S; a dark night deals O, L, R, G or B.
Reports
Verdicts come back in the order sent. A rig cannot report on another rig's share (403).
Judging
A record past maxHfr is rejected with a reason. A missing star size is listed as unverified.
Duplicates
Reporting the same night, filter and panel again is a duplicate, and the accepted hours rise by the larger figure only.
Presence
A telescope that shared where it points shows as online, with what it shared, under the name it gave there; enrolledAs, if sent, is the name it enrolled under.
Errors
Unknown projects and shares get 404 with a sentence. A body that breaks its type gets 422 listing the fields.
Test a client
Put the proxy between the client and a working server, such as the
reference server:
Point the client at http://127.0.0.1:8081 and run its normal night. Press
Ctrl-C to stop the proxy. It prints what the client did wrong (unknown routes,
missing tokens, bodies or queries that break the contract, tokens in URLs)
apart from what the server did wrong, and exits with status 1 if the client
did anything wrong.
In this repository's tests
tests/test_conformance.py runs the suite against two servers:
our reference server, once with a person token and a
pairing code, and once with pairing alone, plus the example client through the
proxy;
Starfront's server, from STARFRONT_DIR or a checkout beside this repository
(../starfront), started under STARFRONT_PYTHON, Starfront's own .venv, or
this Python. It needs FastAPI and uvicorn; without them the test is skipped.
Each server's test projects are made with that server's own tools, which the
protocol leaves out: the reference server's Python tools, and Starfront's
coordinator route with its admin token. The suite itself uses only the
contract's routes.
Where Starfront's server and this draft differ, the check is listed in
STARFRONT_GAPS in the test, so the test follows both as they change. It says
when a listed check starts passing, so the list stays current.
What it cannot check
Coordinator tools. Starting projects, pushing shares and overruling verdicts
are outside the protocol.
The pull order in full. It checks that two rigs avoid each other and that a
rig moves on after a night, not the "thinnest field" order. It checks that the
depth map covers the region and rises, not that each cell's figure is right.
Night length. It does not check how many visits fit a night.
Sign-in in a browser. It starts sign-in and polls once; it cannot finish it.
Revoking telescopes and hashed tokens. The protocol only recommends them.
Files. The optional files extension has no routes yet.
Implementations must pass these scenarios before claiming conformance with this
draft. The conformance tester checks them against a
running server; it runs against both the reference server
and Starfront's server.
Area
Scenario and required result
Discovery
GET /api/v1/health needs no token and gives ok, the protocol number and the server's version. features lists signin or pairing, or both.
Tokens
A person token cannot say hello, fetch work or report; a telescope token cannot enrol or list telescopes. Both get 401. A missing or unknown token gets 401. Pairing, where offered, works once and then fails with 401.
Hello
A hello stores the rig's profile; browsing then judges compatibility from it. A hello with a newer protocol gets 409.
Browsing
GET /api/v1/agent/projects lists every open project with a compatibility check per rule, the hours collected, who takes part and progress per filter. The depth map covers the region with cells and gives seconds per filter for each; accepted reports raise it.
Joining
A rig that meets a project gets an accepted share: the whole region tiled with cells that fit its field, turned to a fixed camera's angle, or one cell for a single project. Joining twice returns the same share. A rig that has not described its optics gets 400; a rig that cannot meet the project, including one with none of the wanted filters, gets 409 with the reason. A rig with only some of the wanted filters can join.
Tonight
Asking twice in the same night returns the same panels and version, even after the rig reports different hours or a different Moon. Asking in the next night deals again. Without a night, a list holds for 20 hours.
Dealing
A rig is dealt only filters it carries. A mosaic night uses one filter. Every visit has at least minFramesPerVisit frames in each filter. Two rigs on one mosaic get different panels on the same night. Panels at full depth are not dealt.
Moon
With a bright Moon up most of the night, a mosaic that wants H and O deals H or S. On a dark night it deals the filter with the most depth still wanted, preferring L, R, G, B and O.
Reports
A report returns one verdict per record, in order. Accepted records add depth where their footprints lie and change later dealing.
Judging
A record outside a rule is rejected with a reason naming it. A star-size, guiding or scale rule that cannot be checked for lack of a measurement is listed under unverified, not passed.
Duplicates
Sending the same telescope, share, night, filter and panel again keeps one record, at the larger figure, and says duplicate.
Presence
A telescope that said hello in the last 25 minutes is listed as online, with only what it chose to share, under the name it gave in its presence.
Errors
Errors are {"detail": ...}. A body that does not match its type gets 422 with a list of bad fields. Unknown fields are kept or ignored, never refused.
Proposed PSF Guard adapter; not implemented. Starfront already implements the
same protocol, and its astrocontrol/collabclient.py is a working client to
read alongside this.
Map (server, project_id) to the existing global Library project and keep the
server's requirements apart from local plans.
Sign the user in with the device flow, or pair with a code, and enrol each
registered rig as its own telescope with its own token. Bind it to the
existing rig and catalog; do not create a second local rig identity.
Say hello from each rig with its optics, sensor, filters and bandpasses, the
sub length each filter is shot at (what its dark library is built for), its
typical star size and guiding in arcseconds, and its hours and window. Convert
TS RA hours to degrees for regions and footprints at the boundary; presence RA
stays in hours.
Ask for tonight from the Director at a safe boundary, naming the night and the
site's Moon. Treat the list as work the server wants, and let the existing
planning core and local safety rules decide when and whether to run it. It
never grants launch authority and never overrides commissioned limits.
Preserve local Director grants, launch ledgers, hardware ownership and safe
execution boundaries. The server reserves nothing.
Keep the share and panel number with every frame.
Report each night, filter and panel from the durable PSF Guard queue: frames,
seconds, the solved footprint, scale, mean HFR in arcseconds, guiding, Moon,
calibration and bandpass. Mark a panel reported only when the server has
recorded it. Never report on the exposure path.
Show the server's verdicts beside local grades; do not overwrite local grades
or reject reasons.
Telescope tokens never authorize local management or database changes.
Keep the Director API and peer database transfer separate. Implement and test
this adapter in PSF Guard.
Adopt the collaboration protocol Starfront already speaks, with its paths, as the
API. Starfront's capture app and server are the first implementation.
Projects are a region of sky, single or mosaic, with depth goals in hours
per filter at every point, and rules in units that mean the same on every rig:
focal length or scale, colour cameras, star size and guiding in arcseconds,
sub length, filters with bandpass limits, Moon limits, lowest altitude and the
fewest frames per visit.
Each telescope tiles a mosaic with its own camera. Shared grids and
per-panel goals are gone; depth is integration time at a point on the sky,
built from the solved footprints of accepted nights.
Tonight's list is dealt once per night the rig names, and held: one filter a
night on a mosaic, chosen by the rig's Moon and what is thinnest; panels by
where others are not tonight, where this rig has been least, and where the
field is thinnest; never fewer than the project's frames per visit.
Credit comes from night reports, judged on what the rig measured. Verdicts
are advisory and list what could not be checked. Reporting again keeps the
larger figure. Image files become an optional extension, still to be written.
Tokens: a person signs in with the device flow and enrols telescopes, or a
telescope pairs with a code. Person and telescope tokens never stand in for each
other.
A night's list holds without exception until the night turns, as Starfront
now does.
Progress and the depth map: project listings give progress per filter, and
GET /api/v1/agent/projects/{id}/depth gives everybody's depth on a fine grid
over the region. A presence may name the telescope for the group's chart.
Joining may carry the night and the Moon, as Starfront now sends, so a
share's first list is tonight's.
Rigs describe their sub length per filter (what their darks are built for),
hours per night and time window, and may share where they point.
Filter names fold to one letter: L, R, G, B, H, O, S. A rig may join with
only some of a project's filters and is dealt only those.
Errors are {"detail": ...}. Objects are open: unknown fields are kept or
ignored, never refused.
Coordinator routes, the 0.1 contributor routes and API keys, the code
catalog, uploads, stacked masters and shared folders leave this draft.
0.1 revisions, superseded by 0.2
These changed draft 0.1 after its first release. Draft 0.2 replaces the 0.1
contributor API that they describe.
List every error code, check-in wait reason and file rejection reason in
spec/codes.md. Servers keep assigning work after goals are met; more data is
welcome. How long a capture report counts is up to each server.
Generate standalone JSON Schemas (schemas/) and a Markdown REST reference
(spec/api.md) from the contract, so clients need no OpenAPI tools.
Check-ins report everything captured but not yet submitted, as per-panel
totals in unsubmitted_captures. Progress shows them as reported_frames;
they steer assignments but earn no credit.
Cut the API to what a contributor's client calls: 15 operations. Project
setup, membership changes, review, manual assessment, retrieval, capacity,
planning policies, activity and snapshot sync move to the server's own tools.
Users join projects on the web; GET /me/projects lists them.
Check-ins return assignments: image, continue or wait. The server decides
what each rig images; there is no proposal, review or automatic-eligibility
step. Recommendation becomes Assignment.
Remove ETags, If-Match, If-None-Match and 304. Remove scopes: a key acts
for its account, optionally limited to some projects.
Rename calibration masters to calibration_frames.
Replace OAuth and participation tokens with API keys. Users create keys on the
server's account pages; one key works on account and project routes, limited
by the participant's role. Remove POST, GET /participations/{id}/tokens,
DELETE /participations/{id}/tokens/{token_id}, their schemas, and the
oauth_issuer, oauth_metadata_url and account_resource capability fields.
Define passband matching, and make every panel of a target need the
objective's full depth. Check-ins report captured frames per panel.
Assessments of masters omit capture IDs.
Give each filter a list of passbands, with optional width, kind, maker and
model, so dual-narrowband filters on color cameras describe both bands.
Objectives and manifests also list passbands.
Make every rig field except name optional, since users may enter details on
the web. Add PATCH /me/equipment/{id} (JSON merge patch). Pairing codes
issued for a rig return its equipment_id. Hand-out uses every advertised rig
capability.
Let an external location point at a shared folder, with path naming the
file, for subs and masters alike.
Make rigs belong to the account. Register once with PUT /me/equipment/{id};
ask for work with POST /me/checkins, and the server picks the project and
panel. Rigs get single panels of the shared picture, not whole mosaics;
Panel.layout places a panel in the target's grid.
Add deliverable to requirements: calibrated_subs or stacked_masters.
Masters carry stack provenance listing every sub; credit counts subs either
way.
Remove jobs: finalize returns the submission, and check-ins return the plan.
Remove POST /projects/{id}/recommendations, GET /jobs/{id}, intents and
live status; adopted plans and shared check-ins replace them.
Collapse scopes to read, contribute and manage. Remove x-scope-rules.
Remove the target and objective list routes and the revision reads for rigs,
capacity and planning policy.
Make snapshot sync an optional feature, sync.
Allow http:// loopback URLs for api_root and account_url. Make
coverage_fraction optional.
Remove Idempotency-Key. Every mutation is safe to retry by design:
client-chosen IDs, one participation per project, target states, If-Match
on shared resources, and same-job finalization. Drop
idempotency_retention_seconds.
Drop preconditions on a participant's own equipment, capacity, planning policy
and intents; PUT creates or replaces them.
Remove POST /uploads/{id}/renew. Each part write extends the session.
Make framing recommendations core. A check-in now needs only equipment and
returns the plan directly with recommendation_ready; unavailable advice is
gone. Capacity and planning policy are optional inputs. A capacity offer only
informs the project team.
Let users join projects on the server's web pages; clients find them with
GET /me/participations.
Add external delivery. Projects may accept files shared outside the API, such
as in Google Drive. Contributors register the file's location and hash;
maintainers record retrieval with
POST /submissions/{id}/artifacts/{artifact_id}/retrieval before assessment.
Add POST /pair. Users issue a single-use pairing code on the account pages;
each client installation trades one for its own API key.
Write the contract in TypeSpec (typespec/). openapi/astrocollab.yaml is now
generated, with examples taken from examples/. The wire contract is otherwise
unchanged.
Add a reference server, an example client and a conformance tester.
0.1.0-draft.1 — 2026-10-03
Extract the collaboration design from PSF Guard into an independent protocol.
Define project publication, participation credentials, equipment and capacity
offers, optional recommendations, nonexclusive intent, snapshot/change sync,
authenticated resumable uploads, versioned assessments and capture credit.
The earlier PSF Guard design's /api/collab/v1 route sketches were not an
implemented interface. This draft uses a discovered API root, illustrated as
https://collab.example/v1. It specifies authenticated chunk uploads rather
than requiring a storage provider's signed-URL protocol.