Educational disclaimer. This is a hands-on starter tutorial in quantum computing, meant for learning. The circuit is the smallest interesting one there is, and the platform tour around it is deliberately shallow. Backend counts, marketplace counts and measurement integers are snapshots from the day this was written, and they will drift. Treat the live pages and the API references as the single source of truth. With AI tools every solution is one prompt away; to get the most value, try before you click.
1. Build
Turn two independent coin flips into one entangled pair. Two qubits, two gates, one shared state.
2. Run it free
Transpile and submit to kipu.sim.qsim, the free Kipu simulator, then read the counts back.
3. Map the stack
Enumerate the hardware layer, find your job in the dashboard, and place L0 to L3 around your circuit.
Wondering what superposition and entanglement actually are, or why quantum is having its moment? Read the primer first, then come back.
Every task below carries collapsed hints: Hint 1 says where to look, Hint 2 gives the shape of the answer, the Solution is full verified code. Think first, then click.
Step 0: Set up the environment
To run your first quantum program, you need a Python project folder with the Hub SDK, and your personal access token.
Build from scratch with uv, qhub-quantum and qhubctl
We teach this approach with uv, use your favorite Python package manager at your own discretion. Either way, the code requires Python 3.11 or newer.
uv init kipu-hub-academy
cd kipu-hub-academy
uv venv
uv add qhub-quantum
You will also want the CLI (optional, recommended), which is a robust authorization route for your code. It ships on npm and needs Node.js 20 or higher:
npm install -g @quantum-hub/qhubctl
After completion
qhub-quantum brings Qiskit (the most popular and widely adopted programming SDK for quantum) with it, and does not require a separate install. To run your qhub python scripts you can now use:
uv run python example.py
Step 1: Connect
On Kipu's Quantum Hub, you authenticate with a personal access token. Think of them as unique identifiers to authorize actions inside your profile and organizations you are part of. Keep it safe like you would protect a password.
Navigation: You can find it on the home page to copy and regenerate if you suspect your secrets have been leaked. To generate and manage additional, context-specific and time-limited access tokens use the options in your profile settings.


