Skip to content

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.


Terminal window
# 1. Register
curl -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 response
curl https://qubithub.co/auth/me \
-H "Authorization: Bearer <access_token>"
Terminal window
# 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.
Terminal window
qubithub login # Interactive email/password prompt
qubithub login --api-key qh_abc123... # Non-interactive API key auth
qubithub login --token eyJhbG... # Non-interactive JWT auth

Verify you’re authenticated:

Terminal window
qubithub whoami

Create a new account. Returns tokens immediately (auto-login).

POST /auth/register

Request body:

FieldTypeRequiredConstraints
emailstringYesValid email address
usernamestringYes3-39 chars, lowercase alphanumeric and hyphens, must start and end with alphanumeric
passwordstringYesMinimum 8 characters
namestringYesDisplay name

Example request:

Terminal window
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:

StatusCause
400Email already registered, password too short, or invalid username format

Authenticate with email and password.

POST /auth/login

Request body:

FieldTypeRequiredDescription
emailstringYesYour email address
passwordstringYesYour password
mfa_codestringNoMFA code (coming soon — not yet available)

Example request:

Terminal window
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:

StatusCause
401Invalid email or password
403Account is inactive

Include the access token in the Authorization header of every authenticated request:

Terminal window
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.

Access tokens are short-lived (15 minutes). Refresh tokens last 30 days and are used to obtain new access tokens.

POST /auth/refresh

Refresh tokens are delivered in two ways:

  1. httpOnly cookie (automatic for browsers) — most secure, no JavaScript access
  2. Response body — for non-browser clients (CLI, SDK, scripts)

Browser clients (cookie is sent automatically):

Terminal window
curl -X POST https://qubithub.co/auth/refresh \
--cookie "refresh_token=eyJhbGciOiJIUzI1NiIs..."

Non-browser clients (pass in request body):

Terminal window
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:

StatusCause
400No refresh token provided
401Token expired, revoked, or invalid
GET /auth/me

Returns the authenticated user’s profile, including organization memberships.

Terminal window
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"
}
]
}
GET /auth/orgs

Returns all organizations the current user belongs to, with their role in each.

Terminal window
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"
}
]
POST /auth/logout

Revokes the refresh token and clears the cookie. Returns 204 No Content.

Terminal window
curl -X POST https://qubithub.co/auth/logout \
-H "Authorization: Bearer <access_token>"

Step 1: Request reset email

POST /auth/forgot-password
Terminal window
curl -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-password
Terminal window
curl -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:

StatusCause
400Token expired (>1 hour) or invalid; password too short
403Account is inactive
404User not found

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.

Requires JWT authentication. The plaintext key is returned only once — save it immediately.

POST /api-keys

Request body:

FieldTypeRequiredDescription
namestringYesDescriptive name (e.g., “CI/CD Pipeline”)
scopesstring[]YesPermission scopes (see Scopes below)
expires_in_daysintegerNoExpiration in days (1-365). null = no expiration
rate_limit_per_minuteintegerNoMax requests per minute (default: 60, max: 1000)

Example request:

Terminal window
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"
}

Once API key authentication is enabled on routes, include the key in the X-API-Key header:

Terminal window
curl https://qubithub.co/circuits \
-H "X-API-Key: qh_a1b2c3d4e5f6g7h8i9j0..."

Or via the CLI:

Terminal window
qubithub login --api-key qh_a1b2c3d4e5f6g7h8i9j0...

Scopes control what an API key is allowed to do. Assign only the scopes your use case needs.

ScopeDescription
circuit:readRead circuit metadata, files, and versions
circuit:writeCreate, update, and delete circuits
dataset:readRead dataset metadata and contents
dataset:writeCreate, update, and delete datasets
runs:submitSubmit circuit execution jobs
org:readRead organization details
org:writeManage organization settings and members
repo:readRead repository contents
repo:writePush to repositories

Example scope combinations:

Use caseScopes
Read-only dashboardcircuit:read, dataset:read
CI/CD pipelinecircuit:read, circuit:write, runs:submit
Organization toolingorg:read, circuit:read, repo:read
GET /api-keys

Returns all your active API keys (without plaintext values).

Terminal window
curl https://qubithub.co/api-keys \
-H "Authorization: Bearer <access_token>"

To include revoked keys, add ?include_inactive=true.

GET /api-keys/{key_id}

Returns details of a specific API key. You must be the key’s creator.

Terminal window
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:

StatusCause
403Not the key’s creator
404Key not found
DELETE /api-keys/{key_id}

Immediately deactivates the key. Returns 204 No Content.

Terminal window
curl -X DELETE https://qubithub.co/api-keys/770e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer <access_token>"

TierRate limit
Free60 req/min
Pro600 req/min

import requests
BASE_URL = "https://qubithub.co"
# Login
response = 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 requests
headers = {"Authorization": f"Bearer {access_token}"}
me = requests.get(f"{BASE_URL}/auth/me", headers=headers)
print(me.json())
# Refresh when the access token expires
response = requests.post(f"{BASE_URL}/auth/refresh", json={
"refresh_token": refresh_token,
})
access_token = response.json()["access_token"]

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())

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 resp

  1. Use API keys for automation, JWT for interactive sessions. API keys are easier to manage for scripts and can be scoped and revoked independently.

  2. Set expiration on API keys. Use expires_in_days to limit the blast radius if a key is leaked. 90 days is a reasonable default for CI/CD.

  3. Use the minimum scopes needed. A monitoring script only needs circuit:read, not circuit:write.

  4. 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 os
    api_key = os.environ["QUBITHUB_API_KEY"]
  5. 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.

  6. 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.

  7. Rotate keys periodically. Create a new key, update your systems, then revoke the old one.


Common authentication errors across all endpoints:

StatusHeaderMeaning
400Bad request (missing fields, invalid format, password too short)
401WWW-Authenticate: BearerMissing or invalid access token / API key
403Account inactive, or not authorized for this resource
404Resource not found

Token lifecycle errors:

ScenarioStatusDetail
Access token expired401Refresh using POST /auth/refresh
Refresh token expired (>30 days)401Log in again with POST /auth/login
API key revoked401Create a new key via POST /api-keys
API key expired401Create a new key with a new expiration
Insufficient scopes403Create a new key with the required scopes