API Reference

MitraSETI Cloud REST API

Programmatic access to the MitraSETI processing pipeline. Upload observation files, start analysis jobs, download results in multiple formats, and manage API keys — all via HTTP.

Tier: Researcher and above Auth: API Key or Cognito JWT Base URL: https://api-dev.deepfieldlabs.dev

Endpoints

Authentication

All API endpoints (except GET /health) require authentication via one of two methods:

Option 1: API Key (Recommended for programmatic access)

Generate an API key from the Dashboard → API Keys tab. Pass it as the x-api-key header:

curl -H "x-api-key: sk_live_xxxxxxxxxxxx" \ https://api-dev.deepfieldlabs.dev/jobs

Option 2: JWT Token (Used by the web dashboard)

Obtained from Cognito after sign-in. Passed as Authorization: Bearer <id_token>:

curl -H "Authorization: Bearer eyJraWQi..." \ https://api-dev.deepfieldlabs.dev/jobs
API Key Expiration: Keys expire after 90 days by default (configurable 1–365 days). Expired keys return 403 Forbidden. You can view expiration dates and generate new keys from the Dashboard.

Quick Start

A complete workflow from upload to results download in 5 steps:

# Set your API key API_KEY="sk_live_xxxxxxxxxxxx" API="https://api-dev.deepfieldlabs.dev" AUTH="-H x-api-key:$API_KEY" # 1. Get presigned upload URL UPLOAD=$(curl -s -X POST "$API/upload-url" $AUTH \ -H "Content-Type: application/json" \ -d '{"filename":"observation.fil","file_size":46000000}') UPLOAD_URL=$(echo $UPLOAD | python3 -c "import sys,json; print(json.load(sys.stdin)['upload_url'])") FILE_KEY=$(echo $UPLOAD | python3 -c "import sys,json; print(json.load(sys.stdin)['file_key'])") # 2. Upload file to S3 curl -X PUT "$UPLOAD_URL" -H "Content-Type: application/octet-stream" \ --data-binary @observation.fil # 3. Submit for analysis JOB=$(curl -s -X POST "$API/analyze" $AUTH \ -H "Content-Type: application/json" \ -d "{\"file_key\":\"$FILE_KEY\",\"filename\":\"observation.fil\"}") JOB_ID=$(echo $JOB | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])") echo "Job submitted: $JOB_ID" # 4. Poll for completion while true; do STATUS=$(curl -s "$API/jobs/$JOB_ID" $AUTH | python3 -c \ "import sys,json; print(json.load(sys.stdin)['status'])") echo "Status: $STATUS" [ "$STATUS" = "complete" ] || [ "$STATUS" = "failed" ] && break sleep 10 done # 5. Download results curl -s "$API/jobs/$JOB_ID/download?format=json" $AUTH | python3 -c \ "import sys,json; print(json.load(sys.stdin)['download_url'])"

Endpoints

GET/health
Health check. No authentication required.

Response (200)

{ "status": "healthy", "version": "0.3.0" }
POST/upload-urlAuth Required
Generate a presigned S3 URL for uploading an observation file. For files > 50 MB, returns multipart upload URLs.

Request Body

FieldTypeRequiredDescription
filenamestringYesName of the file (must end in .fil or .h5)
file_sizeintegerNoFile size in bytes. If provided and > 50 MB, multipart upload is used.

Response — Single Upload (file ≤ 50 MB)

{ "upload_url": "https://mitraseti-dev-data.s3.amazonaws.com/uploads/...", "file_key": "uploads/user-id/uuid/observation.fil", "expires_in": 3600, "max_size_mb": 1024, "multipart": false }

Response — Multipart Upload (file > 50 MB)