Reference: Manage access tokens.
How do I authenticate with my token?
To authenticate, the qhub-SDK provides three options.
- CLI login (recommended). Run
qhubctl login -t <your access token>. That writes~/.config/qhubctl/config.json, where the SDK reads it. - Environment variable. Export
KQH_PERSONAL_ACCESS_TOKEN=<your access token>in the environment you run the tasks from. Make sure to exclude it from uploading to git or docker containers (developer option). - Plain text in code, some objects allow you to pass your access_token directly inline. This risks exposure and leaks to third parties. Be extra careful and rotate your token frequently.
Task 1. Write a Python script that connects to a Quantum Hub backend using your Personal Access Token.
Hint 1, where to look
The connect object lives in qhub.quantum.sdk. The Quickstart shows exemplary implementations. For a detailed backend-specific description check Quantum SDK reference with further documentation-links.
To prove everything works, ask the Hub for something. kipu.sim.qsim is a free Quantum simulator backend for testing the connection. Authenticate with your personal access token.
Hint 2, what to test
Reread Quickstart (Coin Toss). You can omit circuit construction and running a simulation, declare provider = … and backend = … with the appropriate qhub.quantum.sdk import for this exercise.
Solution
# task1_connect.py
from qhub.quantum.sdk import HubQiskitProvider
try:
provider = HubQiskitProvider()
check = provider.get_backend('kipu.sim.qsim')
print(f'Connected. You are connected to {check.name}.')
except Exception as e:
print(f'Connection failed: {e}\n')
print('There are multiple options to complete this task:')
print(' Route 1 (CLI, recommended): qhubctl login -t <your access token>')
print(' Route 2 (ENV): export KQH_PERSONAL_ACCESS_TOKEN=<your access token> and set HubQiskitProvider(access_token=os.getenv("KQH_PERSONAL_ACCESS_TOKEN"))')
print(' Route 3 (CODE): declare HubQiskitProvider(access_token=<your access token>) explicitly in the code. Take extra precautions and replace your token after exposing it.')
uv run python task1_connect.py
# Connected. You are connected to kipu.sim.qsim.
Step 2: The hardware layer
The ground layer of the Hub is hardware: QPUs and simulators from several vendors, all reachable with your credentials.
provider.backends()returns the backend ids, as strings in a list.provider.backends(detailed=True)returns backends with their specifications.
Check the Backend Documentation for a full reference.
Task 2. Write task2_backends.py to a. print the number of all available backends on the qhub-SDK. Then, b. from the detailed list, keep only the simulators, and print one line each with id, and qubit count.
Hint 1, where to look
provider.backends() gives you a list of all available backends.
from qhub.quantum.sdk import HubQiskitProvider
provider = HubQiskitProvider()
print(provider.backends())
For names, do not go by memory. The following code snippet prints all properties of a backend. Check the reference: Backend Documentation for explanations, too.
from qhub.quantum.sdk import HubQiskitProvider
provider = HubQiskitProvider()
detailed = provider.backends(detailed=True)
print(sorted(t for t in dir(detailed[0]) if not t.startswith('_')))
print(sorted(c for c in dir(detailed[0].configuration) if not c.startswith('_')))
Hint 2, which patterns to use
You are interested in the id, type, and configuration.
idis the backend identifier (e.g. kipu.sim.qsim)typeindicates whether a backend is aQPU,SIMULATOR,ANNEALER, orUNKNOWN.configuration.qubit_countholds the qubit count of the backend.
print(len(your_list)) prints the length of a list.
filtered_list = list(i for i in unfiltered_list if i.property == 'VALUE') let's you filter a list based on a property value.
Solution
# task2_backends.py
from qhub.quantum.sdk import HubQiskitProvider
provider = HubQiskitProvider()
detailed = provider.backends(detailed=True)
print(f'Backends the qhub-SDK offers: {len(detailed)}')
simulators = list(b for b in detailed if b.type == 'SIMULATOR')
print(f'\nSimulators ({len(simulators)} of {len(detailed)}):')
for b in simulators:
print(f' {b.id:<24} {b.configuration.qubit_count} qubits')
Backends the qhub-SDK offers: 22
Simulators (6 of 22):
aws.sim.dm1 17 qubits
aws.sim.sv1 34 qubits
azure.ionq.simulator 29 qubits
kipu.sim.qsim 30 qubits
quandela.sim.belenos 12 qubits
qudora.sim.xg1 32 qubits
# -> your count may differ: the catalogue grows, and access is per account. (Tutorial updated on Aug-04 2026)
The identifier is b.id. b.display_name holds the human label ("Kipu QSim Simulator") you might know from the backend page on Kipu's Quantum Hub. The specifications live under b.configuration, which also carries connectivity, gates, shots_range and supported_input_formats, all worth a look once you start choosing hardware for quantum-centric applications.
Hub / Quantum-Backends shows all available backends and their IDs with surface-level info for you to browse on the Hub. Beware, the catalogue is growing: the numbers and availabilities for your account will change over time. Access to Kipu quantum simulator kipu.sim.qsim and Azure IonQ Simulator azure.ionq.simulator is free of charge. Other backends/devices require an account with active payment information.
You can check for details under Pricing. For information about the full service offering and Enterprise accounts, contact sales@kipu-quantum.com.
Step 3: From coin flip to entanglement
Think back to the Quickstart coin toss circuit. A Hadamard operation on each qubit, then measure all. For two coins, both qubit measurements are independent of each other, because nothing in that circuit connects them. With 50-50 probability each for 0 (heads) and 1 (tails), there exist four outcomes, 00, 01, 10 and 11, with equal probability of 25% each.
You enter a bet with a rich consul, that you find to be a suspicious quantum wizard. He suggests both of you bet a gold coin on the outcome of a coin flip. If both coins land on the same symbol, you get to keep it, if they disagree, you concede your coin. A seemingly fair game. Would you take that bet?
Before you answer, you are presented with the ability to think and run quantum experiments.
3a. Build the circuit
Your task. Write task3_circuit.py. Build a two-qubit circuit. Put qubit 0 in superposition with a Hadamard (h) gate. Then, apply a connecting controlled-X (cx) gate, that is controlled by qubit 0 and targets qubit 1. Add the measure_all operation, and print the circuit diagram with print(circuit.draw()).
Hint 1, where to look
Find the gate from the method list.
-
Qiskit's QuantumCircuit API reference lists all methods on that page that you need for this exercise. Look among the controlled gates.
-
Or browse locally, without leaving your editor:
print([m for m in dir(qc) if not m.startswith('_')]).
QuantumCircuit(2) gives you a two qubit setup to build your circuit.
Hint 2, the shape of the answer
circuit = QuantumCircuit(2): creates the two-qubit setup
circuit.h(0): The h-gate applies an equal superposition to qubit 0. A fair coin.
circuit.cx(0,1): The cx-gate takes two qubit arguments. The first is the control qubit, the second is the target. It flips the target (qubit 1) exactly when the control (qubit 0) is 1. On classical input it behaves like an ordinary if-statement. Here the control is in superposition, so the rule applies to all states at once: in the branch where qubit 0 is 0, the target stays 0 (00); in the branch where it is 1, the target flips (11). Neither qubit has a decided value yet; however, it is encoded in the state that they will match.
circuit.measure_all(): Measurements read the qubits' states simultaneously. A circuit with no measurements transpiles and submits fine, then fails when running on a backend. Without measurements, no results to obtain.
Solution
# task3_circuit.py
from qiskit import QuantumCircuit
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0,1)
circuit.measure_all()
print(circuit.draw())
┌───┐ ░ ┌─┐
q_0: ┤ H ├──■───░─┤M├───
└───┘┌─┴─┐ ░ └╥┘┌─┐
q_1: ─────┤ X ├─░──╫─┤M├
└───┘ ░ ║ └╥┘
meas: 2/══════════════╩══╩═
You just applied a controlled-NOT! This maximally entangles the control qubit 0 and target qubit 1: measured simultaneously, the results always match. As the purest form of combining superposition and entanglement we call it the Bell state. A similar classical circuit must read the control bit before deciding to flip the target. The cx gate never reads anything; it acts on both branches of the superposition and eliminates some outcomes, very similar to destructive interference in waves. To investigate this state further you would need a Bell test; check IBM's comprehensive tutorial for more.
3b. Stop and reflect, what do you expect to happen?
What do you expect to measure when running the circuit?
Your coin flip gave four outcomes at roughly a quarter each. With the circuit you just built, which states of 00, 01, 10 and 11 do you expect to survive, and with what probability? Write it down, or discuss with a partner. Check it after the experiment.
Step 4: Run the experiment
transpile(circuit, backend) rewrites your circuit into instructions the target hardware chip can actually use. Each chip can use different gate sets and has a different qubit layout. This is an essential translation step.
Pin the backend to kipu.sim.qsim as introduced in Step 2. It is one of the free simulators on Kipu's Quantum Hub.

