Quickstart

Run your first quantum circuit in under two minutes. No account, no credit card, no hardware required.

Step 1 — Get an API key

One POST request returns a key immediately. The key is displayed once — save it somewhere safe.

# cURL
curl -s -X POST https://api.qumulator.com/keys \
  -H "Content-Type: application/json" \
  -d '{"name": "my-first-key"}'
# Response
{
  "key":        "qum_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "name":       "my-first-key",
  "created_at": "2026-04-20T12:00:00Z"
}

Step 2 — Install the SDK

pip install qumulator-sdk

Step 3 — Run a Bell-state circuit

from qumulator import QumulatorClient

client = QumulatorClient(
    api_url="https://api.qumulator.com",
    api_key="qum_xxxxxxxx...",
)

# Build and run a 2-qubit Bell state
eng = client.circuit.engine(n_qubits=2)
eng.apply('h', 0).apply('cx', [0, 1])

counts = eng.sample(shots=1024)
print(counts)   # {'00': ~512, '11': ~512}
Store your key in the environment variable QUMULATOR_API_KEY and read it with os.environ["QUMULATOR_API_KEY"] to keep it out of source code.

Authentication

Every request to the API (except POST /keys) must include your API key in the X-API-Key header.

curl https://api.qumulator.com/circuits \
  -H "X-API-Key: qum_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

The Python SDK handles this automatically once you pass api_key to QumulatorClient.

Keys are not recoverable after creation. If you lose a key, generate a new one via POST /keys. There is no limit on how many keys you can create.

Base URL & Versioning

Property Value
Base URL https://api.qumulator.com
Protocol HTTPS only
Request format JSON — Content-Type: application/json
Interactive docs api.qumulator.com/docs (Swagger UI)

The API is currently unversioned (no /v1/ prefix). Breaking changes will be announced in advance.

SDK — Installation

pip install qumulator-sdk

Requires Python 3.10 or later. The core package has a single dependency: httpx. Framework adapters (Qiskit, Cirq) require those packages installed separately.

# With Qiskit adapter
pip install qumulator-sdk qiskit

# With Cirq adapter
pip install qumulator-sdk cirq

SDK — QumulatorClient

QumulatorClient is the entry point for all computation types.

from qumulator import QumulatorClient

client = QumulatorClient(
    api_url="https://api.qumulator.com",
    api_key="qum_...",
)
Attribute Type Purpose
client.circuit CircuitClient Quantum circuit simulation (gate builder, QASM, Hamiltonian evolution)
client.klt KLTClient Spin ground states and Pauli Hamiltonian energy
client.hafnian HafnianClient Photonic amplitudes (hafnian, permanent, GBS)
client.homo HomoClient Molecular HOMO/LUMO frontier orbital energies
client.notebook NotebookClient Remote Jupyter notebook execution

SDK — Circuit Simulation

There are two ways to construct a circuit: the fluent gate builder and the direct gate-list API. Both are equivalent and produce identical results. No simulation runs locally — all execution is server-side.

Fluent gate builder

Obtain a CircuitEngine from client.circuit.engine(), accumulate gates with .apply(), then submit with .run() or .sample().

eng = client.circuit.engine(n_qubits=3)

eng.apply('h',   0           )  # Hadamard on qubit 0
   .apply('cx',  [0, 1]       )  # CNOT: control 0, target 1
   .apply('cx',  [0, 2]       )  # CNOT: control 0, target 2
   .apply('rz',  2, params=[1.5708])  # Rz(pi/2) on qubit 2

result = eng.run(
    shots=4096,
    return_statevector=True,
    return_entropy_map=True,
)

print(result.counts)        # {'000': ~2048, '111': ~2048}
print(result.entropy_map)   # [1.0, 0.0, 0.0]  — qubit 0 maximally entangled
print(result.statevector)   # complex ndarray, length 8

Gate-list API