{ "multipart": true, "upload_id": "abc123...", "file_key": "uploads/user-id/uuid/large_file.fil", "parts": [ { "partNumber": 1, "url": "https://...presigned-url-for-part-1..." }, { "partNumber": 2, "url": "https://...presigned-url-for-part-2..." } ], "part_size": 10485760, "max_size_mb": 1024, "expires_in": 3600 }
Multipart uploads: For files > 50 MB, the response includes presigned URLs for each 10 MB part. Upload each part with a PUT request, then call POST /complete-upload with the ETags. See Complete Multipart Upload.

Errors

CodeReason
400Missing filename or unsupported file type
403Missing or invalid authentication
POST/complete-uploadAuth Required
Complete a multipart upload after all parts have been uploaded. Required for files > 50 MB.

Request Body

FieldTypeRequiredDescription
file_keystringYesThe file_key from the upload-url response
upload_idstringYesThe upload_id from the upload-url response
partsarrayYesArray of {"partNumber": 1, "etag": "\"abc123\""} from each part upload response

Response (200)

{ "message": "Upload complete", "file_key": "uploads/user-id/uuid/large_file.fil" }

Example: Multipart upload with curl

# Upload part 1 ETAG1=$(curl -s -X PUT "$PART1_URL" --data-binary @part1.bin \ -D - -o /dev/null | grep -i etag | awk '{print $2}') # Upload part 2 ETAG2=$(curl -s -X PUT "$PART2_URL" --data-binary @part2.bin \ -D - -o /dev/null | grep -i etag | awk '{print $2}') # Complete upload curl -X POST "$API/complete-upload" $AUTH \ -H "Content-Type: application/json" \ -d "{ \"file_key\": \"$FILE_KEY\", \"upload_id\": \"$UPLOAD_ID\", \"parts\": [ {\"partNumber\": 1, \"etag\": $ETAG1}, {\"partNumber\": 2, \"etag\": $ETAG2} ] }"
POST/abort-uploadAuth Required
Abort an incomplete multipart upload. Call this to clean up if the upload fails or is cancelled.

Request Body

FieldTypeRequiredDescription
file_keystringYesThe file_key from the upload-url response
upload_idstringYesThe upload_id from the upload-url response

Response (200)

{ "message": "Upload aborted" }
POST/analyzeAuth Required
Start the processing pipeline for an uploaded file. Returns a job ID for status polling.

Request Body

FieldTypeRequiredDescription
file_keystringYesS3 key from the /upload-url response
filenamestringNoOriginal filename (used for display)
source_namestringNoFriendly name for the observation (defaults to filename)

Response (202)