4a. Transpile, submit, read the counts
Your task. Write task4_run.py. Create and transpile the two-qubit bell circuit for kipu.sim.qsim, submit 1024 shots, print the job id, wait for the job to finish, and print the measurement counts.
Hint 1, where to look
The Quickstart shows both the transpile and readout calls and Job Documentation lists all properties of the job object.
results = job.result()
print(type(results))
print([m for m in dir(results) if not m.startswith('_')])
That two-line habit is the transferable skill in this step, and it is worth more than the answer it gives you here.
Hint 2, the shape of the answer
job.id holds the job ID; print it so you can find the job again later.
job.result() hands you a stock Qiskit result object, that means you need to call job.result().get_counts() for the measurement results.
Submitting is asynchronous. That means you can submit multiple jobs in parallel. At the same time, results take a while to compute on the cloud before the results are returned to your program.
Solution
# task4_run.py
from qhub.quantum.sdk import HubQiskitProvider
from qiskit import QuantumCircuit, transpile
BACKEND_ID = 'kipu.sim.qsim' # free for testing
SHOTS = 1024
circuit = QuantumCircuit(2)
circuit.h(0)
circuit.cx(0, 1)
circuit.measure_all()
# Guard, not a formality. Without measurements this transpiles and submits fine, then
# fails on the backend, and the error surfaces at the readout where it looks unrelated.
if 'measure' not in circuit.count_ops():
raise RuntimeError('This circuit has no measurements, so there is nothing to sample.')
provider = HubQiskitProvider()
backend = provider.get_backend(BACKEND_ID)
transpiled = transpile(circuit, backend)
job = backend.run(transpiled, shots=SHOTS)
print(f'Submitted to {BACKEND_ID}, {SHOTS} shots.')
print(f'Job id: {job.id}')
print(f'Status: {job.status()}')
job.wait_for_final_state()
result = job.result()
print(result.get_counts())
# -> {'00': 525, '11': 499}
# Your specific counts will likely differ. Both states should be measured at roughly the same probability.
result.get_counts() returns the experiment results.
Sometimes the transpiled and original circuits differ from each other. You can monitor them with transpiled.draw() and circuit.draw(). In this case, the circuit is already in its most efficient form.
4b. Did it work?
It worked if only two outcomes appear, 00 and 11, splitting the shots roughly evenly, with 01 and 10 absent or negligible. Every shot, both coins agree. That agreement is the thing two classical coins cannot do.

