REST API

For direct HTTP access without the Python client. All endpoints are under https://campfire.hollisakins.com/api/v1/.

Authentication#

All requests require an Authorization header:

bash
# Using API key
curl -H "Authorization: Bearer sk_your_api_key" \
  https://campfire.hollisakins.com/api/v1/objects

# Using JWT access token (from device flow)
curl -H "Authorization: Bearer eyJ..." \
  https://campfire.hollisakins.com/api/v1/objects

Data Endpoints#

GET /objects#

Query objects (cross-program grouped sky positions) with filters.

Query Parameters:

ParameterTypeDescription
programsstringComma-separated program slugs
fieldsstringComma-separated field names
gratingsstringComma-separated grating types
observationsstringComma-separated observation names
redshift_minfloatMinimum redshift
redshift_maxfloatMaximum redshift
redshift_qualitystringComma-separated quality codes
max_snr_minfloatMinimum max SNR
max_snr_maxfloatMaximum max SNR
listsstringComma-separated tag slugs (e.g., lrd,blagn)
inspected_onlybooleanFilter to inspected objects
has_photometrybooleanFilter to objects with photometry
searchstringText search on object_id / member target_ids
rafloatRA for cone search (degrees)
decfloatDec for cone search (degrees)
radiusfloatSearch radius (arcsec)
limitintPage size, 1–10000 (default: 1000)
cursorstringOpaque cursor from the previous page's pagination.next_cursor (see Pagination)
countbooleanForce the total count on (true) or off (false). Default: counted on the first page, skipped on cursor pages
offsetintDeprecated positional offset; answered with a Deprecation header
sortstringSort column (object_id, ra, dec, redshift, redshift_quality, field, n_targets, n_spectra, max_snr, max_exposure_time, photo_z, distance)
sort_dirstring'asc' or 'desc'

Response:

json
{
  "data": [
    {
      "object_id": "CAMPFIRE-J095850.12+021234.5",
      "ra": 34.1234,
      "dec": -5.4567,
      "redshift": 2.345,
      "redshift_quality": 3,
      "field": "COSMOS",
      "member_targets": [
        {"target_id": "ember_cosmos_p1_12345", "program_slug": "ember-cosmos", "observation": "ember_cosmos_p1", "redshift_auto": 2.345}
      ]
    }
  ],
  "pagination": {"total": 1500, "limit": 1000, "offset": 0, "has_more": true, "next_cursor": "eyJ2IjoxLC..."}
}

GET /spectra/list#

Flat list of spectra (one row per spectrum) with filters.

Same vocabulary as /objects plus per-spectrum filters (dq_flags_include_any, dq_flags_include_all, dq_flags_exclude). Each row carries spectrum_id, target_id, object_id, grating, fits_path, signal_to_noise, exposure_time, redshift_auto, dq_flags. Sort columns: target_id, spectrum_id, field, observation, program_slug, ra, dec, redshift, redshift_quality, redshift_auto, signal_to_noise, exposure_time, grating, distance. Pagination is identical to /objects.

Pagination#

/objects and /spectra/list page with an opaque cursor. Every response carries pagination.next_cursor; pass it back as cursor= with the same filters and sort to get the next page. next_cursor is null on the last page, so a walk needs no total and no trailing empty request:

bash
URL="https://campfire.hollisakins.com/api/v1/objects?fields=cosmos&limit=1000"
CURSOR=""
while :; do
  PAGE=$(curl -s -H "Authorization: Bearer $TOKEN" "$URL${CURSOR:+&cursor=$CURSOR}")
  echo "$PAGE" | jq -c '.data[]'
  CURSOR=$(echo "$PAGE" | jq -r '.pagination.next_cursor // empty')
  [ -z "$CURSOR" ] && break
done

Each page costs the same regardless of how deep into the result set it is: the server seeks to the cursor position instead of re-reading and discarding every earlier row. The cursor encodes the position under one specific filter set and sort; sending it with different filters or a different sort/sort_dir is a 400, not a wrong page. Cursors are not long-lived tokens — mint them from the previous response, do not store them.

pagination.total is the exact count of matching rows. Because it is a second full pass over the filter, it is computed on the first (cursor-less) page and skipped on cursor pages, where it is -1; count=true forces it, count=false skips it on a first page you don't need it for.