{ "job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "submitted", "message": "Processing started. Poll GET /jobs/{job_id} for status.", "status_url": "/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890" }

Errors

CodeReason
400Missing file_key or invalid JSON body
404File not found in S3 (upload first)
413File exceeds tier size limit
429Monthly job limit reached, or too many concurrent jobs (max 5)
500Pipeline failed to start
GET/jobs/{job_id}Auth Required
Get job status and results. Full results are included when status is complete.

Response (200) — Completed Job

{ "job_id": "a1b2c3d4-...", "status": "complete", "source_name": "Voyager1_trimmed", "filename": "Voyager1_trimmed.fil", "file_size": 46137344, "created_at": 1712444400, "completed_at": 1712444430, "results": { "raw_hits": 543, "filtered_hits": 163, "candidates": 20, "processing_time_seconds": 28.4, "top_candidates": [ { "frequency_hz": 1420405751.68, "drift_rate": 0.15, "snr": 14.2, "classification": "NARROWBAND_DRIFTING", "interestingness": 0.82, "rfi_source": "", "source": "matched_filter" } ] } }

Status Values

StatusDescription
submittedJob accepted, pipeline starting
processingPipeline executing (ingest → de-Doppler → classify → export)
completeAnalysis finished, results available
failedPipeline encountered an error
Export artifacts timing: When status becomes complete, the core results are ready. However, export files (catalog, FITS, waterfall) are generated asynchronously and may take 10–30 seconds after completion to become available for download.
GET/jobsAuth Required
List all jobs for the authenticated user, ordered by most recent first.

Query Parameters

ParamTypeDefaultDescription
limitint50Maximum number of jobs to return (max 100)

Response (200)

[ { "job_id": "a1b2c3d4-...", "status": "complete", "source_name": "Voyager1_trimmed.fil", "filename": "Voyager1_trimmed.fil", "file_size": 46137344, "created_at": 1712444400, "completed_at": 1712444430, "results": { ... } }, { "job_id": "b2c3d4e5-...", "status": "submitted", "source_name": "GJ1002.fil", "created_at": 1712444500 } ]
Job history: Jobs submitted via API also appear in the web dashboard and vice versa. History is limited per tier (Free: 10 jobs, Researcher: 50, Institution: 200, Enterprise: 500). Oldest completed jobs are automatically pruned when the limit is exceeded.
GET/jobs/{job_id}/downloadAuth Required
Download results for a completed job. Returns a presigned URL for binary formats, or inline content for CSV.

Query Parameters

ParamTypeDefaultDescription
formatstringjsonOutput format (see table below)

Available Formats

FormatContentDescription
jsonresults.jsonFull pipeline output with raw hits, filtered hits, candidates, processing time
csvCSV textSignal catalog as CSV (returned inline, not as a presigned URL)
catalog_jsoncatalog.jsonClassified signal entries in JSON format
classifiedclassified.jsonML-classified signals with class labels and confidence
fitscatalog.fitsFITS binary table (compatible with TOPCAT, DS9, astropy)
waterfallwaterfall.pngFrequency vs time spectrogram visualization
waterfall_thumbwaterfall_thumb.pngThumbnail version of the waterfall spectrogram

Response — Presigned URL (json, catalog_json, classified, fits, waterfall)

{ "download_url": "https://mitraseti-dev-results.s3.amazonaws.com/jobs/...", "format": "json", "filename": "results.json" }

Response — Inline CSV

source_name,frequency_hz,drift_rate,snr,classification,interestingness,... Voyager1,1420405751.68,0.15,14.2,NARROWBAND_DRIFTING,0.82,... Voyager1,1420405889.12,-0.08,8.7,RFI_TERRESTRIAL,0.15,... ...

Example: Download all formats

# Download JSON results curl -s "$API/jobs/$JOB_ID/download?format=json" $AUTH \ | python3 -c "import sys,json; import urllib.request; \ urllib.request.urlretrieve(json.load(sys.stdin)['download_url'], 'results.json')" # Download CSV (returned inline) curl -s "$API/jobs/$JOB_ID/download?format=csv" $AUTH > signals.csv # Download waterfall spectrogram curl -s "$API/jobs/$JOB_ID/download?format=waterfall" $AUTH \ | python3 -c "import sys,json; import urllib.request; \ urllib.request.urlretrieve(json.load(sys.stdin)['download_url'], 'waterfall.png')" # Download FITS catalog curl -s "$API/jobs/$JOB_ID/download?format=fits" $AUTH \ | python3 -c "import sys,json; import urllib.request; \ urllib.request.urlretrieve(json.load(sys.stdin)['download_url'], 'catalog.fits')"

Errors

CodeReason
400Invalid format parameter
403Job belongs to a different user
404Job not found, or requested format not yet available
GET/usageAuth Required
Get current usage statistics and tier quota details.

Response (200)

{ "user_id": "796e24d8-...", "tier": "researcher", "usage_this_month": 12, "limit": 500, "remaining": 488, "tier_details": { "files_per_month": 500, "max_file_mb": 1024, "price": "$99 USD/mo", "retention_days": 90 } }
POST/api-keysAuth RequiredResearcher+
Generate a new API key. The full key is shown only once in the response — store it securely.

Request Body

FieldTypeDefaultDescription
namestring"Default"Friendly name (e.g. "production-v1", "test-pipeline-2")
ttl_daysinteger90Days until expiration (1–365)

Response (201)

{ "key_id": "3f8a1b2c", "api_key": "sk_live_a1b2c3d4e5f6...", "expires_at": 1720272000, "ttl_days": 90, "warning": "Store this key securely. It will not be shown again." }
Important: The api_key value is only returned at creation time. Copy it immediately. If lost, revoke the key and generate a new one.

Errors

CodeReason
403Free tier cannot create API keys — upgrade required
GET/api-keysAuth RequiredResearcher+
List all API keys for the authenticated user (keys are masked, only metadata is shown).

Response (200)

{ "keys": [ { "key_id": "3f8a1b2c", "name": "production-v1", "created_at": 1712444400, "expires_at": 1720272000, "active": true, "expired": false }, { "key_id": "9d4e5f6a", "name": "old-test", "created_at": 1709852400, "expires_at": 1717680000, "active": true, "expired": true } ] }
DELETE/api-keys/{keyId}Auth RequiredResearcher+
Revoke an API key. The key is marked inactive and can no longer be used for authentication.

Response (200)

{ "message": "API key revoked", "key_id": "3f8a1b2c" }

Errors

CodeReason
403Key belongs to a different user
404Key not found
POST/reportsAuth Required
Submit an error report or feedback for a specific job.

Request Body

FieldTypeRequiredDescription
job_idstringYesThe job ID to report about
error_typestringNopipeline_failure or user_report
descriptionstringNoDescription of the issue
metadataobjectNoAdditional context (browser, screen size, etc.)

Response (201)

{ "report_id": "rpt-abc123...", "message": "Report submitted. Thank you." }

Signal Classifications

The ML classifier assigns one of 9 classes to each detected signal:

ClassDescription
CANDIDATE_ETPotential extraterrestrial signal candidate (highest interest)
NARROWBAND_DRIFTINGNarrowband signal with Doppler drift (possible ET or satellite)
NARROWBAND_STATIONARYNarrowband signal with no drift (likely local RFI)
CHIRPFrequency-swept signal (radar, communications)
PULSEDPeriodic pulsed emission (pulsar or radar)
BROADBANDWide-bandwidth signal (natural or instrumental)
RFI_TERRESTRIALConfirmed terrestrial radio frequency interference
RFI_SATELLITESatellite-origin interference
NOISEStatistical noise or instrument artifact

Export Formats

Each completed job produces the following output files, downloadable via GET /jobs/{job_id}/download?format=...:

FormatFileDescription
jsonresults.jsonFull pipeline output: raw hits, filtered hits, candidates, timing, top signals.
csv(generated)Signal catalog as CSV spreadsheet. Returned inline (not as a presigned URL).
catalog_jsoncatalog.jsonClassified signal entries in JSON format with coordinates, SNR, and ML labels.
classifiedclassified.jsonFull classification output including confidence scores and matched filter results.
fitscatalog.fitsFITS binary table (8 columns: SOURCE, FREQ_HZ, DRIFT_RATE, SNR, CLASS, INTEREST, RFI_SRC, DET_SRC). Compatible with TOPCAT, DS9, and astropy.
waterfallwaterfall.pngFull-resolution frequency vs time spectrogram visualization.
waterfall_thumbwaterfall_thumb.pngThumbnail version of the spectrogram for previews.

Rate Limits & Quotas

TierMonthly JobsMax File SizeConcurrent JobsJob HistoryResult RetentionAPI Access
Explorer (Free)1050 MB5107 daysWeb only
Researcher ($99 USD/mo)5001 GB55090 daysREST API + API keys
Institution ($499 USD/mo)5,0005 GB52001 yearREST API + webhooks
Enterprise (Custom)Custom10 GBCustom500CustomFull access + SLA
429 responses: When you hit a rate limit (monthly quota or concurrent job limit), the API returns a 429 Too Many Requests response with a retry_after field indicating how many seconds to wait before retrying.

Error Responses

All errors follow a consistent format:

{ "error": "Monthly limit reached", "limit": 10, "used": 10, "tier": "free" }
HTTP CodeMeaning
400Bad request — missing required fields or invalid input
403Forbidden — invalid/expired API key, or resource belongs to another user
404Not found — job, file, or endpoint does not exist
413Payload too large — file exceeds tier size limit
429Too many requests — rate limit or quota exceeded
500Server error — pipeline or infrastructure failure

Python Example

Complete Python script that uploads a file, runs analysis, polls for results, and downloads all outputs:

import requests import time import os API = "https://api-dev.deepfieldlabs.dev" API_KEY = "sk_live_xxxxxxxxxxxx" HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"} AUTH = {"x-api-key": API_KEY} FILE_PATH = "observation.fil" file_size = os.path.getsize(FILE_PATH) # 1. Get upload URL resp = requests.post(f"{API}/upload-url", json={"filename": os.path.basename(FILE_PATH), "file_size": file_size}, headers=HEADERS) upload = resp.json() file_key = upload["file_key"] # 2. Upload file if upload.get("multipart"): # Multipart upload for large files parts = [] with open(FILE_PATH, "rb") as f: for part in upload["parts"]: chunk = f.read(upload["part_size"]) r = requests.put(part["url"], data=chunk) parts.append({"partNumber": part["partNumber"], "etag": r.headers["ETag"]}) requests.post(f"{API}/complete-upload", json={"file_key": file_key, "upload_id": upload["upload_id"], "parts": parts}, headers=HEADERS) print("Multipart upload complete") else: # Single upload for small files with open(FILE_PATH, "rb") as f: requests.put(upload["upload_url"], data=f, headers={"Content-Type": "application/octet-stream"}) print("Upload complete") # 3. Start analysis resp = requests.post(f"{API}/analyze", json={"file_key": file_key, "filename": os.path.basename(FILE_PATH)}, headers=HEADERS) job_id = resp.json()["job_id"] print(f"Job submitted: {job_id}") # 4. Poll for results (recommended: 10-second intervals) while True: status = requests.get(f"{API}/jobs/{job_id}", headers=AUTH).json() print(f" Status: {status['status']}") if status["status"] in ("complete", "failed"): break time.sleep(10) # 5. Process results if status["status"] == "complete": results = status["results"] print(f"\nResults: {results['raw_hits']} raw hits, " f"{results.get('filtered_hits', 0)} filtered, " f"{results.get('candidates', 0)} candidates") print(f"Processing time: {results.get('processing_time_seconds', 0):.1f}s") for c in results.get("top_candidates", [])[:5]: freq_mhz = c["frequency_hz"] / 1e6 print(f" {freq_mhz:.3f} MHz | SNR={c['snr']:.1f} | {c['classification']}") # 6. Download results (wait for exports to be generated) time.sleep(15) for fmt in ["json", "csv", "fits", "waterfall"]: resp = requests.get(f"{API}/jobs/{job_id}/download?format={fmt}", headers=AUTH) if fmt == "csv": # CSV is returned inline with open(f"{job_id[:8]}_signals.csv", "w") as f: f.write(resp.text) print(f" Saved {fmt}: {job_id[:8]}_signals.csv") else: data = resp.json() if "download_url" in data: content = requests.get(data["download_url"]) ext = {"json": "json", "fits": "fits", "waterfall": "png"}[fmt] filename = f"{job_id[:8]}.{ext}" with open(filename, "wb") as f: f.write(content.content) print(f" Saved {fmt}: {filename}") else: print(f"Job failed: {status.get('error', 'Unknown error')}")

Supported File Types

ExtensionFormatDescription
.filFilterbankStandard radio astronomy filterbank format from telescopes like GBT, Parkes, MeerKAT
.h5HDF5HDF5-based radio observation files (Breakthrough Listen format)