Now go back to your Step 3b prediction and check it. If it matched, think about what you now think the cx gate does.
Check your answer
00 and 11 survive, at roughly half each. 01 and 10 vanish.
Half the shots the control reads 0, the target is left alone, and you get 00. Half the shots the control reads 1, the target flips, and you get 11. Neither qubit has a determined value before measurement; what is determined is that they will match.
By measuring one qubit, you know the other. The randomness is still there, all of it, but it is now shared instead of independent.
Step 5: Fixing an error message
Error messages are documentation, and the good ones are better than the docs. This step is designed to exercise a troubleshooting session, with a real error.
5a. Trigger it
Connect to an IBM QPU through the provider you have been using all along. Save this as task5_error.py and run it.
# task5_error.py, first version. Expected to raise an error.
from qhub.quantum.sdk import HubQiskitProvider
provider = HubQiskitProvider()
provider.get_backend('ibm.qpu.fez')
Read the traceback. It does not merely tell you that you are wrong. It prints a labelled migration guide, with the replacement lines written out.
5b. Repair it
Your task. Rewrite task5_error.py so it obtains a working handle on ibm.qpu.fez, using what the error message and documentation tells you. Prove the handle is live by printing the backend name. Do not submit to it yet.
Hint 1, where to look
Read the traceback. You can find the whole instruction there. Read the message body, not just its first line: the useful part is below the sentence that tells you what went wrong.
Hint 2, the shape of the answer
IBM's backends are not reachable through the provider you have been using, because they speak IBM's own runtime protocol rather than the Hub's native one.
There is a second entry point for exactly that case, and it is a sibling in the same qhub.quantum.sdk module. You construct it the same way and authenticate with your same credential.
backend.name holds the string you are looking for.
Solution
# task5_error.py, repaired.
from qhub.quantum.sdk import HubQiskitRuntimeService
service = HubQiskitRuntimeService()
backend = service.backend('ibm.qpu.fez')
print(backend.name)
# -> ibm.qpu.fez
# Nothing below this line. IBM hardware bills.
That is a live handle on a 156-qubit IBM machine, repaired by reading the error message. Resolving a handle costs nothing, because nothing was submitted.
The exception you triggered was BackendNotSupportedError. It reveals both the class and the method scoped to your exact wrong call and gave you the replacement lines with a migration guide.
The habit worth taking from this: read tracebacks to the last line before you open a browser tab. On a well-built SDK the answer is usually already in front of you.
Step 6: Find your job
Jobs outlive your process. You can see all your submissions under Quantum Workloads on the Quantum Hub. To find a specific one, filter by Job ID.
6a. From the SDK
Your task. Write task6_jobs.py. List your quantum jobs and print their id, the status, the backend and the shot count. Find the job id from Step 4 in the output. Use the HubQiskitProvider and provider.jobs() to list recent jobs.
Hint 1, where to look
The provider has one method for this, and it takes no arguments. The dir() pattern from Step 2's first hint surfaces it, and Managing quantum jobs covers the surface.
Print one listing entry's field list before you write the loop. The objects are not identical to the job you got back from backend.run().
Solution
# task6_jobs.py
from qhub.quantum.sdk import HubQiskitProvider
provider = HubQiskitProvider()
jobs = provider.jobs()
# jobs() returns one unsorted page of up to 50 jobs; order it newest first
jobs.sort(key=lambda j: j.created_at or '', reverse=True)
print(f'Recent Jobs: {len(jobs)}.')
for j in jobs[:5]:
print(f' {j.id} {str(j.status):<12} {str(j.backend_id):<24} shots={j.shots}')
Getting your jobs from Kipu Quantum Hub, this may take a few seconds...
Recent Jobs: 50.
XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX COMPLETED kipu.sim.qsim shots=1024
# -> your ids, count and history will differ.
6b. From the dashboard
Open the quantum workloads and find the job id from Step 4. You can Retrieve Inputs & Results and even Cancel Jobs from the interface.