offset= is still accepted for one client release. Offset pages are served with Deprecation: true and a Link: <…>; rel="deprecation" header, cost more the deeper they go, and cannot be combined with cursor. The Python client's iter_objects() / iter_spectra() walk with cursors since v0.5.0.

GET /spectra#

Get a signed URL for downloading a FITS file.

ParameterTypeDescription
pathstringFITS file path (required)

Response: {"url": "https://..."}

GET /storage/download#

Download any product by its storage key — the primitive behind the NIRCam bulk-download script generated on each field page. Authorizes the key under your program access, then answers with a 302 to a fresh presigned URL on the object store, so a plain curl -L fetches the bytes directly. Ask for each file as you go: the link is minted at request time, so a long download never outlives it. Byte-range requests pass through to the store, so curl -C - resumes a partial file.

ParameterTypeDescription
keystringStorage key, e.g. data/products/nircam/cosmos/f444w/mosaic_..._sci.fits.gz (required)
redirectstringfalse to get {"url": "https://...", "expires_in": 21600} instead of the redirect
bash
curl -L -C - -o mosaic_sci.fits.gz \
  -H "Authorization: Bearer sk_your_api_key" \
  "https://campfire.hollisakins.com/api/v1/storage/download?key=data%2Fproducts%2Fnircam%2Fcosmos%2Ff444w%2Fmosaic_..._sci.fits.gz"

Keys you may not read, and keys that do not exist, both answer 404. This is also the only endpoint that accepts the download token embedded in a generated NIRCam bulk-download script: a 30-day credential that can download what your account can download and nothing else (the script's CAMPFIRE_API_KEY override uses an ordinary API key instead). For bulk transfers prefer campfire pull (the CLI) — it plans against the local mirror and skips what you already have.

GET /spectrum#

Get spectrum JSON data for plotting.

ParameterTypeDescription
spectrum_idstringStable per-spectrum identifier

Response:

json
{
  "wave": [1.0, 1.1, ...],
  "fnu": [0.5, 0.6, ...],
  "fnu_err": [0.1, 0.1, ...],
  "snr_2d": [[...], [...]],
  "n_spatial": 10,
  "n_wave": 500,
  "profile": [...],
  "profile_fit": [...],
  "profile_pix": [...]
}

GET /redshift-fit#

Get redshift fitting results.

ParameterTypeDescription
target_idstringTarget ID
gratingstringGrating type

Response:

json
{
  "redshift": 2.345,
  "chi2_min": 1.23,
  "confidence": 95.5,
  "z_grid": [0.0, 0.1, ...],
  "chi2_grid": [100, 95, ...],
  "model_wave": [...],
  "model_fnu": [...]
}

GET /metadata#

Get available filter options.

Response:

json
{
  "programs": [
    {"slug": "ember-uds", "program_name": "EMBER-UDS", "pi_name": "...", "is_public": false}
  ],
  "fields": ["COSMOS", "UDS"],
  "gratings": ["PRISM", "G395M"],
  "observations": ["ember_uds_p4"]
}

GET /observations#

List observations with stats.

Response:

json
{
  "observations": [
    {
      "observation": "ember_uds_p4",
      "program_name": "EMBER-UDS",
      "field": "UDS",
      "target_count": 450,
      "spectrum_count": 1350,
      "total_size_bytes": 2147483648
    }
  ]
}

Auth Endpoints#

GET /auth/whoami#

Get current user info. Response: {"user_id": "uuid", "email": "...", "full_name": "..."}

POST /auth/device#

Initiate device flow authorization (for CLI).

Response:

json
{
  "device_code": "...",
  "user_code": "WDJB-MJPQ",
  "verification_uri": "https://campfire.hollisakins.com/cli-auth",
  "verification_uri_complete": "https://campfire.hollisakins.com/cli-auth?code=WDJB-MJPQ",
  "expires_in": 900,
  "interval": 5
}

POST /auth/device/token#

Poll for tokens after user authorization.

Request: {"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": "..."}

Response (success): {"access_token": "...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "..."}

Response (pending): {"error": "authorization_pending"}

POST /auth/refresh#

Refresh an access token.

Request: {"grant_type": "refresh_token", "refresh_token": "..."}

Response: {"access_token": "...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "..."}


Error Codes#

CodeDescription
200Success
400Invalid parameters
401Invalid/missing authentication
403No access to resource
404Not found
429Rate limited
500Server error

Rate Limits#

  • Standard: 100 requests/minute
  • Burst: Up to 10 concurrent requests