Authentication
QubitHub supports two authentication methods:
- JWT tokens — for browser sessions, interactive use, and all API endpoints
- API keys — for future programmatic access (scripts, CI/CD, SDK)
All API endpoints currently require JWT authentication. API key infrastructure is built and keys can be created and managed, but route-level API key authentication is not yet enabled. This guide documents both methods so you can create keys now and use them as endpoint support is added.
Quick start
Section titled “Quick start”Option A: JWT (interactive sessions)
Section titled “Option A: JWT (interactive sessions)”# 1. Registercurl -X POST https://qubithub.co/auth/register \ -H "Content-Type: application/json" \ -d '{ "email": "you@example.com", "username": "your-username", "password": "s3cure-Passw0rd!", "name": "Your Name" }'
# 2. Use the access_token from the responsecurl https://qubithub.co/auth/me \ -H "Authorization: Bearer <access_token>"Option B: API key (programmatic access)
Section titled “Option B: API key (programmatic access)”# 1. Create an API key (requires JWT auth first)curl -X POST https://qubithub.co/api-keys \ -H "Authorization: Bearer <access_token>" \ -H "Content-Type: application/json" \ -d '{ "name": "my-script", "scopes": ["circuit:read", "circuit:write"] }'
# 2. Save the key from the response — you'll use it once API key auth is enabled# API key route support is coming soon. For now, use JWT for all requests.Option C: CLI authentication
Section titled “Option C: CLI authentication”qubithub login # Interactive email/password promptqubithub login --api-key qh_abc123... # Non-interactive API key authqubithub login --token eyJhbG... # Non-interactive JWT authVerify you’re authenticated:
qubithub whoamiJWT authentication
Section titled “JWT authentication”Register
Section titled “Register”Create a new account. Returns tokens immediately (auto-login).
POST /auth/registerRequest body:
| Field | Type | Required | Constraints |
|---|---|---|---|
email | string | Yes | Valid email address |
username | string | Yes | 3-39 chars, lowercase alphanumeric and hyphens, must start and end with alphanumeric |
password | string | Yes | Minimum 8 characters |
name | string | Yes | Display name |
Example request:
curl -X POST https://qubithub.co/auth/register \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com", "username": "alice-q", "password": "Qu4ntum!Leap#42", "name": "Alice Quantum" }'Example response (201 Created):
{ "access_token": "eyJhbGciOiJIUzI1NiIs...", "refresh_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "bearer", "expires_in": 900, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "alice@example.com", "username": "alice-q", "name": "Alice Quantum", "avatar": null, "organizations": null }}Errors:
| Status | Cause |
|---|---|
400 | Email already registered, password too short, or invalid username format |
Authenticate with email and password.
POST /auth/loginRequest body:
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Your email address |
password | string | Yes | Your password |
mfa_code | string | No | MFA code (coming soon — not yet available) |
Example request:
curl -X POST https://qubithub.co/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "alice@example.com", "password": "Qu4ntum!Leap#42" }'Example response (200 OK):
{ "access_token": "eyJhbGciOiJIUzI1NiIs...", "refresh_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "bearer", "expires_in": 900, "user": { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "alice@example.com", "username": "alice-q", "name": "Alice Quantum", "avatar": null, "organizations": null }}Errors:
| Status | Cause |
|---|---|
401 | Invalid email or password |
403 | Account is inactive |
Using access tokens
Section titled “Using access tokens”Include the access token in the Authorization header of every authenticated request:
curl https://qubithub.co/auth/me \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Access tokens expire after 15 minutes. Use the refresh endpoint to get a new one without re-entering credentials.
Refresh tokens
Section titled “Refresh tokens”Access tokens are short-lived (15 minutes). Refresh tokens last 30 days and are used to obtain new access tokens.
POST /auth/refreshRefresh tokens are delivered in two ways:
- httpOnly cookie (automatic for browsers) — most secure, no JavaScript access
- Response body — for non-browser clients (CLI, SDK, scripts)
Browser clients (cookie is sent automatically):
curl -X POST https://qubithub.co/auth/refresh \ --cookie "refresh_token=eyJhbGciOiJIUzI1NiIs..."Non-browser clients (pass in request body):
curl -X POST https://qubithub.co/auth/refresh \ -H "Content-Type: application/json" \ -d '{"refresh_token": "eyJhbGciOiJIUzI1NiIs..."}'Example response (200 OK):
{ "access_token": "eyJhbGciOiJIUzI1NiIs...", "refresh_token": "eyJhbGciOiJIUzI1NiIs...", "token_type": "bearer"}Errors:
| Status | Cause |
|---|---|
400 | No refresh token provided |
401 | Token expired, revoked, or invalid |
Get current user
Section titled “Get current user”GET /auth/meReturns the authenticated user’s profile, including organization memberships.
curl https://qubithub.co/auth/me \ -H "Authorization: Bearer <access_token>"Example response (200 OK):
{ "id": "550e8400-e29b-41d4-a716-446655440000", "email": "alice@example.com", "username": "alice-q", "name": "Alice Quantum", "avatar": null, "organizations": [ { "id": "660e8400-e29b-41d4-a716-446655440000", "name": "Alice's Organization", "role": "owner" } ]}List organizations
Section titled “List organizations”GET /auth/orgsReturns all organizations the current user belongs to, with their role in each.
curl https://qubithub.co/auth/orgs \ -H "Authorization: Bearer <access_token>"Example response (200 OK):
[ { "id": "660e8400-e29b-41d4-a716-446655440000", "name": "Alice's Organization", "role": "owner" }, { "id": "770e8400-e29b-41d4-a716-446655440000", "name": "Quantum Research Lab", "role": "member" }]Logout
Section titled “Logout”POST /auth/logoutRevokes the refresh token and clears the cookie. Returns 204 No Content.
curl -X POST https://qubithub.co/auth/logout \ -H "Authorization: Bearer <access_token>"Password reset
Section titled “Password reset”Step 1: Request reset email
POST /auth/forgot-passwordcurl -X POST https://qubithub.co/auth/forgot-password \ -H "Content-Type: application/json" \ -d '{"email": "alice@example.com"}'Always returns 204 No Content (prevents email enumeration). If the email exists, a reset link is sent with a 1-hour expiry.
Step 2: Reset password with token
POST /auth/reset-passwordcurl -X POST https://qubithub.co/auth/reset-password \ -H "Content-Type: application/json" \ -d '{ "token": "eyJhbGciOiJIUzI1NiIs...", "password": "N3w$ecure!Pass99" }'Returns 204 No Content on success. All existing sessions are invalidated — you must log in again on all devices.
Errors:
| Status | Cause |
|---|---|
400 | Token expired (>1 hour) or invalid; password too short |
403 | Account is inactive |
404 | User not found |
API key authentication
Section titled “API key authentication”API keys are for programmatic access: CI/CD pipelines, scripts, the QubitHub SDK, or any automated workflow. They do not expire by default and support fine-grained scopes.
Create an API key
Section titled “Create an API key”Requires JWT authentication. The plaintext key is returned only once — save it immediately.
POST /api-keysRequest body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Descriptive name (e.g., “CI/CD Pipeline”) |
scopes | string[] | Yes | Permission scopes (see Scopes below) |
expires_in_days | integer | No | Expiration in days (1-365). null = no expiration |
rate_limit_per_minute | integer | No | Max requests per minute (default: 60, max: 1000) |
Example request:
curl -X POST https://qubithub.co/api-keys \ -H "Authorization: Bearer <access_token>" \ -H "Content-Type: application/json" \ -d '{ "name": "CI/CD Pipeline", "scopes": ["circuit:read", "runs:submit"], "expires_in_days": 90, "rate_limit_per_minute": 100 }'Example response (201 Created):
{ "id": "770e8400-e29b-41d4-a716-446655440000", "name": "CI/CD Pipeline", "key": "qh_a1b2c3d4e5f6g7h8i9j0...", "key_prefix": "qh_a1b2c", "scopes": ["circuit:read", "runs:submit"], "rate_limit_per_minute": 100, "expires_at": "2026-06-25T12:00:00", "created_at": "2026-03-27T12:00:00"}Using API keys
Section titled “Using API keys”Once API key authentication is enabled on routes, include the key in the X-API-Key header:
curl https://qubithub.co/circuits \ -H "X-API-Key: qh_a1b2c3d4e5f6g7h8i9j0..."Or via the CLI:
qubithub login --api-key qh_a1b2c3d4e5f6g7h8i9j0...Scopes
Section titled “Scopes”Scopes control what an API key is allowed to do. Assign only the scopes your use case needs.
| Scope | Description |
|---|---|
circuit:read | Read circuit metadata, files, and versions |
circuit:write | Create, update, and delete circuits |
dataset:read | Read dataset metadata and contents |
dataset:write | Create, update, and delete datasets |
runs:submit | Submit circuit execution jobs |
org:read | Read organization details |
org:write | Manage organization settings and members |
repo:read | Read repository contents |
repo:write | Push to repositories |
Example scope combinations:
| Use case | Scopes |
|---|---|
| Read-only dashboard | circuit:read, dataset:read |
| CI/CD pipeline | circuit:read, circuit:write, runs:submit |
| Organization tooling | org:read, circuit:read, repo:read |
List API keys
Section titled “List API keys”GET /api-keysReturns all your active API keys (without plaintext values).
curl https://qubithub.co/api-keys \ -H "Authorization: Bearer <access_token>"To include revoked keys, add ?include_inactive=true.
Get a single API key
Section titled “Get a single API key”GET /api-keys/{key_id}Returns details of a specific API key. You must be the key’s creator.
curl https://qubithub.co/api-keys/770e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer <access_token>"Example response (200 OK):
{ "id": "770e8400-e29b-41d4-a716-446655440000", "name": "CI/CD Pipeline", "key_prefix": "qh_a1b2c", "scopes": ["circuit:read", "runs:submit"], "rate_limit_per_minute": 100, "is_active": true, "last_used_at": "2026-03-27T14:30:00", "expires_at": "2026-06-25T12:00:00", "created_at": "2026-03-27T12:00:00"}Errors:
| Status | Cause |
|---|---|
403 | Not the key’s creator |
404 | Key not found |
Revoke an API key
Section titled “Revoke an API key”DELETE /api-keys/{key_id}Immediately deactivates the key. Returns 204 No Content.
curl -X DELETE https://qubithub.co/api-keys/770e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer <access_token>"Rate limits
Section titled “Rate limits”| Tier | Rate limit |
|---|---|
| Free | 60 req/min |
| Pro | 600 req/min |
Python examples
Section titled “Python examples”JWT authentication
Section titled “JWT authentication”import requests
BASE_URL = "https://qubithub.co"
# Loginresponse = requests.post(f"{BASE_URL}/auth/login", json={ "email": "alice@example.com", "password": "Qu4ntum!Leap#42",})tokens = response.json()access_token = tokens["access_token"]refresh_token = tokens["refresh_token"]
# Make authenticated requestsheaders = {"Authorization": f"Bearer {access_token}"}me = requests.get(f"{BASE_URL}/auth/me", headers=headers)print(me.json())
# Refresh when the access token expiresresponse = requests.post(f"{BASE_URL}/auth/refresh", json={ "refresh_token": refresh_token,})access_token = response.json()["access_token"]API key authentication (coming soon)
Section titled “API key authentication (coming soon)”Once API key auth is enabled on routes, usage will look like:
import requests
BASE_URL = "https://qubithub.co"API_KEY = "qh_a1b2c3d4e5f6g7h8i9j0..."
headers = {"X-API-Key": API_KEY}
# List circuits (requires API key route support — not yet enabled)circuits = requests.get(f"{BASE_URL}/circuits", headers=headers)print(circuits.json())Token refresh pattern
Section titled “Token refresh pattern”For long-running scripts, wrap your requests with automatic refresh:
import requests
class QubitHubClient: def __init__(self, email: str, password: str, base_url: str = "https://qubithub.co"): self.base_url = base_url self._refresh_token = None self._access_token = None self._login(email, password)
def _login(self, email: str, password: str): resp = requests.post(f"{self.base_url}/auth/login", json={ "email": email, "password": password, }) resp.raise_for_status() data = resp.json() self._access_token = data["access_token"] self._refresh_token = data["refresh_token"]
def _refresh(self): resp = requests.post(f"{self.base_url}/auth/refresh", json={ "refresh_token": self._refresh_token, }) resp.raise_for_status() self._access_token = resp.json()["access_token"]
def request(self, method: str, path: str, **kwargs): headers = kwargs.pop("headers", {}) headers["Authorization"] = f"Bearer {self._access_token}" resp = requests.request(method, f"{self.base_url}{path}", headers=headers, **kwargs) if resp.status_code == 401: self._refresh() headers["Authorization"] = f"Bearer {self._access_token}" resp = requests.request(method, f"{self.base_url}{path}", headers=headers, **kwargs) return respSecurity best practices
Section titled “Security best practices”-
Use API keys for automation, JWT for interactive sessions. API keys are easier to manage for scripts and can be scoped and revoked independently.
-
Set expiration on API keys. Use
expires_in_daysto limit the blast radius if a key is leaked. 90 days is a reasonable default for CI/CD. -
Use the minimum scopes needed. A monitoring script only needs
circuit:read, notcircuit:write. -
Never commit keys to version control. Store API keys in environment variables or a secrets manager.
Terminal window # .env (never commit this file)QUBITHUB_API_KEY=qh_a1b2c3d4e5f6g7h8i9j0...import osapi_key = os.environ["QUBITHUB_API_KEY"] -
Revoke compromised keys immediately. If a key is exposed in logs, a commit, or a paste, revoke it via
DELETE /api-keys/{id}and create a new one. -
Store refresh tokens securely. For browser clients, the httpOnly cookie handles this automatically. For CLI/SDK clients, store the refresh token in a secure location (keyring, encrypted config), not in plaintext files.
-
Rotate keys periodically. Create a new key, update your systems, then revoke the old one.
Error reference
Section titled “Error reference”Common authentication errors across all endpoints:
| Status | Header | Meaning |
|---|---|---|
400 | — | Bad request (missing fields, invalid format, password too short) |
401 | WWW-Authenticate: Bearer | Missing or invalid access token / API key |
403 | — | Account inactive, or not authorized for this resource |
404 | — | Resource not found |
Token lifecycle errors:
| Scenario | Status | Detail |
|---|---|---|
| Access token expired | 401 | Refresh using POST /auth/refresh |
| Refresh token expired (>30 days) | 401 | Log in again with POST /auth/login |
| API key revoked | 401 | Create a new key via POST /api-keys |
| API key expired | 401 | Create a new key with a new expiration |
| Insufficient scopes | 403 | Create a new key with the required scopes |
Next steps
Section titled “Next steps”- CLI Quickstart — get up and running with the CLI
- Web UI Guide — browse and run circuits in the browser
- qubithub.toml Reference — circuit manifest specification
- API Reference — full endpoint documentation