result = client.circuit.run(
    gates=[
        ('h',  0),
        ('cx', [0, 1]),
    ],
    n_qubits=2,
    shots=2048,
)

Supported gates

Gate(s) Qubits Params Description
h, x, y, z 1 Pauli and Hadamard
s, t, sdg, tdg 1 Phase / T gates and their adjoints
rx, ry, rz 1 [θ] Rotation gates; angle in radians
u 1 [θ, φ, λ] General single-qubit unitary (IBM U gate)
cx, cnot 2 CNOT (control, target)
cz 2 Controlled-Z
swap 2 SWAP
iswap 2 iSWAP
fsim 2 [θ, φ] fSim gate (Google Sycamore / Willow family)
syc 2 Sycamore gate
ecr 2 Echoed cross-resonance (IBM native gate)
ccx, toffoli 3 Toffoli (CCNOT)
cswap, fredkin 3 Fredkin
unitary 1–N matrix Arbitrary unitary; pass a 2N×2N complex matrix via params
Qubits are 0-indexed. For two-qubit gates, pass a list: eng.apply('cx', [0, 1]). Bitstrings in results are MSB-first: qubit 0 is the leftmost character.

engine() parameters

Parameter Type Description
n_qubits int required Number of qubits in the register
mode str Simulation mode. Default: "auto". See Simulation Modes.
bond_dim int Bond dimension cap for "compressed" and "tensor" modes

run() parameters

Parameter Type Default Description
shots int 1024 Measurement samples (1 to 1 000 000)
seed int None RNG seed for reproducible sampling
return_statevector bool False Include complex amplitude vector (requires N ≤ 25)
return_probabilities bool False Include probability vector (requires N ≤ 25)
return_entropy_map bool False Include per-qubit entanglement entropy values

SDK — OpenQASM Input

Both OpenQASM 2 and OpenQASM 3 source strings are accepted. Pass the source to client.circuit.run_qasm().

qasm_source = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3]   c;

