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 a panel, filter and exposure that suit its field and filters. Upload your calibrated subs, and they go into a stack no single backyard could build.
This is a spec, not an implementation. It defines an API for capture software such as N.I.N.A. to build in, and for servers that host projects. No software supports it yet. The only code here is an example server and client for testing.
DRAFT 0.1.0-draft.1 The specification may change incompatibly.
M31 / H-ALPHA / 412 OF 600 HOURSTONIGHT
For software authors Build it into N.I.N.A. and other capture apps15 REST calls Pair, check in, uploadJSON Schema For every payloadRead the REST reference →
HOW YOU TAKE PART
Pair once. Shoot when it's clear.
You join a project on its website and pair your rig once. On each clear night your capture software asks the server what to shoot, and the server picks the panel with the fewest hours that your rig can frame. You upload your subs when they are ready.
A 2000 mm SCT gets one panel of M31; a 135 mm lens takes the whole galaxy in one frame. A dual-band filter on a color camera can feed the H-alpha and OIII goals at once. As panels fill, the server moves rigs on to whatever is still thin.
Two rigs can shoot the same panel on the same night. Good data from both goes in the stack.
⌁
Every good sub counts
Subs are checked for focus, framing and calibration. Each accepted sub counts once toward your hours, whether you send it alone or inside your own master.
↗
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.
A project is one deep image that many astrophotographers shoot together, such
as a 600-hour H-alpha mosaic of M31. It says which targets to shoot, through
which filters, and how many hours each panel needs. Everyone shoots on their own
nights with their own gear. At dusk each rig asks the server what to shoot and
gets the panel that most needs data and fits its field and filters. The subs
come back, the server checks them, and the good ones go into the stack.
The server never moves your mount and never reserves a target. Two people can
shoot the same panel on the same night; good data from both counts.
This is a draft specification for capture software, such as N.I.N.A., to build
in. No software supports it yet. This page follows one rig from pairing to
counted subs, and is written for people adding the API to their software. Each step
shows the request and the important part of the response. Full payloads are in
examples/, and the reference server
runs every step on your machine, with sample projects.
The flow
Step
Where
Request
1. Join a project and get a pairing code
Server's web pages
None
2. Pair the client
Client
POST /pair
3. Describe the rig
Client
PUT /me/equipment/{rig}
4. Ask what to image
Client
POST /me/checkins
5. Capture
Your own software
None
6. Upload
Client
POST /projects/{id}/submissions, PUT /uploads/{id}/parts/{n}, POST /submissions/{id}/finalize
7. Read the result
Client
GET /submissions/{id}, GET /projects/{id}/progress
Steps 1 to 3 happen once per rig. Steps 4 to 7 repeat each night. The examples
use these shell variables:
API=https://collab.example/v1 # the server's API root, from GET /capabilities
KEY=... # the API key from step 2
INSTALLATION=$(uuidgen) # made once per installation, then kept
RIG=$(uuidgen) # made once per rig, then kept
1. Join a project and get a pairing code
The user does this in a browser. The server's account_url from
GET /capabilities leads to its pages. There the user picks a project, accepts
its terms, and issues a pairing code for the rig. Servers should let the user do
both in one step on the project page.
A project says what it wants back:
Calibrated subs: each calibrated exposure as its own file. The project
stacks everything.
Stacked masters: you stack your own subs and send the master, with a list
of the subs inside it.
The client finds the projects you joined with GET /me/projects.
2. Pair the client
The user enters the code in the client. The client trades it for its own API
key. This request needs no Authorization header:
The key appears only in this response. Store it in the system credential store
and send it on every other request. See Authentication.
3. Describe the rig
The server hands out work by what each rig can do: its field of view, sampling,
filters, color or mono sensor, and, if given, where it is. The user may enter
some of this on the server's web pages when setting the rig up; a pairing code
issued for that rig returns its equipment_id. The client can fill in the rest.
To register a new rig, or replace its description:
The example describes a 6248×4176 mono camera with 3.76 µm pixels on a 400 mm
lens, with an H-alpha filter: about 3.4°×2.2° of sky. To change only some fields
and keep the rest, such as filters entered on the web, send a merge patch:
Each filter lists its passbands. A dual-narrowband filter on a color camera
lists two, H-alpha and OIII, so one night can serve two objectives.
Color rig example.
Each panel says where to point, which filter to use, how long each exposure
should be and how many to take. Full response.
You will rarely get a whole mosaic. When a target is bigger than your field, the
server splits it into a grid and gives you the panel that most needs data;
layout says which column and row it is. Other rigs get other panels. An
assignment may list a few panels in order: move to the next when the current one
has its suggested frames. Later check-ins may send you to another panel or target
as the picture fills in.
action
What to do
image
Work through the panels in assignment, in order.
continue
Keep working on your current assignment.
wait
Nothing suits this rig now. reason_codes say why, such as rig_incomplete.
Check in again after next_checkin_seconds. Send the assignment you are working
on as assignment_id, and report what you have captured but not yet submitted
as unsubmitted_captures: totals per panel, replacing your last report. You
don't have to upload to show progress. The server counts reported frames when it
hands out work, which matters most in a masters project, where subs wait until
there are enough to stack. There is nothing to accept or decline: your own software still
decides when and whether to point the rig, within its own safety limits.
5. Capture
Capture and calibrate with your usual software. Keep the recommendation and
panel IDs with each frame; the manifest refers to them. For a masters project,
stack your subs once you have enough for the project's master_rules.
6. Upload
Upload happens in three requests: describe, send bytes, finish.
Describe. Send a manifest with an id you choose. It lists each file with
its hash, size, exposure and calibration history.
Example with subs;
example with a master.
Progress counts subs and seconds, whether they arrived alone or inside masters.
It keeps accepted data apart from assigned frames, frames reported but not yet
submitted, and uploads still in review. Only accepted data counts toward a goal.
Sharing files outside the API
Some projects accept files shared through a service such as Google Drive. The
project's requirements then include external_delivery, with instructions.
Share the file as told, then send the usual manifest with two extra fields on
each artifact:
There is nothing to upload. Finalize as usual. A maintainer downloads the file,
checks it against your hash and records the result. Assessment then proceeds as
for uploaded files. Example manifest.
Rules for every request
Rule
What to do
Base URL
Join every path to api_root from /capabilities.
Credentials
Send Authorization: Bearer <key>. Never put a key in a URL or log.
Bodies
Send and expect JSON, except upload parts, which are raw bytes. Servers reject unknown fields.
Retries
Every request is safe to repeat as is. Creates use an id you choose, so a retry finds the record.
202 Accepted
Work continues on the server. Read the resource again later.
Version 0.1.0-draft.1. Paths are relative to the API root from GET /capabilities, for example https://collab.example/v1.
Send Authorization: Bearer <api key> where a key is required. Bodies are JSON
unless stated. Errors use application/problem+json; act on code. Every type
links to a standalone JSON Schema, so you can validate
payloads without OpenAPI tools. The protocol gives the rules
behind each route, and codes lists every error code and reason.
Takes no Authorization header. A code works once and expires within an hour. An unknown, used or expired code returns 401 invalid_pairing_code. Do not retry automatically; if the response is lost, issue a new code.
Change only the fields sent, using JSON merge patch (RFC 7396): null removes a field. Lets a rig report what it knows without erasing what the user entered on the web.
The server picks the project and panel where this rig adds most, using everything the rig advertises, and assigns it. Repeating a check-in is safe and creates nothing new.
The API reports three kinds of codes: error codes on failed requests, reasons a
check-in says wait, and reasons the server rejects a file. Clients act on the
code, not on the wording of detail.
Servers use these codes for these cases. They MAY add codes of their own. A
client that meets an unknown error code acts on the HTTP status; an unknown
wait or rejection reason is shown to the user as is.
Error codes
Failed requests return application/problem+json
with status and code. Invalid input also lists field errors, each with a
JSON pointer.
400 Bad request
Code
Meaning
What to do
invalid_json
The body is not valid JSON.
Fix the client.
invalid_cursor
The list cursor is unknown or expired.
Start the list again without a cursor.
invalid_limit
limit is outside 1–100.
Use a limit in range.
401 Unauthorized
Code
Meaning
What to do
authentication_required
No API key was sent.
Send Authorization: Bearer <api key>.
invalid_credentials
The key is unknown, expired or revoked.
Stop and ask the user to pair again.
invalid_pairing_code
The pairing code is unknown, used or expired.
Ask the user for a new code. Do not retry on your own.
403 Forbidden
Code
Meaning
What to do
membership_inactive
The account is not an active member of this project, or the key is limited to other projects.
Stop work for this project. The user can check membership on the web.
404 Not found
Code
Meaning
What to do
not_found
No such resource, or this account may not see it.
Check the ID.
405 Method not allowed
Code
Meaning
What to do
method_not_allowed
The route exists but not with this method.
Fix the client.
409 Conflict
Code
Meaning
What to do
id_conflict
This submission id already exists with a different body.
Use a new ID for a new submission.
part_conflict
This part was already received with different bytes.
Check the file; it changed after upload began.
upload_incomplete
Finalize was called before every part arrived.
Read GET /uploads/{id} and send the missing parts.
upload_expired
The upload session expired while idle.
Submit again under a new ID.
upload_finalized
The submission is already finalized; parts cannot change.
Nothing to do; read the submission.
terms_consent_required
The project's terms changed.
Ask the user to accept them on the web, then finalize again. Staged bytes are kept.
submission_deadline_passed
The project no longer accepts submissions for this revision.
Stop submitting to this project.
413 Content too large
Code
Meaning
What to do
payload_too_large
The JSON body exceeds max_json_bytes.
Split the manifest into smaller submissions.
too_many_artifacts
The manifest lists more than max_artifacts_per_submission files.
Split the manifest.
artifact_too_large
A file exceeds max_artifact_bytes.
The project cannot take this file.
image_too_large
A decoded image exceeds max_decoded_pixels.
The project cannot take this file.
422 Invalid data
Code
Meaning
What to do
invalid_request
The body does not match the schema. errors points at each bad field.
Fix the data.
invalid_reference
An ID refers to something unknown: a rig, filter, objective, assignment or panel.
Check the IDs against the project and your rigs.
invalid_range
A minimum is greater than its maximum, or a time range runs backward.
Fix the range.
invalid_revision
The project revision is unknown.
Read the project and use its revision.
duplicate_id
Two artifacts in one manifest share an ID.
Give each artifact its own ID.
invalid_supersede
supersedes_artifact_id names a file that is not yours or has another capture identity.
Fix the reference.
invalid_part_number
The part number is outside 1 to part_count.
Fix the client.
part_size_mismatch
A part is not part_size_bytes long, or the last part has the wrong remainder.
Split the file as the session says.
digest_mismatch
A part's bytes do not match X-Part-SHA256. The part was not recorded.
Send the part again.
capture_deadline_passed
A frame started after the project's capture deadline.
Leave it out.
deliverable_mismatch
A sub sent to a masters project, or a master to a subs project.
Send what the project's deliverable asks for.
too_few_subs
A master has fewer subs than master_rules.min_sub_count.
Stack more subs first.
invalid_stack
A master's sub_count, sub list or integration do not agree.
Fix the stack block.
drizzle_not_allowed
A drizzled master went to a project that does not accept drizzle.
Send an undrizzled master.
external_delivery_not_accepted
The project does not accept shared files from this provider.
Upload the file instead.
429 and 5xx
Status
Code
Meaning
What to do
429
rate_limited
Too many requests.
Wait Retry-After seconds.
500
internal_error
The server failed.
Retry the same request later.
503
unavailable
The server is down for a while.
Retry the same request later.
Check-in wait reasons
A check-in that returns wait lists why in reason_codes.
Reason
Meaning
What to do
rig_incomplete
The rig lacks what planning needs: sensor size, pixel size, focal length, color state or a filter.
Fill in the rig, on the web or with PATCH.
no_active_projects
The account is not an active member of any project the key covers.
Join a project on the web.
no_matching_filter
No filter on the rig serves any objective.
Check the rig's filters and passbands.
sampling_out_of_range
The rig's sampling suits no objective.
Try other binning or optics.
color_state_mismatch
The projects want mono data and the rig is color, or the reverse.
Use another rig.
target_too_low
No target rises high enough at the rig's site.
Check again later in the season.
goals_met
The project has closed after meeting its goals.
Nothing to do here.
While a project is open, servers SHOULD keep assigning useful work rather than
wait: more data improves the picture. A rig may get a panel other rigs also
hold, or a panel that has already met its goal; data past a goal is surplus,
credited to the contributor.
File rejection reasons
When the server rejects a file, its result in GET /submissions/{id} lists why
in reason_codes.
Reason
Meaning
digest_mismatch
The file's bytes do not match its manifest hash.
bandpass_mismatch
The file's passbands serve none of the objectives it names.
exposure_out_of_range
The exposure is outside the objective's range.
sampling_out_of_range
The image's sampling is outside the processing group's range.
coverage_too_low
The frame covers too little of the panel it names.
fresh_solve_missing
The objective needs a fresh solve of the submitted pixels and none was given.
required_measurement_missing
A required quality measurement is missing.
quality_limit
A quality measurement is outside the objective's limits.
duplicate_capture
A sub in this file was already credited, alone or in another master.
external_unavailable
A shared file could not be fetched.
A replaced file shows the state superseded, not a rejection.
Full response.
4. Store the key in the system credential store, not a plain config file.
The server never shows it again.
5. Check it:
sh
curl -H "Authorization: Bearer $KEY" $API/me/projects
200 means the key works and lists the user's projects. 401 means the key
is wrong, expired or revoked.
6. Use it on every route. The same key lists projects, registers rigs, asks
for work and uploads frames.
Make installation_id a random UUID when the client is installed, and keep it.
Pairing again with the same ID replaces that installation's old key, so a user
can re-pair a rig without leaving stale keys behind.
A code works once and expires within an hour. If pairing fails with
401 invalid_pairing_code, ask the user for a new code. Never retry pairing on
your own: if the response was lost, the user issues a new code and revokes the
orphan key on the account pages.
Some servers also let users copy a key from the account pages. Treat a pasted
key the same way: store it, check it, use it.
What a key can do
A key acts for its account. It can do everything a contributor does: list the
account's projects, register rigs, check in and submit data. When issuing the
pairing code, the user may limit the key to some projects or set an expiry.
On project routes the server also checks membership: only active members of a
project receive assignments for it and submit data to it. A key limited to other
projects gets 403 or 404.
Server checklist
To support API keys, a server needs:
web pages where users issue pairing codes and list and revoke keys;
POST /pair, which consumes a code and creates a key in one transaction, and
revokes any earlier key for the same account and installation;
at least 128 bits of randomness in each code and key, stored only as hashes;
codes that work once and expire within an hour, and a 429 after repeated
bad codes;
for each key: account, client name, installation, optional project list,
optional expiry, optional rig, last use;
on each request: look up the key by hash; reject unknown, expired or revoked
keys with 401; then check membership.
This page walks through every example payload in order. For a shorter tour with
requests, read How the API works. The examples use synthetic data
and placeholder keys and hashes. The example index
maps each file to its schema, and python -m reference.client runs the same flow
against the reference server.
Discover the server and pair
Capabilities give the API root, the
account pages and the limits, such as the 8 MiB largest upload part.
The user joins a project and issues a pairing code on the server's web pages.
The client trades the code for its
API key, shown only once. This key is
limited to one project.
Read the project
GET /me/projects lists the projects
the account has joined. The project holds
its current requirements: 120 accepted H-alpha frames of 300–600 seconds each,
at least 36,000 seconds in all, over the M31 footprint, with sampling,
calibration, a fresh pixel solve, an optional FWHM limit and deadlines. Accepted
data must meet both the frame and the integration goal, on every panel.
Describe the rig
Register the rig once with
PUT /me/equipment/{id}: a 6248×4176 mono sensor with 3.76 µm pixels, a 400 mm
focal length and a 3 nm H-alpha filter, about 3.36°×2.25° of sky. The
response is the saved rig. A
merge patch changes only the fields
it names; the result has a new
revision and keeps everything else. A color rig
lists two passbands, H-alpha and OIII, on its dual-narrowband filter.
Ask what to image
Check in with the rig, the assignment it is
working on and what it has captured but not yet submitted: 12 frames, one hour,
on its panel. The
response carries an
assignment:
one panel covering the whole target, H-alpha, 96 exposures of 300 seconds;
9.6 estimated rig-hours, including acquisition overhead;
6.4 estimated hours of accepted integration after quality assessment.
The 800 mm rig sees half that field. In this
project the target needs two panels at that
scale, and other 800 mm rigs share the same grid. This
assignment gives the rig both panels, in
order, because both need data: 48 exposures each, with about 23% overlap.
layout places each in the grid. A rig with too little described gets
wait with rig_incomplete.
Upload calibrated subs
Submit a manifest after calibration.
It lists a 300-second exposure with its capture ID, dark and flat hashes, pixel
unit, file hashes and fresh solve, and names the assignment and panel. Raw
frames and calibration frames stay local.
The response opens an upload
session for 104,371,200 bytes: twelve parts of 8,388,608 bytes and a last part
of 3,707,904 bytes. Send raw bytes with Content-Length and X-Part-SHA256; the
server checks each part and returns a receipt.
After an interruption, read the received parts
and resume.
When every part has arrived, finalize. The server returns the
submission in processing while
it assesses the files; read it again until it is
complete. Here the frame is accepted
and credits one frame and 300 seconds. If the terms changed, finalization
returns terms_consent_required until
the user accepts them on the web.
Progress keeps accepted data apart from
assigned, reported and pending frames. A recalibrated file
uses a new artifact ID and supersedes_artifact_id but keeps its capture
identity, so acceptance replaces the earlier credit rather than adding to it.
Send stacked masters
A project that wants stacked masters
gets one file per stack. The manifest
lists the 24 subs in the master and how they were stacked. Acceptance credits
24 subs and 7,200 seconds.
Share files outside the API
A project that accepts external delivery
lets contributors share files through a service such as Google Drive. The
manifest points at a shared folder and
names each file with path, in place of an upload. The project fetches the file,
checks its hash and then assesses it as usual.
Errors
Errors share one shape. Examples: an invalid body
with field pointers, a reused submission ID with a different body
(id_conflict), different bytes for a received
part (part_conflict) and a
rate limit.
MUST and MUST NOT mark requirements. SHOULD marks a recommendation; MAY marks
an option. The OpenAPI contract defines payload
structure. This document defines the behavior that schemas cannot check.
1. What the API covers
A project describes one picture that many contributors build together. The API
is what a contributor's client calls: pair a rig, describe it, ask what to
image, and send the results. It has 15 operations.
Everything else belongs to the server and its own tools: signup, joining
projects, setting requirements, reviewing members, assessing data by hand,
moderation and billing. This protocol says how the server must behave toward
contributors, not how owners run it.
The server never controls equipment and never reserves a target. The client's
own software decides when and whether to point a rig.
2. Conventions
Clients start from an API root and call public GET /capabilities. It gives the
server ID, API root, account pages (account_url), supported versions, optional
features and limits. Routes are relative to the API root. Servers use HTTPS;
http:// is allowed only on loopback addresses, for local development.
Resources have opaque UUIDs. A capture is identified by (origin_id,
capture_id): the producer picks a persistent origin UUID, and the client keeps
both IDs when it submits, recalibrates or stacks the capture. Hashes detect
identical bytes; they do not prove who made them.
Quantity
Convention
Coordinates
ICRS; RA in degrees [0, 360), Dec in degrees [-90, 90].
Footprint
Tangent-plane rectangle centered on the stated coordinates; width and height in degrees. At position angle zero, width runs east-west and height north-south.
Position angle
Degrees east of celestial north, measured along the height axis.
Sampling
Arcseconds per pixel.
Wavelength
Nanometers.
Focal length / pixel size
Millimeters / micrometers.
Duration / size
Seconds / bytes.
Timestamp
RFC 3339 UTC with Z.
Bodies are JSON, except upload parts, which are raw bytes. Omit unknown values
rather than sending zero. Servers reject unknown request fields; clients ignore
unknown response fields. Lists take an opaque cursor and limit (1–100,
default 50) and return next_cursor, null on the last page.
Errors use application/problem+json
with a stable code, the HTTP status, a short detail, a request ID and, for
invalid input, field errors. Clients act on code. Codes lists
every error code, check-in wait reason and file rejection reason.
Status
Meaning
401
Missing, unknown, expired or revoked key.
403
The account may not do this, for example it is not an active member.
404
Not found, or not visible to this account.
409
Conflict with current state, such as id_conflict or upload_incomplete.
413
Input exceeds a size limit.
422
Invalid or incompatible data.
429
Too many requests; wait Retry-After seconds.
503
Temporarily unavailable; retry later.
Servers limit JSON size, part size, decoded pixels, storage and compute, and
check actual input, not declared sizes. They reject client paths, executable
metadata and requests to fetch arbitrary URLs.
3. Pairing and API keys
Every private request carries Authorization: Bearer <api key>. Each client
installation gets its own key by pairing:
On the server's web pages, the user issues a pairing code. The user may limit
the key to some projects, set an expiry, or issue the code for a rig already
set up on the web.
The user enters the code in the client.
The client calls POST /pair with the code, a persistent installation_id
and a client_name. It sends no Authorization header.
The server returns the key once, with Cache-Control: no-store, and the rig's
equipment_id when the code was issued for a rig.
A code works once and expires within one hour. The server consumes the code and
creates the key in one transaction. Pairing again with the same
installation_id revokes that installation's old key. An unknown, used or
expired code returns 401 invalid_pairing_code; repeated failures return 429.
Clients MUST NOT retry pairing on their own. If the response is lost, the user
issues a new code and revokes the orphan key. Servers MAY also let users copy a
key from the web pages; every server MUST support pairing.
A key acts for its account, limited to its projects if the user chose some.
Servers store only hashes of codes and keys, each with at least 128 bits of
randomness, and let users list and revoke keys. They check the key and the
account's membership on every request.
Clients store keys in the system credential store and send them only in the
Authorization header, over HTTPS, to the server's own origin. Keys never go in
URLs, manifests or logs.
4. Projects and membership
Users join projects on the server's web pages, where they accept the project's
terms. GET /me/projects lists the projects the account has joined, with its
membership state. Only active members receive assignments and submit data.
When a client learns that membership is no longer active, it stops starting new
work for that project.
GET /projects/{id} returns the project's current requirements and their
revision, which rises whenever the owners publish changes. Submissions name the
revision their frames were taken for. A later revision MUST NOT change how data
taken for an earlier one is judged.
Requirements contain:
Targets: each with a footprint on the sky.
Objectives: for a target, the accepted passbands, exposure range,
processing group, minimum coverage, quality rules and goal. The goal, in frames,
seconds of integration or both, is the depth every part of the target needs.
Processing groups: sampling range, color state and calibration steps.
Terms, deadlines and deliverable:calibrated_subs (each calibrated
exposure as its own file) or stacked_masters (masters the contributor stacks,
with master_rules).
external_delivery, when the project accepts files shared outside the API.
Captures must start before capture_deadline; submissions must be finalized
before submission_deadline. Both bounds are exclusive.
5. Rigs
A rig belongs to the account and serves every project it joins. The user may set
it up on the web, the client may register it with PUT /me/equipment/{id}, or
both. PATCH with a JSON merge patch changes only the fields sent, so a rig can
report its focal length without erasing filters entered on the web.
Only the name is required. To plan for a rig, the server needs the unbinned
sensor size, pixel size, focal length, color state and at least one filter.
Optional fields include binning, camera and telescope names, rotation and an
approximate site. Field of view depends on sensor size, pixel size and focal
length; sampling also depends on binning.
Each filter has a stable ID and a list of passbands, each with a name, center
wavelength and, when known, width. A narrowband or luminance filter has one
passband; a dual-narrowband filter on a color camera has two. Filters may also
give their kind, maker and model.
A changed description creates a new revision; an identical one does not. Servers
keep every revision that an assignment or submission cites.
6. Assignments
Asking for work
A rig asks what to image with POST /me/checkins, naming its equipment ID. It
may limit the choice to some projects and name the assignment it is working on.
The server answers with one of three actions:
Action
Meaning
image
assignment holds new work. Start it at a safe point.
continue
Keep working on the current assignment.
wait
Nothing suits this rig now. reason_codes say why; see codes.
Each answer gives next_checkin_seconds. The server decides; there is no
proposal for the client to accept or reject. A repeated check-in creates nothing
new.
Reporting progress before upload
A rig may capture for days before it submits, and in a masters project subs wait
until there are enough to stack. A check-in reports this with
unsubmitted_captures: for each panel, the frames and integration captured but
not yet submitted, and when the last one was taken. Each report is a total that
replaces the rig's previous one, so repeating it changes nothing. A rig sends
[] once everything is submitted.
The server counts reported frames when it hands out panels, so it does not send
more rigs to a panel whose data is already on its way, and shows them in
progress as reported_frames. Reported frames earn no credit; only submitted,
accepted data does. How long a report counts without a submission is up to each
server.
How the server assigns work
The server picks the project and panel where the rig adds most, using what the
rig advertises:
Rig capability
Used for
Field of view
Panel size: whole targets for wide rigs, single panels for long ones.
Sampling
Objectives whose sampling range the rig meets.
Filters and passbands
Objectives the rig can serve. A dual-band filter can serve two objectives in the same frames.
Color state
Objectives whose processing group accepts mono or color data.
Rotation and angle
Panel angle: fixed cameras keep their angle.
Site, when given
Targets that rise high enough there.
A filter serves an objective when one of its passbands matches one of the
objective's accepted passbands: its center lies inside the accepted band and,
when both widths are known, its width is between a quarter of the accepted width
and the full accepted width. When the accepted band gives no width, centers must
agree within 5 nm. So a 3 nm H-alpha filter serves a 7 nm H-alpha objective, but
a narrowband filter does not serve a 300 nm luminance objective.
For a target larger than a rig's field, the server lays a grid of panels over it.
Each panel needs the objective's full goal, and the objective is complete when
every panel is. The server tracks depth per panel and hands each rig the panels
that most need data, so no rig has to build a whole mosaic. Rigs with similar
fields SHOULD share a grid, but a rig only gets panels that fit its own field.
Panel.layout gives a panel's column and row.
An assignment usually holds one panel. It may list a few, in order, when the
first will be done before the night ends; the rig moves on when the current
panel has its suggested frames. Each panel gives where to point, the filter, the
exposure, the suggested frame count and the objectives it serves. Every panel
MUST fit the rig's field, use one of its filters, and meet the objective's
sampling and exposure rules.
Assignments reserve nothing. Two rigs may get overlapping panels, and useful data
from both counts. More data is always welcome: while a project is open, servers
SHOULD keep assigning useful work rather than answer wait, even for a panel
other rigs already hold or one that has met its goal. Data past a goal is
surplus, credited to the contributor.
Client duties
Clients check each assignment against their own safety limits before use. They
stop starting new frames for an assignment after its expires_at, never change
an exposure already in progress, and keep the assignment and panel IDs with each
capture.
7. Submissions
Manifest
A submission is an immutable manifest for one project, with a client-chosen
id, the project revision and up to 100 artifacts. An artifact is one file:
Calibrated sub (kind: calibrated_sub): one exposure, with its capture
identity, raw and calibrated hashes, rig revision, filter and passbands,
exposure, capture time, calibration history and measurements.
Stacked master (kind: stacked_master): a registered, linear master with a
stack block listing every sub in it by capture identity, time and hash, the
sub count and total integration, and how the subs were registered, normalized,
rejected and weighted. All subs share one exposure length.
Calibration history records each calibration frame's role and hash, the
operations, parameters and software versions. Darks and flats are never sent.
A fresh_pixel_solve must solve the submitted pixels; headers and predicted
coordinates do not count.
Servers reject an artifact whose kind does not match the project's
deliverable with 422 deliverable_mismatch. A master needs at least
master_rules.min_sub_count subs (422 too_few_subs), a sub_count equal to
its list and an integration equal to sub_count × exposure_seconds
(422 invalid_stack), and allow_drizzle if drizzled
(422 drizzle_not_allowed).
Upload
Each artifact arrives one of two ways, for subs and masters alike:
upload: the server returns an upload session with a fixed part size. The
client sends PUT /uploads/{id}/parts/{n} with the raw bytes,
Content-Length and lowercase hex X-Part-SHA256. Parts are numbered from 1;
all but the last have exactly part_size_bytes. Parts may arrive in any order.
The server checks size and hash and records each part atomically: the same
bytes return the same receipt, different bytes for a received part return
409 part_conflict, and a bad hash returns 422 digest_mismatch without
recording the part. GET /uploads/{id} lists received parts. Each accepted part
extends the session; an idle session expires after the staging period, and the
client then submits again under a new ID.
external: the file is shared outside the API, if the project's
external_delivery lists the provider (otherwise
422 external_delivery_not_accepted). The location gives the provider, an
HTTPS link and the share time. The link may point at a shared folder, with
path naming the file inside it. There is no upload session; the artifact
waits in awaiting_retrieval until the project fetches the file and checks its
hash. A server fetches files itself only over HTTPS from hosts it lists in
external_retrieval_hosts. Links may grant access: only the submitter and the
project's maintainers may see them.
POST /submissions/{id}/finalize checks that every upload is complete
(409 upload_incomplete otherwise) and starts assessment. It returns 202 with
the submission in processing. Clients read GET /submissions/{id} until it is
complete; each artifact then shows accepted or rejected with reason codes
and the credit it earned. One bad file does not hold up the rest.
At creation, part writes and finalization, servers check membership, current
terms consent, deadlines and quota. If the terms changed, finalization returns
409 terms_consent_required until the user accepts them on the web; staged
bytes are kept.
8. Assessment and credit
The server assesses each artifact against the requirements of the revision it
names: hashes, passbands, sampling, exposure, coverage of its panel, required
evidence and quality rules. Missing required evidence fails; missing optional
evidence does not. Only accepted artifacts earn credit. Maintainers may also
assess by hand with the server's own tools, under the same rules.
Credit counts subs and seconds of integration. An accepted sub credits one frame
and its exposure; an accepted master credits its sub count and integration. A
sub earns credit once per project, whether alone or inside a master: a master
that repeats a credited sub is rejected with duplicate_capture. Assessment
judges a master's pixels as a whole; the sub list is the contributor's claim.
One frame may credit several objectives when its filter serves them all.
To replace a file, submit a new artifact ID with supersedes_artifact_id and the
same capture identity; acceptance replaces the old credit, rejection keeps it.
Frames accepted after a panel's goal is met count as surplus: credited to the
contributor, not to the goal.
GET /projects/{id}/progress shows each objective's goal and its assigned,
reported, pending, accepted, rejected and surplus frames, and whether every panel has the
goal's depth. Only accepted data counts toward a goal.
9. Retries
Every request is safe to repeat as is. There are no idempotency keys and no
preconditions.
Request
Why a repeat is safe
Create a submission
The client picks the id. The same id and body returns the existing submission with 200; a different body returns 409 id_conflict.
PUT and PATCH a rig
They set the same values again; no new revision.
Upload a part
The same bytes return the same receipt.
Finalize
Returns the same submission.
Check in
Creates nothing new; a capture report replaces the last one.
Pair
Never repeated automatically; see section 3.
10. Out of scope
This draft leaves out equipment control, central scheduling, federation, raw
image upload, dataset downloads, payments, a required quality algorithm, and the
owner and maintainer tools for running projects. Servers publish their signup
rules, quotas, terms, assessment methods and retention policies.
This is an example for testing and for implementers to read, not a server to
run for real projects. The protocol is meant to be built into existing capture
software, such as N.I.N.A., and into servers that host projects.
A small in-memory server and a walkthrough client for the AstroCollab
contributor API. Read them to see how the rules in
the protocol fit together. Do not deploy the server: it
keeps all state in memory, serves plain HTTP and has no web pages.
The server loads the contract at start. It uses
it to route requests and validate request bodies, so the Python code holds only
the rules a schema cannot express. For each route's fields and errors, see the
REST reference; for payload shapes, the JSON Schemas in
schemas/.
Run it
Use Python 3.12 or later and install requirements-dev.txt (PyYAML and
jsonschema). From the repository root:
python -m reference.server --port 8080
With no options, the server creates the accounts alice and bob, prints an
API key and a pairing code for each, loads the sample projects
and makes both accounts active members. To choose the secrets:
--key creates an account with an API key. --pairing-code issues a
single-use code that a client trades for a key at POST /pair; codes expire
after an hour. Every account named either way joins the sample projects.
--join ACCOUNT=PROJECT adds a membership. --check-responses validates every
response against the contract, --verbose logs requests and --no-sample
skips the sample projects.
Use --pairing-code instead of --key to pair first. The client lists your
projects, describes a rig, checks in, submits one exposure for the panel it was
given, finalizes, polls the submission, reports frames not yet submitted and
reads progress:
# Ask what to image
POST /me/checkins -> 200 Check in; the server assigns a panel.
Rig 400 mm: image North America and Pelican nebulae, 1 panel 3.4°×2.2°, H-alpha, 180 × 300 s
…
# Report frames not yet submitted, and keep going
POST /me/checkins -> 200 Check in with 5 frames waiting.
action: continue
GET /projects/…/progress -> 200 Read the totals.
accepted 1 frames (300 s), 5 reported, goal 144 frames on each panel
The manifest comes from examples; the client fills in the IDs
the server handed out.
Sample projects
At start the server creates three projects from reference/sample_project.json.
Each entry is a project ID and a RequirementSet.
"Sample sky survey" takes calibrated subs. Its targets differ in size so rigs
with different focal lengths get different work:
Target
Size
Bands
Sampling
North America and Pelican nebulae
3°×2°
H-alpha, OIII (mono and one-shot color)
1.4–4″/px
Veil Nebula complex
3°×3°
OIII, H-alpha
1.4–4″/px
M31
3°×1°, angle 35°
Luminance
1.4–4″/px
M42
1°×1°
H-alpha, short luminance
0.3–1″/px
M51
0.2°×0.15°
Luminance
0.3–1″/px
NGC 7662
0.05°
OIII
0.3–1″/px
"Sample masters" takes stacked masters of the Heart Nebula, the Rosette
Nebula and M33: at least 10 subs per master, no drizzle.
"Sample shared files" takes calibrated luminance subs of the Pleiades shared
through Google Drive or an HTTPS link (external delivery) instead of uploads.
Rig 400 mm: image North America and Pelican nebulae, 1 panel 3.4°×2.2°, H-alpha, 180 × 300 s
Rig 2000 mm A: image M42 Orion Nebula, panel 1 of 6 (column 1, row 1), H-alpha, 300 × 120 s
Rig 2000 mm B: image M42 Orion Nebula, panel 2 of 6 (column 2, row 1), H-alpha, 300 × 120 s
Each rig uses a 6248×4176 mono camera with 3.76 µm pixels and H-alpha, OIII
and luminance filters: one on a 400 mm f/5 refractor, two on 2000 mm SCTs. The
two long rigs get different panels of the same mosaic.
How the server assigns work
POST /me/checkins names one rig. The server considers the account's active
projects (narrowed by the key's projects and the request's project_ids),
plans for each, and assigns the work with the largest remaining deficit. The
rules live in planning.py:
The rig must state sensor size, pixel size, focal length, color state and
at least one filter. Otherwise the answer is wait with rig_incomplete.
Binning defaults to 1, rotation to fixed at 0°.
Field of view per axis is 2·atan(pixels × pixel size ÷ (2 × focal length)),
from unbinned pixels. Sampling is 206.265 × pixel µm × binning ÷ focal mm.
A filter serves an objective when one of its passbands matches one the
objective accepts: the center lies inside the accepted band and, when both
widths are known, the filter's width is between a quarter of the accepted
width and the full accepted width. With no accepted width, centers must be
within 5 nm. So narrowband filters do not serve luminance goals.
The objective must also match the rig's color state and sampling, and,
when the rig gives a site latitude, its target must rise above 30°
(90° − |latitude − declination|).
The project owns each mosaic. A target can have several grids of panels
with 15% overlap, one per panel size. A rig works on the grid with the
largest panels that still fit its field; if none fits, the server lays a
new grid sized to that rig. No rig gets a panel larger than its field. A
target that fits a field with 10% to spare gets one panel. Fixed cameras
keep their angle: the grid covers the target as that camera sees it.
Each panel needs the objective's full goal. The objective is complete when
every panel of one grid is. Frames that name no panel count toward the
whole target only until a grid exists.
The rig gets the panel that needs the most frames, after subtracting other
rigs' live assignments and their reported, unsubmitted frames. One panel is
the usual assignment; the next is added only if the first would finish
within a 6-hour night (at most 3).
More data is always good. While a project is open, a rig whose only options
have met their goals, or whose panels other rigs already hold, still gets
work: the least-deep panel, for one night. Frames past a goal are surplus,
credited to the contributor.
Exposure is the objective's minimum. Suggested frames close the panel's
deficit, assuming 80% pass. Estimates add 20% overhead.
A dual-band filter serves several objectives at once: the panel lists every
objective on the same target and processing group that the filter covers,
and an accepted frame is credited to each.
When nothing fits, the answer is wait with a reason from
spec/codes.md: rig_incomplete, no_active_projects,
no_matching_filter, sampling_out_of_range, color_state_mismatch or
target_too_low. goals_met means the project has closed after meeting
its goals.
Check-ins are safe to repeat. Unchanged work comes back as the same
assignment; once the rig names it in assignment_id, the answer is
continue. unsubmitted_captures is a total per panel that replaces the
rig's last report, so a repeat changes nothing. Reported frames show in
progress as reported_frames and steer other rigs away from that panel; they
earn no credit. Frames leave the report when a submission names their panel,
and reports stop counting after the submission deadline.
Submissions and credit
Uploads: fixed-size parts, part hashes, out-of-order parts,
part_conflict and upload_incomplete. Each accepted part extends the
upload's expiry. Finalizing returns the submission in processing; poll it
until complete. A repeated create with the same ID and body returns 200
with the submission; a different body returns 409 id_conflict.
Codes: errors, wait reasons and rejection reasons follow
spec/codes.md. A key limited to other projects, or an
inactive membership, gets 403 membership_inactive.
Automatic assessment: the server checks the file hash, passbands,
exposure range, the rig's sampling against the processing group,
fresh-solve evidence and the manifest's measurements. When
the frame names a panel and carries a solve, the solved center must cover
the panel by at least the objective's minimum_coverage_fraction
(coverage_too_low).
Credit: accepted frames count toward the panel they name. Once a panel
has the full goal, more frames there are surplus: accepted, but with no
credited_frames.
Deliverables: a project takes calibrated subs or stacked masters, not
both (422 deliverable_mismatch). A master needs sub_count equal to its
subs and at least the minimum (422 too_few_subs), integration equal to
subs × exposure (±1 s) and first and last times that match its subs
(422 invalid_stack), and no drizzle unless allowed
(422 drizzle_not_allowed). An accepted master credits sub_count frames.
Unique captures: each capture, alone or inside a master, earns credit
once per project. A later artifact with a credited capture is rejected with
reason duplicate_capture in its result; the assess_artifact tool
refuses it with 409 duplicate_capture.
External delivery: when a project revision lists providers, an artifact
may point to a shared file (url, optional path). It gets no upload
session and waits in awaiting_retrieval until a maintainer records the
fetch with record_retrieval.
Server tools
A real server manages projects, members and reviews in its own web pages. This
one exposes them as methods on httpd.api:
import threading
from reference.server import serve
httpd = serve(port=0, keys={"alice": "ALICE_SECRET"}, check_responses=True)
threading.Thread(target=httpd.serve_forever, daemon=True).start()
api = httpd.api
project_id = api.create_project(requirements) # Publish revision 1; state open.
api.publish(project_id, new_requirements) # Publish the next revision.
api.join("bob", project_id) # Active member; consents to current terms.
code = api.issue_pairing_code("bob", equipment_id=rig) # As the account pages would.
api.assess_artifact(artifact_id, "rejected", ["quality_limit"])
api.record_retrieval(artifact_id, "verified", sha256_hex, size_bytes)
issue_pairing_code(account, code=None, project_ids=None, key_ttl=None,
equipment_id=None) takes the choices a user makes when issuing a code: a
project list, a key lifetime and the rig the code is for. serve() joins every
account in keys to the sample projects; httpd.sample_project_ids lists them.
When a project's terms change, call join again to record consent; until then
submissions get 409 terms_consent_required.
What it leaves out
No image decoding or quality measurement: the server trusts the manifest's
measurements and solve, so it never returns 413 image_too_large.
No fetching: it never opens external URLs and publishes no
external_retrieval_hosts.
No web pages, signup or key list. Use --key, --pairing-code, --join or
the server tools.
No HTTPS. The contract allows http:// only on loopback hosts; real servers
must use HTTPS.
No visibility windows beyond the latitude check: no time of night, moon or
horizon. No expiry for stale unsubmitted_captures reports.
No quotas, rate limits or upload staging cleanup, and no persistence.
The tests start the server on a free port, run the walkthrough, check in rigs
against the sample projects, and probe keys, rigs, uploads, coverage, masters,
external delivery and the server tools. The server checks every response
against the contract during the tests.
These tools check an AstroCollab server or client against the
OpenAPI contract and the
conformance scenarios. They need Python 3.12 or later
and the packages in requirements-dev.txt.
Test a server
The API serves contributors only, so the suite cannot create projects. Before
you run it, prepare on a test server:
an open project that takes calibrated subs;
optionally, an open project that takes stacked masters, and one that accepts
external delivery;
one or two accounts that are active members of those projects, each with an
API key.
The suite registers rigs and uploads synthetic frames. Do not run it against a
live service.
The suite reads each project's requirements and builds rigs and manifests to
match its first objective. Checks that need an optional flag are skipped
without it. To test pairing, add --pairing-code CODE with an unused code from
the server's account pages, and --second-pairing-code CODE for the same
account. Add --allow-http-loopback for a local server on http://127.0.0.1.
Add --schemas schemas to validate bodies against the standalone JSON Schemas
instead of the OpenAPI components; the results should be the same. The proxy
takes the same option.
The suite prints one line per check:
PASS repeated_put_keeps_revision [Rigs]
FAIL changed_part_conflicts [Multipart]: PUT /uploads/.../parts/1 returned 200, expected 409 part_conflict
SKIP submissions_hidden_from_others [Privacy]: needs --second-participant-key
The bracket names the row in spec/conformance.md. A check fails if the server
returns the wrong status or code, or if any response breaks the contract. The
suite exits with status 1 if any check fails.
The suite and the proxy also warn about any problem code, check-in wait
reason or file rejection reason missing from spec/codes.md.
Servers may add codes, so these are WARN lines, not failures.
What the checks cover
Credentials./capabilities is public. The key is an active
member of every project named. Missing and unknown keys get 401. A
submission to a project the key does not cover fails.
Pairing. A code yields a working key, with Cache-Control: no-store, and
then fails with invalid_pairing_code. Pairing the same installation again
revokes the old key.
Rigs. An identical PUT keeps the revision; a change makes a new one. A
PATCH changes one field and keeps the rest. A rig with only a name gets
wait with reason rig_incomplete.
Privacy. Another account cannot see the rig or read the submission.
Asking for work. A new rig checks in with only its ID and the time, and
gets image or wait. Every panel must fit the rig's field of view (2%
tolerance, in either orientation), name the rig and one of its filters, keep
exposures within the objective's range and suit its sampling. The filter must
serve each listed objective by the passband rule in protocol §6: a passband's
center lies inside an accepted band, and its width is between ¼ and 1× the
accepted width when both are known. Without an accepted width, centers must
agree within 5 nm.
Sharing out the picture. When the project's target is larger than a
long rig's field, two identical rigs must get different panels. Each panel
must still fit.
Reporting progress. A rig reports 5 unsubmitted frames for its assigned
panel; reported_frames rises by 5 for each objective the panel lists. The
same report again changes nothing, and [] returns it to where it was.
Reports never change accepted frames or integration.
Retry. A repeated submission with the same ID and body returns the same
submission with 200; a changed body gets 409 id_conflict. Finalizing
twice returns the same submission.
Multipart. Parts sent out of order are all listed. An identical retry
returns the same receipt. Changed bytes get part_conflict, a wrong digest
gets digest_mismatch, and finalizing early gets upload_incomplete.
Recalibration. An accepted replacement for a capture leaves the project's
credited captures and integration unchanged.
Stacked masters. A sub sent to the masters project, or a master sent to
the subs project, gets deliverable_mismatch. A master below min_sub_count
gets too_few_subs or is rejected, and earns no credit. An accepted master
adds one accepted frame per sub. A master that reuses a credited sub earns
nothing new.
External delivery. A project without external_delivery rejects
external artifacts. On the external project, an external artifact gets no
upload session and stays awaiting_retrieval after finalize, with no credit.
Its link does not appear in public views or to another member.
Test a client
Run the proxy between the client and a working server, such as the reference
server:
Point the client at http://127.0.0.1:8081/v1 and run its normal workflow.
Press Ctrl-C to stop the proxy. It prints client faults and server faults
separately and exits with status 1 if it found client faults.
The proxy reports a client fault when a request:
uses an unknown route, method or query parameter;
lacks Authorization or another required header;
sends a body that breaks its schema, or a part whose length or SHA-256 is wrong;
puts a credential in the URL.
It rewrites api_root in /capabilities so the client keeps using the proxy.
What these tools do not check
Black-box tests cannot see everything. These rows of spec/conformance.md
need implementation tests of their own:
Terms and deadlines: projects and memberships are managed outside the API.
No locks, No useful work and Client duties: these depend on two clients
uploading at once, changing demand and local client behavior.
Geometry and Quality: these need real images and sky coverage.
Partial batches and Attribution.
The suite covers parts of other rows. For Finalize races and Retry it repeats
requests in sequence, not at the same moment. For Asking for work and Sharing
out the picture it checks that panels fit, use valid settings and differ
between rigs, not that the server chose the best framing. For Stacked masters
it cannot check that a master's pixels combine the subs it lists. For External
delivery it stops at awaiting_retrieval; recording retrieval happens outside
the API. For Abuse it only checks that unknown fields are rejected.
The proxy sees one client's traffic. It cannot tell whether the client applies
framing at a safe boundary, keeps local captures after losing access, or stores
keys safely.
Implementations must pass these scenarios before claiming conformance. The
conformance tester checks many rows against a live
server; its README lists the rows it cannot check.
Area
Scenario and required result
Pairing
A code yields one working key and then fails with 401, including when two clients race. Expired codes fail. The response has Cache-Control: no-store. Pairing the same installation again revokes the old key. Repeated bad codes return 429.
Credentials
Unknown, expired and revoked keys fail with 401 on every private route. A key limited to one project fails on another. Accounts that are not active members get no assignments for a project and cannot submit to it.
Privacy
A contributor cannot read another account's rigs, check-ins or submissions. External links are visible only to the submitter and the project's maintainers.
Rigs
PUT with an identical body keeps the revision; a change creates one. PATCH changes only the fields sent. A rig missing planning fields gets wait with rig_incomplete.
Asking for work
A check-in with only a rig ID returns image or wait. Each panel fits the rig's field, uses one of its filters, and meets the objective's sampling and exposure rules. A passband matches only under the protocol's rule: narrowband filters do not serve luminance objectives.
Sharing out the picture
For a target larger than the rig's field, rigs get panels from a grid, not whole mosaics. Every panel needs the goal's full depth. Two similar rigs get different panels when that spreads coverage. Later check-ins move a rig as panels fill.
No locks
Two rigs given overlapping panels both upload useful data. Neither excludes the other; credit follows assessment.
No useful work
Check-in returns wait or continue with a retry interval. A failed planning attempt leaves the current assignment in force until it expires.
Client duties
A new assignment changes only future frames. A capture underway keeps its assignment and panel. Clients stop starting frames after expires_at.
Geometry
Check RA wrap, polar fields, rectangle orientation and coverage against the coordinate conventions.
Quality
Missing optional FWHM is not a failure. Missing a required fresh solve cannot pass. Embedded WCS and target coordinates do not substitute for submitted-pixel evidence.
Reporting progress
A check-in's unsubmitted_captures replaces the rig's last report. Reported frames appear in progress as reported_frames, steer later assignments, and never earn credit. [] clears the report.
Retry
Repeating a submission with the same id and body returns the existing one with 200; a changed body returns 409 id_conflict. Finalizing twice and repeated check-ins create nothing new.
Multipart
Parts arrive out of order; a lost receipt and a repeated identical part do not duplicate bytes. Different content for a received part conflicts. Validate final-part size and actual digests.
External delivery
A project without external_delivery rejects external artifacts. External artifacts get no upload session and wait for retrieval. A hash mismatch after retrieval rejects the artifact. Credit follows only after verification and assessment.
Partial batches
One corrupt artifact reports its own failure. Other artifacts are still assessed. No credit exists before assessment.
Finalize races
Two clients finalize the same submission at once and get one assessment.
Recalibration
Rejecting a replacement keeps the earlier credit. Accepting it replaces that credit.
Stacked masters
A sub sent to a masters project, or a master to a subs project, fails with 422 deliverable_mismatch. A master below min_sub_count, with a sub count that differs from its list, or with mismatched integration earns no credit. An accepted master credits its sub count once; a master repeating a credited sub is rejected with duplicate_capture.
Attribution
The same file under different contributors triggers review without showing one contributor's manifest to the other.
Terms and deadlines
Finalization requires current terms consent. Deadlines are exclusive. A new requirements revision does not change how earlier data is judged.
Abuse
Oversized JSON or images, decompression bombs, malicious paths, arbitrary fetch URLs and cross-project IDs cannot bypass checks or resource limits.
Rejection fixtures test structural constraints.
Implementations must separately test range ordering, referenced IDs, hashes,
unique capture credit and spherical coverage.
Map (server_id, project_id) to the existing global Library project and keep
remote requirement revisions apart from local plans.
Pair each registered rig with the server, so each has its own key. Bind it to
the existing rig and catalog; do not create a second local rig identity.
Convert TS RA hours to ICRS degrees at the boundary.
Register each rig's sensor, optics and filters with PUT /me/equipment/{id},
and use PATCH when local settings change, keeping what the user entered on
the server's web pages.
Check in from the Director at safe boundaries. Treat an assignment as work the
server wants, then let the existing planning core and local safety rules
decide when and whether to run it. An assignment never grants launch authority
and never overrides commissioned limits.
Preserve local Director grants, launch ledgers, hardware ownership and safe
execution boundaries. Assignments reserve nothing.
Keep the assignment and panel IDs with every capture.
Read FITS/XISF through image_io; preserve physical pixel units. Generate
calibrated subs, or stacked masters when the project asks for them, with
source, calibration-frame and algorithm fingerprints beneath the database
slug. Publish files atomically and keep raw files unchanged.
Prefer a durable PSF Guard submission queue. A Director background worker may
submit through the same API once calibration exists, using the rig's key and
the same capture identity. Never upload on the exposure path.
Show the server's results beside local grades; do not overwrite local grades
or reject reasons when a project rejects or no longer needs a frame.
API keys never authorize local management or database changes.
Keep the Director API and peer database transfer separate. Implement and test
this adapter in PSF Guard.
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.