In addition to the Web-UI, you can retrieve results of previous jobs using the Job ID and the HubQuantumClient (constructed with an api_key argument): Job Retrieval Reference.
Step 7: Kipu's Quantum Hub four layers
So far, you have been working in the bottom two layers of the four-layer Quantum Hub.

Read bottom up. You have already worked in two of them.
L0 Hardware. QPUs and simulators from several vendors. You worked with it in Step 2 and submitted to it in Step 4.
L1 Orchestration. Everything that carries a workload down to L0 and results back up: the SDK, the MCP server, qhubctl, the REST API, data pools, classical compute, observability. HubQiskitProvider is L1. Steps 1, 4, 5 and 6 were all L1 work.
L2 Services. Kipu's own algorithms, plus an open marketplace around them. Miray solves combinatorial optimization problems (a BF-DCQO solver), on simulator and hardware. Rimay does quantum machine learning (DQFE feature mapping), on simulator and hardware.
Each shows a price label of Free, On Request or Commercial. Miray Advanced Quantum Optimizer - Simulator is Free. Rimay - Quantum Feature Extraction - Simulator is Free. Illay Base Quantum Optimizer is Free. That means you can use them to test your quantum-centric workflows without being billed extra on top of your license. Later Tutorials will use them to teach how to use services.
L3 Applications. Actual products and demos you would ship to a customer. Komatsu predictive maintenance, KPMG satellite image recognition, DB Systel anomaly detection in network traffic. Browse the Marketplace use cases for more examples.
Optional, wire up the MCP server
L1 also exposes the Hub to AI coding agents over MCP. It is a remote HTTP server, so there is nothing to install. Check the MCP Server Reference for more details.
The server authenticates via OAuth. The first time your AI client connects, it opens a browser window where you log in to the Kipu Quantum Hub and authorize access. No tokens or credentials are stored in your config files.
Further exploration
Think about one candidate application from your own work. A real problem, in one sentence. At which layer would you build it?
- L3: there might be a L2 service you could build it on.
- L2: or lower, think about what is missing from the marketplace today.
- L1: you are building infrastructure, and we would like to offer you a job.
- L0: you are building hardware, and we would like to partner with you.
Business aside, we hope you enjoy building with the Kipu Hub. The team is super receptive to feedback. Please reach out and let us figure out a solution for your (quantum-centric) ideas.
The map is only worth something if your own problem fits on it.
Documentation: Kipu Quantum Hub | Hub docs | Quickstart | Quantum SDK reference | Access tokens | Managing quantum jobs | CLI reference | MCP server setup | Backends catalogue | Dashboard
Happy Optimizing 🚀