h  q[0];
cx q[0], q[1];
cx q[0], q[2];
measure q -> c;
"""

result = client.circuit.run_qasm(
    qasm=qasm_source,
    shots=2048,
    mode="exact",
    return_entropy_map=True,
)
print(result.counts)    # {'000': ~1024, '111': ~1024}
QASM 2 circuits exported from Qiskit are fully supported. The server handles both formats transparently. Standard gate libraries including stdgates.inc, qelib1.inc, and custom gate definitions are all supported.

Custom unitary gates via the instructions API

For gates with no standard QASM name (e.g. the SYC gate in Google Willow circuits), use the instructions input with an explicit matrix:

import numpy as np

# Arbitrary 2-qubit unitary (e.g. Google SYC gate)
my_gate = np.array([
    [1, 0,     0,   0   ],
    [0, 0,    -1j,  0   ],
    [0, -1j,   0,   0   ],
    [0, 0,     0,   np.exp(1j * np.pi / 6)],
])

eng = client.circuit.engine(n_qubits=4)
eng.apply('unitary', [0, 1], params=my_gate.tolist())
   .apply('unitary', [2, 3], params=my_gate.tolist())

result = eng.run(shots=1024)

SDK — Simulation Modes

Pass mode= to engine() or run(). The default "auto" lets the server select the optimal backend for your circuit.

Mode Best for N limit
"auto" General use. Server selects the backend based on circuit size and structure. ≤ 1000
"exact" Small circuits requiring full amplitude precision. Statevector and probability arrays available. ≤ 25
"compressed" Structured circuits with moderate entanglement — VQE, QAOA, chemistry ansätze, shallow random circuits. ≤ 1000
"tensor" Circuits with a 1D or near-1D connectivity pattern. ≤ 1000
"hamiltonian" Direct time evolution under a Pauli-string Hamiltonian. Use with evolve_hamiltonian(). ≤ 1000
"gaussian" Clifford-heavy circuits. Returns a GaussianCertificate classifying non-Clifford content. Memory scales as O(N²). ≤ 1000
Depth limits apply depending on qubit count. Exceeding a tier limit returns HTTP 422 with a self-documenting error. See Simulation Limits.

Hamiltonian evolution

Use mode="hamiltonian" and call evolve_hamiltonian() to apply e−iHt directly from a Pauli-string Hamiltonian, without requiring a gate decomposition.

eng = client.circuit.engine(n_qubits=4, mode="hamiltonian")

eng.apply('h', 0).apply('h', 1)  # initial state preparation

eng.evolve_hamiltonian(
    pauli_terms=[
        ( 0.5,  'ZZII'),   # ZZ coupling on sites 0-1
        ( 0.5,  'IZZI'),   # ZZ coupling on sites 1-2
        (-1.0, 'XIIX'),   # transverse field
    ],
    t=1.0,  # evolution time
)

result = eng.run(shots=2048, return_entropy_map=True)

Pauli strings use I X Y Z per qubit; the leftmost character is qubit 0.

Gaussian mode and the simulation certificate

eng = client.circuit.engine(n_qubits=20, mode="gaussian")

for i in range(20):
    eng.apply('h', i)
for i in range(0, 19, 2):
    eng.apply('cz', [i, i+1])
eng.apply('t', 5)   # non-Clifford gate

result = eng.run(shots=1024)
cert   = result.gaussian_certificate

print(cert.rcs_certificate)            # "LIKELY_GAUSSIAN"
print(cert.entanglement_regime)        # "area_law"
print(cert.gaussian_fidelity)          # 0.991
print(cert.wigner_negativity_estimate) # small positive float
Certificate label Meaning
GAUSSIAN_SIMULABLE Purely Clifford circuit; Gaussian approximation is exact.
LIKELY_GAUSSIAN Non-Clifford content is small; high-fidelity approximation.
NON_GAUSSIAN_CORRECTION_NEEDED Substantial non-Clifford content; switch to "exact" or "compressed" for full accuracy.

SDK — Qiskit Integration

QumulatorBackend is a drop-in replacement for any Qiskit simulator backend. Build your circuit in Qiskit as normal; the adapter transpiles and runs it on Qumulator.

from qumulator import QumulatorClient
from qumulator.backends.qiskit_backend import QumulatorBackend
from qiskit import QuantumCircuit

client  = QumulatorClient(api_url="https://api.qumulator.com", api_key="qum_...")
backend = QumulatorBackend(client)

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
qc.measure_all()

job    = backend.run(qc, shots=2048)
result = job.result()
counts = result.get_counts()
print(counts)   # {'00': ~1024, '11': ~1024}
Bit ordering: Qiskit places qubit 0 as the least-significant bit. The adapter reverses bitstrings at output so get_counts() returns Qiskit-standard keys. No changes to your existing code are needed.

Specifying a simulation mode

# compressed mode for a large variational circuit
backend = QumulatorBackend(client, mode="compressed")
job     = backend.run(qc, shots=8192)

Using with Qiskit VQE / QAOA

Any Qiskit circuit can be sent to Qumulator. The adapter automatically transpiles to the supported gate set. Gates that expose gate.to_matrix() are submitted as arbitrary unitaries and simulated exactly.

# Run your parameterised ansatz on Qumulator instead of a local simulator
bound_circuit = ansatz.assign_parameters(theta_values)
bound_circuit.measure_all()

job = backend.run(bound_circuit, shots=4096)
counts = job.result().get_counts()
expectation_value = compute_energy(counts, hamiltonian)

SDK — Cirq Integration

QumulatorSimulator implements Cirq's Simulator interface and supports both sampling and statevector simulation.

import cirq
from qumulator import QumulatorClient
from qumulator.backends.cirq_simulator import QumulatorSimulator

client = QumulatorClient(api_url="https://api.qumulator.com", api_key="qum_...")
sim    = QumulatorSimulator(client)

q0, q1 = cirq.LineQubit.range(2)
circuit = cirq.Circuit(
    cirq.H(q0),
    cirq.CNOT(q0, q1),
    cirq.measure(q0, q1, key='result'),
)

# Sampling
result = sim.run(circuit, repetitions=1024)
print(result.histogram(key='result'))  # Counter({0: ~512, 3: ~512})

# Statevector
sv = sim.simulate(circuit)
print(sv.final_state_vector)
# array([0.707+0j, 0+0j, 0+0j, 0.707+0j])
All Cirq gates are submitted via cirq.unitary(), so any gate with a unitary representation is supported — including custom operations and Sycamore-family gates.

Google Sycamore / Willow circuits

q = cirq.LineQubit.range(4)
circuit = cirq.Circuit(
    [cirq.H(qi) for qi in q],
    cirq.SYC(q[0], q[1]),
    cirq.SYC(q[2], q[3]),
    [cirq.measure(qi, key=str(i)) for i, qi in enumerate(q)],
)
result = sim.run(circuit, repetitions=2048)

SDK — Spin Ground States

Compute ground-state energies for interacting spin systems or arbitrary Pauli-string Hamiltonians. Returns the minimum energy configuration plus a per-site entanglement entropy map.

Pauli Hamiltonian (recommended)

Pass a dictionary of Pauli-string → coefficient entries. Each key is an N-character string over {I, X, Y, Z}, one character per site.

# H2 molecule — STO-3G, Jordan-Wigner mapping (2 sites)
result = client.klt.run(
    pauli_hamiltonian={
        "II": -1.8572750,   # constant / nuclear repulsion
        "ZI": -0.3979374,
        "IZ": -0.3979374,
        "ZZ":  0.3980202,
        "XX":  0.1809270,
        "YY":  0.1809270,
    },
)
print(result.energy)       # ground-state energy in Hartree
print(result.entropy_list) # per-site entanglement entropy

Ising / Heisenberg J-matrix

import numpy as np

N = 16
J = np.random.randn(N, N)
J = (J + J.T) / 2  # symmetrize
np.fill_diagonal(J, 0)

result = client.klt.run(interaction_matrix=J.tolist())
print(result.energy)   # ground-state energy
print(result.states)   # per-site spin expectation values
print(result.max_S)    # max bipartite entanglement entropy

KLTResult fields

Field Type Description
energy float Ground-state energy
states list[float] Per-site expectation values
entropy_list list[float] Per-site entanglement entropy
max_S float Maximum bipartite entanglement entropy
mean_S float Mean entanglement entropy across all bipartitions

SDK — Photonic Amplitudes

Compute the hafnian, permanent, or Gaussian boson sampling amplitudes from a symmetric coupling matrix. The result is a complex number computed to configurable precision.

import numpy as np

n = 8
A = np.random.randn(n, n)
A = (A + A.T) / 2
np.fill_diagonal(A, 0)

result = client.hafnian.run(matrix_real=A.tolist())

print(result.value)    # complex: haf_real + 1j*haf_imag
print(result.elapsed)  # server compute time in seconds

Complex matrices and precision

# Complex coupling matrix, 50 decimal digits of precision
result = client.hafnian.run(
    matrix_real=A_real.tolist(),
    matrix_imag=A_imag.tolist(),
    mp_dps=50,   # default: 34
)

HafnianResult fields

Field Type Description
haf_real float Real part of the hafnian
haf_imag float Imaginary part
value complex Property: haf_real + 1j*haf_imag
elapsed float Server-side compute time (seconds)
max_S float Maximum coupling entropy
n_edges int Number of non-trivial coupling pairs
est_matchings float Estimated number of perfect matchings

SDK — Molecular Orbitals

Compute HOMO and LUMO frontier orbital energies from a SMILES string. Results include the HOMO–LUMO gap and per-atom orbital density weights.

# Resveratrol
result = client.homo.run("Oc1ccc(/C=C/c2cc(O)cc(O)c2)cc1")

print(result.homo_E_eV)    # e.g. -5.88 eV
print(result.lumo_E_eV)    # e.g. -1.40 eV
print(result.gap_eV)       # HOMO-LUMO gap in eV
print(result.heavy_symbols) # ['C', 'C', ..., 'O', 'O']
print(result.homo_density)  # per-heavy-atom HOMO weight

Basis set and functional

# Use a larger basis set and a different functional
result = client.homo.run(
    "c1ccccc1",   # benzene
    basis="6-31g",
    xc="pbe0",
)

HomoResult fields

Field Type Description
homo_E_eV float HOMO energy in eV
lumo_E_eV float LUMO energy in eV
gap_eV float HOMO–LUMO gap in eV
homo_density list[float] Per-heavy-atom HOMO orbital density
lumo_density list[float] Per-heavy-atom LUMO orbital density
heavy_symbols list[str] Element symbol for each heavy atom
n_occ int Number of occupied orbitals
basis str Basis set used (default: sto-3g)
xc str Exchange-correlation functional (default: b3lyp)

SDK — Notebook Execution

Submit a Jupyter notebook (.ipynb) for remote execution. The notebook runs in an isolated sandbox environment with qumulator-sdk and the full scientific Python stack pre-installed.

# Submit and wait for completion
with open("my_experiment.ipynb", "rb") as f:
    nb_bytes = f.read()

status = client.notebook.run(nb_bytes)
print(status.status)   # "completed"
# status.result contains the executed notebook with outputs

Async: submit then poll

import time

with open("my_experiment.ipynb", "rb") as f:
    job_id = client.notebook.submit(f.read())

print("Job submitted:", job_id)

while True:
    status = client.notebook.status(job_id)
    if status.is_done:
        break
    time.sleep(3)

if status.ok:
    executed_notebook = status.result
else:
    print("Failed:", status.error)
Pre-installed packages: numpy, scipy, matplotlib, pandas, qiskit, cirq, and qumulator-sdk. Additional packages can be installed at the top of your notebook with !pip install ....

REST API — API Keys

Create a key

POST /keys

No authentication required.

Field Type Description
name string Human-readable label
# Response 201
{
  "key":        "qum_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "name":       "my-key",
  "created_at": "2026-04-20T12:00:00Z"
}

REST API — Circuits

Submit a circuit

POST /circuits

Returns job_id immediately. Simulation runs asynchronously.

Field Type Description
qasm string OpenQASM 2 or 3 source. Provide either qasm or instructions.
instructions array JSON gate instruction list. Requires n_qubits.
n_qubits int Register width. Required when using instructions.
shots int Measurement samples. Default: 1024. Max: 1 000 000.
mode string Simulation mode. Default: statevector.
seed int RNG seed for reproducible sampling.
return_statevector bool Include complex amplitude vector in result.
return_entropy_map bool Include per-qubit entropy values in result.
bond_dim int Bond dimension cap for tensor-network modes.
# Submit via cURL
curl -X POST https://api.qumulator.com/circuits \
  -H "X-API-Key: qum_..." \
  -H "Content-Type: application/json" \
  -d '{
    "qasm": "OPENQASM 3.0; include \"stdgates.inc\"; qubit[2] q; h q[0]; cx q[0],q[1]; bit[2] c; measure q->c;",
    "shots": 1024,
    "mode": "statevector"
  }'

# Response 202
{ "job_id": "01HX8M7PVKTA..." }

List recent circuits

GET/circuits

Returns the most recent circuit jobs for your API key.

Get a circuit job

GET/circuits/{job_id}

REST API — Jobs & Polling

All simulation endpoints return a job_id immediately. Poll until status is "completed" or "failed".

# Poll a job
curl https://api.qumulator.com/circuits/01HX8M7PVKTA \
  -H "X-API-Key: qum_..."

# While running
{ "job_id": "01HX...", "status": "running", "result": null }

# When complete
{
  "job_id":  "01HX...",
  "status":  "completed",
  "result": {
    "counts":      { "00": 512, "11": 512 },
    "n_qubits":    2,
    "shots":       1024,
    "entropy_map": [1.0, 1.0]
  }
}
Status Meaning
queued Accepted and waiting for a worker
running Simulation in progress
completed Result available in result field
failed Error message in error field

Reference — Result Types

CircuitResult

Field Type Description
counts dict[str, int] Measurement outcome counts. Keys are MSB-first bitstrings (qubit 0 leftmost).
n_qubits int Register width
shots int Total measurement samples
statevector ndarray|None Complex amplitude vector of length 2N. Populated when return_statevector=True.
probabilities ndarray|None Probability vector. Populated when return_probabilities=True.
entropy_map list[float]|None Per-qubit entanglement entropy in bits. 1.0 = maximally entangled; 0.0 = product state.
gaussian_certificate GaussianCertificate|None Populated when mode="gaussian".
most_probable str (property) Bitstring with the highest count

GaussianCertificate

Field Type Description
rcs_certificate str GAUSSIAN_SIMULABLE | LIKELY_GAUSSIAN | NON_GAUSSIAN_CORRECTION_NEEDED
entanglement_regime str|None area_law | transitional | volume_law
wigner_negativity_estimate float|None Estimated non-Clifford correction from T-gate content
gaussian_fidelity float|None Estimated fidelity of the Gaussian approximation (0–1)
xeb_lower_bound float|None Cross-entropy benchmark lower bound

JobStatus

Field Type Description
job_id str Job identifier
status str queued | running | completed | failed
result dict|None Engine result when completed
error str|None Error message when failed
is_done bool Property: True when completed or failed
ok bool Property: True when completed

Reference — Simulation Limits

Circuit depth, not qubit count, determines memory. Requests exceeding a tier limit return HTTP 422 with a self-documenting error message.

Tier Qubit range Max depth Peak memory Notes
1 1–20 20 layers ~335 MB Full-depth VQE, QAOA, variational ansätze at any structure.
2 21–54 6 layers ~3.5 MB Covers IBM Eagle (27 q) and Osprey (54 q) layouts.
3 55–105 5 layers ~1.7 MB Covers Willow-scale (105 q) near-term superconducting layouts.
4 106–1000 3 layers < 1 MB A 1000-qubit depth-3 circuit uses less memory than a JPEG image.
N > 1000 Rejected (HTTP 422). Contact us for large-scale access.
"exact" mode is limited to N ≤ 25. All modes follow the tier depth limits above.

Reference — Rate Limits

Limit Value
Requests per key per minute 60
Window type Sliding (per-second resolution)
HTTP status when exceeded 429 with Retry-After header

The Retry-After header tells you exactly how many seconds to wait. The SDK surfaces this as QumulatorHTTPError(status_code=429).

from qumulator import QumulatorHTTPError
import time

try:
    result = eng.run(shots=1024)
except QumulatorHTTPError as e:
    if e.status_code == 429:
        retry_after = int(e.response.headers.get("Retry-After", 10))
        time.sleep(retry_after)
        result = eng.run(shots=1024)   # retry once

Reference — Errors

All error responses use the shape {"detail": "...message..."}.

Status Meaning
400 Malformed JSON or missing required field.
401 Missing or invalid X-API-Key header.
422 Validation error — circuit exceeds a tier limit, unknown gate, or unsupported mode. The response body includes a self-documenting message with the full tier table.
429 Rate limit exceeded. Check the Retry-After header.
500 Internal server error. Retry after a moment; contact us if it persists.
# Example 422 response (circuit exceeds tier limit)
{
  "detail": "Limit Exceeded: 30 qubits at depth 15. Tier 2 (21-54 q) max depth is 6.
  Tiers: T1(1-20q,d≤20) T2(21-54q,d≤6) T3(55-105q,d≤5) T4(106-1000q,d≤3)"
}
Questions, bug reports, or large-scale access requests? Contact us via the main site.