Repository: rigetti/qcs-sdk-rust (language: python)

Issue:
fix: deserialize null fidelity and duration as "perfect" values
See attached issue for details.

Closes #179.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index 80e88a991..e7145189a 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -11,6 +11,10 @@ jobs:
         with:
           profile: minimal
           toolchain: stable
+      - name: Install protoc
+        uses: arduino/setup-protoc@v1
+        with:
+          version: '3.20.1'
 
       - uses: Swatinem/rust-cache@v1
       - name: Install cargo-make
diff --git a/.gitignore b/.gitignore
index 9c5dd662c..4c9bc5cdc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 
 # Local config
 .env
+venv/
 
 # JetBrains
 .idea
diff --git a/Cargo.lock b/Cargo.lock
index 34e418431..74f52e89b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,44 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "anyhow"
+version = "1.0.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
+
+[[package]]
+name = "async-stream"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -19,6 +57,51 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "axum"
+version = "0.5.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-http",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9f0c0a60006f2a293d82d571f635042a72edf927539b7685bd62d361963839b"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "mime",
+ "tower-layer",
+ "tower-service",
+]
+
 [[package]]
 name = "base64"
 version = "0.13.0"
@@ -86,6 +169,16 @@ version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "chrono"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "cmake"
 version = "0.1.48"
@@ -95,6 +188,22 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
 [[package]]
 name = "cpufeatures"
 version = "0.2.5"
@@ -116,9 +225,9 @@ dependencies = [
 
 [[package]]
 name = "digest"
-version = "0.10.3"
+version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
+checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
 dependencies = [
  "block-buffer",
  "crypto-common",
@@ -144,6 +253,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "either"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+
 [[package]]
 name = "encoding_rs"
 version = "0.8.31"
@@ -210,6 +325,21 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
 [[package]]
 name = "form_urlencoded"
 version = "1.1.0"
@@ -422,6 +552,12 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[package]]
+name = "http-range-header"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"
+
 [[package]]
 name = "httparse"
 version = "1.8.0"
@@ -471,6 +607,31 @@ dependencies = [
  "tokio-rustls",
 ]
 
+[[package]]
+name = "hyper-timeout"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
+dependencies = [
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tokio-io-timeout",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
+dependencies = [
+ "bytes",
+ "hyper",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+]
+
 [[package]]
 name = "idna"
 version = "0.3.0"
@@ -512,11 +673,20 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b"
 
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
 
 [[package]]
 name = "js-sys"
@@ -527,6 +697,20 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "jsonwebtoken"
+version = "8.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa4b4af834c6cfd35d8763d359661b90f2e45d8f750a0849156c7f4671af09c"
+dependencies = [
+ "base64",
+ "pem",
+ "ring",
+ "serde",
+ "serde_json",
+ "simple_asn1",
+]
+
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -608,15 +792,15 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.132"
+version = "0.2.135"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
+checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
 
 [[package]]
 name = "lock_api"
-version = "0.4.8"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390"
+checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
 dependencies = [
  "autocfg",
  "scopeguard",
@@ -637,6 +821,12 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
 
+[[package]]
+name = "matchit"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb"
+
 [[package]]
 name = "memchr"
 version = "2.5.0"
@@ -697,6 +887,12 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "multimap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
+
 [[package]]
 name = "multipart"
 version = "0.18.0"
@@ -715,6 +911,24 @@ dependencies = [
  "twoway",
 ]
 
+[[package]]
+name = "native-tls"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9"
+dependencies = [
+ "lazy_static",
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
 [[package]]
 name = "nom"
 version = "7.1.1"
@@ -822,11 +1036,65 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "num_threads"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "once_cell"
-version = "1.14.0"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+
+[[package]]
+name = "openssl"
+version = "0.10.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
+checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
 
 [[package]]
 name = "parking_lot"
@@ -857,6 +1125,52 @@ version = "1.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
 
+[[package]]
+name = "pbjson"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "599fe9aefc2ca0df4a96179b3075faee2cacb89d4cf947a00b9a89152dfffc9d"
+dependencies = [
+ "base64",
+ "serde",
+]
+
+[[package]]
+name = "pbjson-build"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b237671f44cffe48853b93cba79d2613a62c8250b902a93f11f4be4539c84287"
+dependencies = [
+ "heck",
+ "itertools",
+ "prost",
+ "prost-types",
+]
+
+[[package]]
+name = "pbjson-types"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "588ae4a372e9fa9c20524dacd39e666b6abd29ff2830b9ebec5437e41634067d"
+dependencies = [
+ "bytes",
+ "chrono",
+ "pbjson",
+ "pbjson-build",
+ "prost",
+ "prost-build",
+ "serde",
+]
+
+[[package]]
+name = "pem"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
+dependencies = [
+ "base64",
+]
+
 [[package]]
 name = "percent-encoding"
 version = "2.2.0"
@@ -917,15 +1231,78 @@ version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
 
+[[package]]
+name = "prettyplease"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fead41e178796ef8274dc612a7d8ce4c7e10ca35cd2c5b5ad24cac63aeb6c0"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
 [[package]]
 name = "proc-macro2"
-version = "1.0.43"
+version = "1.0.46"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
 dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "prost"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "399c3c31cdec40583bb68f0b18403400d01ec4289c383aa047560439952c4dd7"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f835c582e6bd972ba8347313300219fed5bfa52caf175298d860b61ff6069bb"
+dependencies = [
+ "bytes",
+ "heck",
+ "itertools",
+ "lazy_static",
+ "log",
+ "multimap",
+ "petgraph",
+ "prost",
+ "prost-types",
+ "regex",
+ "tempfile",
+ "which",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7345d5f0e08c0536d7ac7229952590239e77abf0a0100a1b1d890add6ea96364"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dfaa718ad76a44b3415e6c4d53b17c8f99160dcb3a99b10470fce8ad43f6e3e"
+dependencies = [
+ "bytes",
+ "prost",
+]
+
 [[package]]
 name = "pyo3"
 version = "0.17.2"
@@ -1025,6 +1402,9 @@ dependencies = [
  "maplit",
  "num",
  "qcs-api",
+ "qcs-api-client-common",
+ "qcs-api-client-grpc",
+ "qcs-api-client-openapi",
  "quil-rs",
  "reqwest",
  "rmp-serde",
@@ -1036,6 +1416,7 @@ dependencies = [
  "thiserror",
  "tokio",
  "toml 0.5.9",
+ "tonic",
  "uuid",
  "warp",
  "zmq",
@@ -1054,6 +1435,55 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "qcs-api-client-common"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe995a0ff1f81031921a6429fb1b5829a1d92c8a28d0de6444c7917a97884a52"
+dependencies = [
+ "dirs",
+ "futures",
+ "jsonwebtoken",
+ "reqwest",
+ "serde",
+ "thiserror",
+ "tokio",
+ "toml 0.5.9",
+]
+
+[[package]]
+name = "qcs-api-client-grpc"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2e9dc8c73ce5da2266624af250405a4601a09de33f814271215aef6ed9c0ee7"
+dependencies = [
+ "http-body",
+ "pbjson",
+ "pbjson-build",
+ "pbjson-types",
+ "prost",
+ "prost-build",
+ "qcs-api-client-common",
+ "serde",
+ "thiserror",
+ "tonic",
+ "tonic-build",
+ "tower",
+]
+
+[[package]]
+name = "qcs-api-client-openapi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b9ed4f4287030fd2afa6a75e451b81a1dc30978150bad925f281306b8a8d516"
+dependencies = [
+ "qcs-api-client-common",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "url",
+]
+
 [[package]]
 name = "qcs-sdk-python"
 version = "0.2.1-rc.22"
@@ -1064,6 +1494,8 @@ dependencies = [
  "pythonize",
  "qcs",
  "qcs-api",
+ "qcs-api-client-common",
+ "quil-rs",
  "serde_json",
  "tokio",
 ]
@@ -1076,9 +1508,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
 
 [[package]]
 name = "quil-rs"
-version = "0.13.7"
+version = "0.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af63c2862d1efd67989fff78f430469fe764b76d280041420a9f3ca8236a6ef1"
+checksum = "d3e0177b20a1153ef459e63e21815a64a4a442c107edb55c831142c68e481988"
 dependencies = [
  "indexmap",
  "lexical",
@@ -1123,9 +1555,9 @@ dependencies = [
 
 [[package]]
 name = "rand_core"
-version = "0.6.3"
+version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
  "getrandom",
 ]
@@ -1150,6 +1582,21 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "regex"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
+dependencies = [
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
+
 [[package]]
 name = "remove_dir_all"
 version = "0.5.3"
@@ -1175,11 +1622,13 @@ dependencies = [
  "http-body",
  "hyper",
  "hyper-rustls",
+ "hyper-tls",
  "ipnet",
  "js-sys",
  "log",
  "mime",
  "mime_guess",
+ "native-tls",
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
@@ -1189,6 +1638,7 @@ dependencies = [
  "serde_json",
  "serde_urlencoded",
  "tokio",
+ "tokio-native-tls",
  "tokio-rustls",
  "tower-service",
  "url",
@@ -1248,6 +1698,18 @@ dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile 1.0.1",
+ "schannel",
+ "security-framework",
+]
+
 [[package]]
 name = "rustls-pemfile"
 version = "0.2.1"
@@ -1284,6 +1746,16 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
 
+[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys",
+]
+
 [[package]]
 name = "scoped-tls"
 version = "1.0.0"
@@ -1306,6 +1778,29 @@ dependencies = [
  "untrusted",
 ]
 
+[[package]]
+name = "security-framework"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
 [[package]]
 name = "serde"
 version = "1.0.145"
@@ -1371,9 +1866,9 @@ dependencies = [
 
 [[package]]
 name = "sha1"
-version = "0.10.4"
+version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549"
+checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -1389,6 +1884,18 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "simple_asn1"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
+dependencies = [
+ "num-bigint",
+ "num-traits",
+ "thiserror",
+ "time",
+]
+
 [[package]]
 name = "simple_logger"
 version = "2.3.0"
@@ -1411,9 +1918,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.9.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
+checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
 
 [[package]]
 name = "socket2"
@@ -1461,15 +1968,21 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.99"
+version = "1.0.102"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
+checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
 dependencies = [
  "proc-macro2",
  "quote",
  "unicode-ident",
 ]
 
+[[package]]
+name = "sync_wrapper"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8"
+
 [[package]]
 name = "target-lexicon"
 version = "0.12.4"
@@ -1510,6 +2023,24 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d634a985c4d4238ec39cacaed2e7ae552fbd3c476b552c1deac3021b7d7eaf0c"
+dependencies = [
+ "itoa",
+ "libc",
+ "num_threads",
+ "time-macros",
+]
+
+[[package]]
+name = "time-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
+
 [[package]]
 name = "tinyvec"
 version = "1.6.0"
@@ -1545,6 +2076,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "tokio-io-timeout"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
+dependencies = [
+ "pin-project-lite",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-macros"
 version = "1.8.0"
@@ -1556,6 +2097,16 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "tokio-native-tls"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
 [[package]]
 name = "tokio-rustls"
 version = "0.23.4"
@@ -1569,9 +2120,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-stream"
-version = "0.1.9"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9"
+checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
 dependencies = [
  "futures-core",
  "pin-project-lite",
@@ -1619,6 +2170,99 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "tonic"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55b9af819e54b8f33d453655bef9b9acc171568fb49523078d0cc4e7484200ec"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-timeout",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "prost-derive",
+ "rustls-native-certs",
+ "rustls-pemfile 1.0.1",
+ "tokio",
+ "tokio-rustls",
+ "tokio-stream",
+ "tokio-util",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+ "tracing-futures",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c6fd7c2581e36d63388a9e04c350c21beb7a8b059580b2e93993c526899ddc"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "prost-build",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap",
+ "pin-project",
+ "pin-project-lite",
+ "rand",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-range-header",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
 [[package]]
 name = "tower-service"
 version = "0.3.2"
@@ -1627,25 +2271,47 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
 
 [[package]]
 name = "tracing"
-version = "0.1.36"
+version = "0.1.37"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
 dependencies = [
  "cfg-if",
  "log",
  "pin-project-lite",
+ "tracing-attributes",
  "tracing-core",
 ]
 
+[[package]]
+name = "tracing-attributes"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "tracing-core"
-version = "0.1.29"
+version = "0.1.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
 dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "tracing-futures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
+dependencies = [
+ "pin-project",
+ "tracing",
+]
+
 [[package]]
 name = "try-lock"
 version = "0.2.3"
@@ -1703,15 +2369,15 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.4"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
 
 [[package]]
 name = "unicode-normalization"
-version = "0.1.21"
+version = "0.1.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
 dependencies = [
  "tinyvec",
 ]
@@ -1754,6 +2420,12 @@ dependencies = [
  "getrandom",
 ]
 
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
 [[package]]
 name = "version_check"
 version = "0.9.4"
@@ -1895,13 +2567,24 @@ dependencies = [
 
 [[package]]
 name = "webpki-roots"
-version = "0.22.4"
+version = "0.22.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
+checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be"
 dependencies = [
  "webpki",
 ]
 
+[[package]]
+name = "which"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b"
+dependencies = [
+ "either",
+ "libc",
+ "once_cell",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
diff --git a/README.md b/README.md
index d464c1954..2c3230e1e 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ In order to run all checks exactly the same way that CI does, use `makers ci-flo
 
 ### Commits
 
-Commits should follow the conventional commit syntax, with one of the following [scopes][scopes]:
+Commits should follow the conventional commit syntax, with one of the following [scopes](scopes):
 
 - `lib` or `rust`: changes to the rust SDK
 - `python`: changes to the Python bindings
diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml
index 15085bbff..d3dbb6f3a 100644
--- a/crates/lib/Cargo.toml
+++ b/crates/lib/Cargo.toml
@@ -21,7 +21,10 @@ lazy_static = "1.4.0"
 log = "0.4.17"
 num = "0.4.0"
 qcs-api = "0.2.1"
-quil-rs = "0.13.7"
+qcs-api-client-common = "0.2.3"
+qcs-api-client-openapi = "0.3.4"
+qcs-api-client-grpc = "0.2.4"
+quil-rs = "0.14"
 reqwest = { version = "0.11.12", default-features = false, features = ["rustls-tls", "json"] }
 rmp-serde = "1.1.1"
 serde = { version = "1.0.145", features = ["derive"] }
@@ -31,6 +34,7 @@ thiserror = "1.0.37"
 tokio = { version = "1.21.2", features = ["fs"] }
 toml = "0.5.9"
 uuid = { version = "1.2.1", features = ["v4"] }
+tonic = { version = "0.8.0", features = ["tls", "tls-roots"] }
 zmq = { version = "0.9.2", features = ["vendored"] }
 
 [dev-dependencies]
@@ -38,6 +42,7 @@ erased-serde = "0.3.23"
 float-cmp = "0.9.0"
 hex = "0.4.3"
 maplit = "1.0.2"
+qcs-api-client-grpc = { version = "0.2.4", features = ["server"] }
 simple_logger = { version = "2.3.0", default-features = false }
 tempfile = "3.3.0"
 tokio = { version = "1.21.2", features = ["macros", "rt-multi-thread"] }
diff --git a/crates/lib/examples/delayed_job_retrieval.rs b/crates/lib/examples/delayed_job_retrieval.rs
index 3a5a64b03..aec7fc164 100644
--- a/crates/lib/examples/delayed_job_retrieval.rs
+++ b/crates/lib/examples/delayed_job_retrieval.rs
@@ -14,10 +14,10 @@ async fn main() {
     let job_handle = exe
         .submit_to_qpu("Aspen-11")
         .await
-        .expect("Failed to submit");
+        .expect("Program should be sucessfully submitted for execution");
     // Do some other stuff
     let _data = exe
         .retrieve_results(job_handle)
         .await
-        .expect("Failed to retrieve results");
+        .expect("Results should be successfully retrieved");
 }
diff --git a/crates/lib/examples/execute.rs b/crates/lib/examples/execute.rs
index 61e495eef..fe628d97b 100644
--- a/crates/lib/examples/execute.rs
+++ b/crates/lib/examples/execute.rs
@@ -6,7 +6,6 @@ const PROGRAM: &str = r#"
 DECLARE ro BIT[2]
 DECLARE theta REAL
 RX(theta) 0
-X 0
 CNOT 0 1
 MEASURE 0 ro[0]
 MEASURE 1 ro[1]
@@ -15,13 +14,12 @@ MEASURE 1 ro[1]
 #[tokio::main]
 async fn main() {
     let mut exe = Executable::from_quil(PROGRAM);
-    let theta = PI;
 
     let result = exe
-        .with_parameter("theta", 0, theta)
+        .with_parameter("theta", 0, PI)
         .execute_on_qpu("Aspen-11")
         .await
-        .expect("Executed program on QPU");
+        .expect("Program should execute successfully");
 
     dbg!(&result);
 }
diff --git a/crates/lib/examples/local.rs b/crates/lib/examples/local.rs
new file mode 100644
index 000000000..903f286b4
--- /dev/null
+++ b/crates/lib/examples/local.rs
@@ -0,0 +1,26 @@
+use std::f64::consts::PI;
+
+use qcs::Executable;
+
+const PROGRAM: &str = r#"
+DECLARE ro BIT[2]
+DECLARE theta REAL
+RX(theta) 0
+X 0
+CNOT 0 1
+MEASURE 0 ro[0]
+MEASURE 1 ro[1]
+"#;
+
+#[tokio::main]
+async fn main() {
+    let mut exe = Executable::from_quil(PROGRAM);
+
+    let result = exe
+        .with_parameter("theta", 0, PI)
+        .execute_on_qpu("Aspen-11")
+        .await
+        .expect("Program should execute successfully");
+
+    println!("{:?}", result);
+}
diff --git a/crates/lib/examples/parametric_compilation.rs b/crates/lib/examples/parametric_compilation.rs
index 1e9136296..f699cc8f6 100644
--- a/crates/lib/examples/parametric_compilation.rs
+++ b/crates/lib/examples/parametric_compilation.rs
@@ -1,10 +1,11 @@
-//! This example implements the [Parametric Compilation example from pyQuil][pyquil] in Rust.
+//! This example implements the [Parametric Compilation example from pyQuil](pyquil) in Rust.
 //! [pyquil]: https://pyquil-docs.rigetti.com/en/stable/basics.html?highlight=parametric#parametric-compilation
 
 use std::f64::consts::PI;
 use std::time::Duration;
 
 use qcs::Executable;
+use qcs_api_client_grpc::models::controller::{readout_values::Values, IntegerReadoutValues};
 
 const PROGRAM: &str = r#"
 DECLARE ro BIT
@@ -27,7 +28,7 @@ async fn main() {
 
     for i in 0..=50 {
         let theta = step * f64::from(i);
-        let mut data = exe
+        let data = exe
             .with_parameter("theta", 0, theta)
             .execute_on_qpu("Aspen-11")
             .await
@@ -35,8 +36,23 @@ async fn main() {
         total_execution_time += data
             .duration
             .expect("Aspen-11 should always report duration");
-        let result = data.registers.remove("ro").expect("Missing ro register");
-        parametric_measurements.append(&mut result.into_i16().unwrap()[0])
+
+        let ro_readout_data = data
+            .readout_data
+            .get_readout_values_for_field("ro")
+            .expect("readout values should contain 'ro'")
+            .expect("'ro' should contain readout values");
+        let first_ro_data = ro_readout_data
+            .first()
+            .expect("'ro' should contain at least one readout value")
+            .clone();
+        let first_ro_values = first_ro_data
+            .expect("first readout value should ")
+            .values
+            .expect("'ro' should have readout values");
+        if let Values::IntegerValues(IntegerReadoutValues { mut values }) = first_ro_values {
+            parametric_measurements.append(&mut values)
+        }
     }
 
     println!("Total execution time: {:?}", total_execution_time);
diff --git a/crates/lib/examples/quil_t.rs b/crates/lib/examples/quil_t.rs
index ab64dbf1b..3779bf4d6 100644
--- a/crates/lib/examples/quil_t.rs
+++ b/crates/lib/examples/quil_t.rs
@@ -1,8 +1,9 @@
-//! This example runs a basic [Quil-T program from pyQuil][pyquil] in Rust.
+//! This example runs a basic [Quil-T program from pyQuil](pyquil) in Rust.
 //!
 //! [pyquil]: https://pyquil-docs.rigetti.com/en/stable/quilt_getting_started.html#Another-example:-a-simple-T1-experiment
 
 use qcs::Executable;
+use qcs_api_client_grpc::models::controller::{readout_values::Values, IntegerReadoutValues};
 
 /// This program doesn't do much, the main point is that it will fail if quilc is invoked.
 const PROGRAM: &str = r#"
@@ -21,10 +22,15 @@ async fn main() {
         .compile_with_quilc(false)
         .execute_on_qpu("Aspen-11")
         .await
-        .expect("Executed program on QPU")
-        .registers
-        .remove("ro")
-        .expect("Found ro register");
+        .expect("Program should execute successfully")
+        .readout_data
+        .get_readout_values("ro".to_string(), 0)
+        .expect("Readout data should include 'ro'")
+        .values
+        .expect("Readout data should include values");
 
-    assert!(result.as_i16().is_some());
+    match result {
+        Values::IntegerValues(IntegerReadoutValues { values }) => assert!(!values.is_empty()),
+        _ => panic!("expected IntegerReadoutValues, got {:?}", result),
+    }
 }
diff --git a/crates/lib/src/api.rs b/crates/lib/src/api.rs
index 3ce92f265..e62ab9490 100644
--- a/crates/lib/src/api.rs
+++ b/crates/lib/src/api.rs
@@ -1,36 +1,37 @@
-//! This module provides a small interface for doing compilation, translation,
-//! and execution.
+//! This module provides convenience functions to handle compilation,
+//! translation, parameter arithmetic rewriting, and results collection.
 
-use log::warn;
+use std::{collections::HashMap, str::FromStr};
+
+use qcs_api_client_grpc::{
+    models::controller::{readout_values, ControllerJobExecutionResult},
+    services::controller::{
+        get_controller_job_results_request::Target, GetControllerJobResultsRequest,
+    },
+};
+use quil_rs::expression::Expression;
 use quil_rs::{program::ProgramError, Program};
 use serde::Serialize;
 
-use crate::{
-    configuration::Configuration,
-    qpu::{
-        self, engagement,
-        quilc::{self, TargetDevice},
-        rewrite_arithmetic::{self, Substitutions},
-        rpcq::Client,
-        runner::{self, JobId},
-        translation,
-    },
+use crate::qpu::{
+    self,
+    client::{GrpcClientError, Qcs},
+    quilc::{self, TargetDevice},
+    rewrite_arithmetic::{self, Substitutions},
+    runner,
+    translation::{self, EncryptedTranslationResult},
+    IsaError,
 };
-use std::{collections::HashMap, convert::TryFrom};
 
 /// Uses quilc to convert a Quil program to native Quil
-///
-/// # Errors
-///
-/// See [`quilc::compile_program`].
 pub fn compile(
     quil: &str,
-    target_device: TargetDevice,
-    config: &Configuration,
+    target: TargetDevice,
+    client: &Qcs,
 ) -> Result<String, Box<dyn std::error::Error>> {
-    quilc::compile_program(quil, target_device, config)
-        .map_err(std::convert::Into::into)
-        .map(String::from)
+    quilc::compile_program(quil, target, client)
+        .map_err(Into::into)
+        .map(|p| p.to_string(true))
 }
 
 /// Collection of errors that can result from rewriting arithmetic.
@@ -66,11 +67,9 @@ pub struct RewriteArithmeticResult {
 /// May return an error if the program fails to parse, or the parameter arithmetic
 /// cannot be rewritten.
 pub fn rewrite_arithmetic(
-    native_quil: &str,
-) -> Result<RewriteArithmeticResult, RewriteArithmeticError> {
-    let program: Program = native_quil.parse()?;
-
-    let (program, subs) = qpu::rewrite_arithmetic::rewrite_arithmetic(program)?;
+    native_quil: quil_rs::Program,
+) -> Result<RewriteArithmeticResult, rewrite_arithmetic::Error> {
+    let (program, subs) = qpu::rewrite_arithmetic::rewrite_arithmetic(native_quil)?;
     let recalculation_table = subs.into_iter().map(|expr| expr.to_string()).collect();
 
     Ok(RewriteArithmeticResult {
@@ -79,22 +78,26 @@ pub fn rewrite_arithmetic(
     })
 }
 
+/// Errors that can happen during translation
+#[derive(Debug, thiserror::Error)]
+pub enum TranslationError {
+    /// The program could not be translated
+    #[error("Could not translate quil: {0}")]
+    Translate(#[from] GrpcClientError),
+    /// The result of translation could not be deserialized
+    #[error("Could not serialize translation result: {0}")]
+    Serialize(#[from] serde_json::Error),
+}
+
 /// The result of a call to [`translate`] which provides information about the
 /// translated program.
-#[derive(Clone, Debug, PartialEq, Default, Serialize)]
+#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize)]
 pub struct TranslationResult {
-    /// The memory defined in the program.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub memory_descriptors:
-        Option<::std::collections::HashMap<String, qcs_api::models::ParameterSpec>>,
     /// The translated program.
     pub program: String,
     #[serde(skip_serializing_if = "Option::is_none")]
     /// The memory locations used for readout.
-    pub ro_sources: Option<Vec<Vec<String>>>,
-    /// ISO8601 timestamp of the settings used to translate the program. Translation is deterministic; a program translated twice with the same settings by the same version of the service will have identical output.
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub settings_timestamp: Option<String>,
+    pub ro_sources: Option<HashMap<String, String>>,
 }
 
 /// Translates a native Quil program into an executable
@@ -106,21 +109,16 @@ pub async fn translate(
     native_quil: &str,
     shots: u16,
     quantum_processor_id: &str,
-    config: &Configuration,
-) -> Result<TranslationResult, translation::Error> {
-    let translation = translation::translate(
-        qpu::RewrittenQuil(native_quil.to_string()),
-        shots,
-        quantum_processor_id,
-        config,
-    )
-    .await?;
+    client: &Qcs,
+) -> Result<TranslationResult, TranslationError> {
+    let EncryptedTranslationResult { job, readout_map } =
+        translation::translate(quantum_processor_id, native_quil, shots.into(), client).await?;
+
+    let program = serde_json::to_string(&job)?;
 
     Ok(TranslationResult {
-        memory_descriptors: translation.memory_descriptors,
-        program: translation.program,
-        ro_sources: translation.ro_sources,
-        settings_timestamp: translation.settings_timestamp,
+        ro_sources: Some(readout_map),
+        program,
     })
 }
 
@@ -137,8 +135,8 @@ pub async fn submit(
     program: &str,
     patch_values: HashMap<String, Vec<f64>>,
     quantum_processor_id: &str,
-    config: &Configuration,
-) -> Result<String, runner::Error> {
+    client: &Qcs,
+) -> Result<String, SubmitError> {
     // Is there a better way to map these patch_values keys? This
     // negates the whole purpose of [`submit`] using `Box<str>`,
     // instead of `String` directly, which normally would decrease
@@ -147,19 +145,33 @@ pub async fn submit(
         .into_iter()
         .map(|(k, v)| (k.into_boxed_str(), v))
         .collect();
-    let engagement = engagement::get(String::from(quantum_processor_id), config)
-        .await
-        .map_err(|e| runner::Error::Qpu(format!("Unable to get engagement: {:?}", e)))?;
-    let rpcq_client = Client::try_from(&engagement).map_err(|e| {
-        warn!("Unable to connect to QPU via RPCQ: {:?}", e);
-        runner::Error::Connection(e)
-    })?;
 
-    let job_id = runner::submit(program, &patch_values, &rpcq_client)?;
+    let job = serde_json::from_str(program)?;
+    let job_id = runner::submit(quantum_processor_id, job, &patch_values, client).await?;
 
     Ok(job_id.0)
 }
 
+/// Errors that may occur when submitting a program for execution
+#[derive(Debug, thiserror::Error)]
+pub enum SubmitError {
+    /// Failed to fetch the desired ISA
+    #[error("Failed to fetch ISA: {0}")]
+    IsaError(#[from] IsaError),
+
+    /// Failed a gRPC API call
+    #[error("Failed a gRPC call: {0}")]
+    GrpcError(#[from] GrpcClientError),
+
+    /// Quilc compilation failed
+    #[error("Failed quilc compilation: {0}")]
+    QuilcError(#[from] quilc::Error),
+
+    /// Job could not be deserialized
+    #[error("Failed to deserialize job: {0}")]
+    DeserializeError(#[from] serde_json::Error),
+}
+
 /// Evaluate the expressions in `recalculation_table` using the numeric values
 /// provided in `memory`.
 ///
@@ -171,13 +183,14 @@ pub fn build_patch_values(
 ) -> Result<HashMap<Box<str>, Vec<f64>>, String> {
     let substitutions: Substitutions = recalculation_table
         .iter()
-        .map(|expr| expr.parse())
+        .map(|expr| Expression::from_str(expr))
         .collect::<Result<_, _>>()
-        .map_err(|e| format!("Unable to interpret recalc table: {:?}", e))?;
+        .map_err(|e| format!("Unable to interpret recalculation table: {:?}", e))?;
     rewrite_arithmetic::get_substitutions(&substitutions, memory)
 }
 
-/// A 64-bit complex number.
+/// A convenience type that describes a Complex-64 value whose real
+/// and imaginary parts of both f32.
 pub type Complex64 = [f32; 2];
 
 /// Data from an individual register. Each variant contains a vector with the expected data type
@@ -189,6 +202,8 @@ pub enum Register {
     F64(Vec<f64>),
     /// A register of 16-bit integers
     I16(Vec<i16>),
+    /// A register of 32-bit integers
+    I32(Vec<i32>),
     /// A register of 64-bit complex numbers
     Complex64(Vec<Complex64>),
     /// A register of 8-bit integers (bytes)
@@ -216,6 +231,28 @@ pub struct ExecutionResult {
     dtype: String,
 }
 
+impl From<readout_values::Values> for ExecutionResult {
+    fn from(values: readout_values::Values) -> Self {
+        match values {
+            readout_values::Values::ComplexValues(c) => Self {
+                shape: vec![c.values.len(), 1],
+                dtype: "complex".into(),
+                data: Register::Complex64(
+                    c.values
+                        .iter()
+                        .map(|c| [c.real.unwrap_or(0.0), c.imaginary.unwrap_or(0.0)])
+                        .collect(),
+                ),
+            },
+            readout_values::Values::IntegerValues(i) => Self {
+                shape: vec![i.values.len(), 1],
+                dtype: "integer".into(),
+                data: Register::I32(i.values),
+            },
+        }
+    }
+}
+
 /// Execution readout data for all memory locations.
 #[derive(Serialize)]
 pub struct ExecutionResults {
@@ -223,45 +260,49 @@ pub struct ExecutionResults {
     execution_duration_microseconds: Option<u64>,
 }
 
-/// Fetches results for the corresponding job
+impl From<ControllerJobExecutionResult> for ExecutionResults {
+    fn from(result: ControllerJobExecutionResult) -> Self {
+        let buffers = result
+            .readout_values
+            .into_iter()
+            .filter_map(|(key, value)| {
+                value
+                    .values
+                    .map(ExecutionResult::from)
+                    .map(|result| (key, result))
+            })
+            .collect();
+
+        Self {
+            buffers,
+            execution_duration_microseconds: result.execution_duration_microseconds,
+        }
+    }
+}
+
+/// Fetches results for the job
 ///
 /// # Errors
 ///
-/// May error if an engagement is not available, an RPCQ client cannot be built,
-/// or retrieval of results fails.
-///
-/// # Panics
-///
-/// Panics if a [`Register`] cannot be constructed from a result buffer.
+/// May error if a [`gRPC`] client cannot be constructed, or a [`gRPC`]
+/// call fails.
 pub async fn retrieve_results(
     job_id: &str,
     quantum_processor_id: &str,
-    config: &Configuration,
-) -> Result<ExecutionResults, runner::Error> {
-    let engagement = engagement::get(String::from(quantum_processor_id), config)
-        .await
-        .map_err(|e| runner::Error::Qpu(format!("Unable to get engagement: {:?}", e)))?;
-    let rpcq_client = Client::try_from(&engagement).map_err(|e| {
-        warn!("Unable to connect to QPU via RPCQ: {:?}", e);
-        runner::Error::Qpu(format!("Unable to connect to QPU via RPCQ: {:?}", e))
-    })?;
-
-    let results = runner::retrieve_results(JobId(job_id.to_string()), &rpcq_client)?;
-    let execution_duration_microseconds = results.execution_duration_microseconds;
-    let buffers = results
-        .buffers
-        .into_iter()
-        .map(|(name, buffer)| {
-            let shape = buffer.shape.clone();
-            let dtype = buffer.dtype.to_string();
-            // TODO Get rid of this unwrap.
-            let data = Register::from(qpu::runner::Register::try_from(buffer).unwrap());
-            (name, ExecutionResult { shape, data, dtype })
-        })
-        .collect::<HashMap<_, _>>();
+    client: &Qcs,
+) -> Result<ExecutionResults, GrpcClientError> {
+    let request = GetControllerJobResultsRequest {
+        job_execution_id: Some(job_id.into()),
+        target: Some(Target::QuantumProcessorId(quantum_processor_id.into())),
+    };
 
-    Ok(ExecutionResults {
-        buffers,
-        execution_duration_microseconds,
-    })
+    client
+        .get_controller_client(quantum_processor_id)
+        .await?
+        .get_controller_job_results(request)
+        .await?
+        .into_inner()
+        .result
+        .map(ExecutionResults::from)
+        .ok_or_else(|| GrpcClientError::ResponseEmpty("Controller Job Execution Results".into()))
 }
diff --git a/crates/lib/src/executable.rs b/crates/lib/src/executable.rs
index 6489b1378..d791f1377 100644
--- a/crates/lib/src/executable.rs
+++ b/crates/lib/src/executable.rs
@@ -5,19 +5,24 @@ use std::collections::HashMap;
 use std::sync::Arc;
 use std::time::Duration;
 
-use quil_rs::program::ProgramError;
-use quil_rs::Program;
+use qcs_api_client_common::configuration::LoadError;
+use qcs_api_client_common::ClientConfiguration;
 
-use crate::configuration::{Configuration, LoadError, RefreshError};
+use crate::execution_data;
+use crate::qpu::client::Qcs;
 use crate::qpu::rewrite_arithmetic;
-use crate::qpu::{engagement, ExecutionError, JobId};
-use crate::{qpu, qvm, ExecutionData};
+use crate::qpu::runner::JobId;
+use crate::qpu::ExecutionError;
+use crate::{qpu, qvm};
+use quil_rs::program::ProgramError;
+use quil_rs::Program;
 
 /// The builder interface for executing Quil programs on QVMs and QPUs.
 ///
 /// # Example
 ///
 /// ```rust
+/// use qcs_api_client_common::ClientConfiguration;
 /// use qcs::{Executable, RegisterData};
 ///
 ///
@@ -33,7 +38,7 @@ use crate::{qpu, qvm, ExecutionData};
 ///
 /// #[tokio::main]
 /// async fn main() {
-///     let mut result = Executable::from_quil(PROGRAM).with_shots(4).execute_on_qvm().await.unwrap();
+///     let mut result = Executable::from_quil(PROGRAM).with_config(ClientConfiguration::default()).with_shots(4).execute_on_qvm().await.unwrap();
 ///     // We know it's i8 because we declared the memory as `BIT` in Quil.
 ///     // "ro" is the only source read from by default if you don't specify a .read_from()
 ///     let data = result.registers.remove("ro").expect("Did not receive ro data").into_i8().unwrap();
@@ -63,7 +68,8 @@ pub struct Executable<'executable, 'execution> {
     readout_memory_region_names: Option<Vec<&'executable str>>,
     params: Parameters,
     compile_with_quilc: bool,
-    config: Option<Arc<Configuration>>,
+    config: Option<ClientConfiguration>,
+    client: Option<Arc<Qcs>>,
     qpu: Option<qpu::Execution<'execution>>,
     qvm: Option<qvm::Execution>,
 }
@@ -94,6 +100,7 @@ impl<'executable> Executable<'executable, '_> {
             params: Parameters::new(),
             compile_with_quilc: true,
             config: None,
+            client: None,
             qpu: None,
             qvm: None,
         }
@@ -113,6 +120,7 @@ impl<'executable> Executable<'executable, '_> {
     /// # Example
     ///
     /// ```rust
+    /// use qcs_api_client_common::ClientConfiguration;
     /// use qcs::Executable;
     ///
     /// const PROGRAM: &str = r#"
@@ -126,6 +134,7 @@ impl<'executable> Executable<'executable, '_> {
     /// #[tokio::main]
     /// async fn main() {
     ///     let mut result = Executable::from_quil(PROGRAM)
+    ///         .with_config(ClientConfiguration::default()) // Unnecessary if you have ~/.qcs/settings.toml
     ///         .read_from("first")
     ///         .read_from("second")
     ///         .execute_on_qvm()
@@ -169,6 +178,7 @@ impl<'executable> Executable<'executable, '_> {
     /// # Example
     ///
     /// ```rust
+    /// use qcs_api_client_common::ClientConfiguration;
     /// use qcs::Executable;
     ///
     /// const PROGRAM: &str = "DECLARE theta REAL[2]";
@@ -176,6 +186,7 @@ impl<'executable> Executable<'executable, '_> {
     /// #[tokio::main]
     /// async fn main() {
     ///     let mut exe = Executable::from_quil(PROGRAM)
+    ///         .with_config(ClientConfiguration::default()) // Unnecessary if you have ~/.qcs/settings.toml
     ///         .read_from("theta");
     ///     
     ///     for theta in 0..2 {
@@ -204,7 +215,7 @@ impl<'executable> Executable<'executable, '_> {
             .remove(&param_name)
             .unwrap_or_else(|| vec![0.0; index]);
 
-        if index + 1 > values.len() {
+        if index >= values.len() {
             values.resize(index + 1, 0.0);
         }
 
@@ -213,9 +224,17 @@ impl<'executable> Executable<'executable, '_> {
 
         self
     }
+
+    /// Set the default configuration to be used when constructing clients
+    #[must_use]
+    pub fn with_config(mut self, config: ClientConfiguration) -> Self {
+        self.config = Some(config);
+        self
+    }
 }
 
-type ExecuteResult = Result<ExecutionData, Error>;
+type ExecuteResultQVM = Result<execution_data::Qvm, Error>;
+type ExecuteResultQPU = Result<execution_data::Qpu, Error>;
 
 impl Executable<'_, '_> {
     /// Specify a number of times to run the program for each execution. Defaults to 1 run or "shot".
@@ -254,8 +273,9 @@ impl Executable<'_, '_> {
     /// # Errors
     ///
     /// See [`Error`].
-    pub async fn execute_on_qvm(&mut self) -> ExecuteResult {
-        let config = self.get_config().await.unwrap_or_default();
+    pub async fn execute_on_qvm(&mut self) -> ExecuteResultQVM {
+        let config = self.get_config().await?;
+
         let mut qvm = if let Some(qvm) = self.qvm.take() {
             qvm
         } else {
@@ -265,29 +285,34 @@ impl Executable<'_, '_> {
             .run(self.shots, self.get_readouts(), &self.params, &config)
             .await;
         self.qvm = Some(qvm);
-        result.map_err(Error::from).map(|registers| ExecutionData {
-            registers,
-            duration: None,
-        })
+        result
+            .map_err(Error::from)
+            .map(|registers| execution_data::Qvm {
+                registers,
+                duration: None,
+            })
     }
 
-    /// Load `self.config` if not yet loaded, then return a reference to it.
-    async fn get_config(&mut self) -> Result<Arc<Configuration>, Error> {
+    async fn get_config(&mut self) -> Result<ClientConfiguration, Error> {
         if let Some(config) = &self.config {
             Ok(config.clone())
         } else {
-            let config = Arc::new(Configuration::load().await?);
+            let config = ClientConfiguration::load().await?;
             self.config = Some(config.clone());
             Ok(config)
         }
     }
 
-    /// Refresh `self.config` and return it.
-    async fn refresh_config(&mut self) -> Result<Arc<Configuration>, Error> {
-        let config = self.get_config().await?.as_ref().clone();
-        let refreshed = Arc::new(config.refresh().await?);
-        self.config = Some(refreshed.clone());
-        Ok(refreshed)
+    /// Load `self.client` if not yet loaded, then return a reference to it.
+    async fn get_client(&mut self) -> Result<Arc<Qcs>, Error> {
+        if let Some(client) = &self.client {
+            Ok(client.clone())
+        } else {
+            let config = self.get_config().await?;
+            let client = Arc::new(Qcs::with_config(config));
+            self.client = Some(client.clone());
+            Ok(client)
+        }
     }
 }
 
@@ -302,31 +327,15 @@ impl<'execution> Executable<'_, 'execution> {
                 return Ok(qpu);
             }
         }
-        let mut config = self.get_config().await?;
-        match qpu::Execution::new(
+        qpu::Execution::new(
             self.quil.clone(),
             self.shots,
             id,
-            config.clone(),
+            self.get_client().await?,
             self.compile_with_quilc,
         )
         .await
-        {
-            Ok(qpu) => Ok(qpu),
-            Err(ExecutionError::Unauthorized) => {
-                config = self.refresh_config().await?;
-                qpu::Execution::new(
-                    self.quil.clone(),
-                    self.shots,
-                    id,
-                    config,
-                    self.compile_with_quilc,
-                )
-                .await
-                .map_err(Error::from)
-            }
-            Err(err) => Err(Error::from(err)),
-        }
+        .map_err(Error::from)
     }
 
     /// Compile the program and execute it on a QPU, waiting for results.
@@ -355,7 +364,10 @@ impl<'execution> Executable<'_, 'execution> {
     /// 1. Missing parameters that should be filled with [`Executable::with_parameter`]
     ///
     /// [quilc]: https://github.com/quil-lang/quilc
-    pub async fn execute_on_qpu(&mut self, quantum_processor_id: &'execution str) -> ExecuteResult {
+    pub async fn execute_on_qpu(
+        &mut self,
+        quantum_processor_id: &'execution str,
+    ) -> ExecuteResultQPU {
         let job_handle = self.submit_to_qpu(quantum_processor_id).await?;
         self.retrieve_results(job_handle).await
     }
@@ -372,28 +384,22 @@ impl<'execution> Executable<'_, 'execution> {
         &mut self,
         quantum_processor_id: &'execution str,
     ) -> Result<JobHandle<'execution>, Error> {
-        let mut qpu = self.qpu_for_id(quantum_processor_id).await?;
-        let mut config = self.get_config().await?;
-
-        let response = match qpu.submit(&self.params, self.get_readouts(), &config).await {
-            Ok(response) => Ok(response),
-            Err(ExecutionError::Unauthorized) => {
-                config = self.refresh_config().await?;
-                qpu.submit(&self.params, self.get_readouts(), &config).await
-            }
-            Err(err) => Err(err),
-        }
-        .map(|job_id| JobHandle {
+        let JobHandle {
+            job_id,
+            readout_map,
+            ..
+        } = self
+            .qpu_for_id(quantum_processor_id)
+            .await?
+            .submit(&self.params)
+            .await?;
+
+        // The unpacking/re-packing here is to avoid lifetime issues.
+        Ok(JobHandle {
             job_id,
             quantum_processor_id,
-        });
-        self.qpu = if let Err(ExecutionError::QpuUnavailable(_)) = response {
-            // Could mean maintenance which requires recompile
-            None
-        } else {
-            Some(qpu)
-        };
-        response.map_err(Error::from)
+            readout_map,
+        })
     }
 
     /// Wait for the results of a job submitted via [`Executable::submit_to_qpu`] to complete.
@@ -401,9 +407,12 @@ impl<'execution> Executable<'_, 'execution> {
     /// # Errors
     ///
     /// See [`Executable::execute_on_qpu`].
-    pub async fn retrieve_results(&mut self, job_handle: JobHandle<'execution>) -> ExecuteResult {
+    pub async fn retrieve_results(
+        &mut self,
+        job_handle: JobHandle<'execution>,
+    ) -> ExecuteResultQPU {
         let qpu = self.qpu_for_id(job_handle.quantum_processor_id).await?;
-        qpu.retrieve_results(job_handle.job_id)
+        qpu.retrieve_results(job_handle.job_id, job_handle.readout_map)
             .await
             .map_err(Error::from)
     }
@@ -470,9 +479,9 @@ pub enum Error {
     /// [`Executable::retrieve_results`] can invalidate the handle.
     #[error("The job handle was not valid")]
     InvalidJobHandle,
-    /// This error indicates that there was no current QPU engagement available for the user.
-    #[error("No engagement for QPU: {0}")]
-    NoEngagement(engagement::Error),
+    /// Occurs when failing to construct a [`QcsClient`].
+    #[error("The QCS client configuration failed to load")]
+    QcsConfigLoadFailure(#[from] LoadError),
 }
 
 #[derive(Debug, Copy, Clone, Eq, PartialEq)]
@@ -502,42 +511,18 @@ pub enum Service {
     Qpu,
 }
 
-impl From<LoadError> for Error {
-    fn from(err: LoadError) -> Self {
-        Self::Settings(format!("{}", err))
-    }
-}
-
-impl From<RefreshError> for Error {
-    fn from(err: RefreshError) -> Self {
-        match err {
-            RefreshError::NoRefreshToken => Self::Settings(String::from(
-                "No `refresh_token` was found in your QCS secrets file for the selected profile. \
-                    You can change profiles with the `QCS_PROFILE_NAME` environment variable.",
-            )),
-            RefreshError::FetchError(_) => Self::Authentication,
-        }
-    }
-}
-
 impl From<ExecutionError> for Error {
     fn from(err: ExecutionError) -> Self {
         match err {
-            ExecutionError::NoEngagement(e) => Self::NoEngagement(e),
-            ExecutionError::QpuNotFound => Self::QpuNotFound,
-            ExecutionError::QpuUnavailable(duration) => Self::QpuUnavailable(duration),
-            ExecutionError::Unauthorized => Self::Authentication,
-            ExecutionError::QcsCommunication => Self::Connection(Service::Qcs),
-            ExecutionError::Quil(e) => Self::Quil(e),
             ExecutionError::Unexpected(inner) => Self::Unexpected(format!("{:?}", inner)),
             ExecutionError::Quilc { .. } => Self::Connection(Service::Quilc),
-            ExecutionError::Qcs(message) => Self::Unexpected(message),
-            ExecutionError::ProgramNotSubmitted => Self::InvalidJobHandle,
+            ExecutionError::QcsClient(v) => Self::Unexpected(format!("{:?}", v)),
+            ExecutionError::IsaError(v) => Self::Unexpected(format!("{:?}", v)),
+            ExecutionError::ReadoutParse(v) => Self::Unexpected(format!("{:?}", v)),
+            ExecutionError::Quil(e) => Self::Quil(e),
             ExecutionError::Compilation { details } => Self::Compilation(details),
-            ExecutionError::Translation(message) => Self::Translation(message),
             ExecutionError::RewriteArithmetic(e) => Self::RewriteArithmetic(e),
             ExecutionError::Substitution(message) => Self::Substitution(message),
-            ExecutionError::MissingRoSources => Self::MissingRoSources,
         }
     }
 }
@@ -557,18 +542,58 @@ impl From<qvm::Error> for Error {
 
 /// The result of calling [`Executable::submit_to_qpu`]. Represents a quantum program running on
 /// a QPU. Can be passed to [`Executable::retrieve_results`] to retrieve the results of the job.
-#[derive(Debug, Clone, Eq, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct JobHandle<'executable> {
     job_id: JobId,
     quantum_processor_id: &'executable str,
+    readout_map: HashMap<String, String>,
 }
 
-impl JobHandle<'_> {
+impl<'a> JobHandle<'a> {
+    #[must_use]
+    pub(crate) fn new(
+        job_id: JobId,
+        quantum_processor_id: &'a str,
+        readout_map: HashMap<String, String>,
+    ) -> Self {
+        Self {
+            job_id,
+            quantum_processor_id,
+            readout_map,
+        }
+    }
+
     /// The string representation of the QCS Job ID. Useful for debugging.
     #[must_use]
     pub fn job_id(&self) -> &str {
         self.job_id.0.as_str()
     }
+
+    /// The readout map from source readout memory locations to the
+    /// filter pipeline node which publishes the data.
+    #[must_use]
+    pub fn readout_map(&self) -> &HashMap<String, String> {
+        &self.readout_map
+    }
+}
+
+#[cfg(test)]
+mod describe_get_config {
+    use qcs_api_client_openapi::common::ClientConfiguration;
+
+    use crate::Executable;
+
+    #[tokio::test]
+    async fn it_resizes_params_dynamically() {
+        let mut exe = Executable::from_quil("").with_config(ClientConfiguration::default());
+        let foo_len = |exe: &mut Executable| exe.params.get("foo").unwrap().len();
+
+        exe.with_parameter("foo", 0, 0.0);
+        assert_eq!(foo_len(&mut exe), 1);
+
+        exe.with_parameter("foo", 10, 10.0);
+        assert_eq!(foo_len(&mut exe), 11);
+    }
 }
 
 #[cfg(test)]
diff --git a/crates/lib/src/execution_data.rs b/crates/lib/src/execution_data.rs
index 731e8a900..fe27355cc 100644
--- a/crates/lib/src/execution_data.rs
+++ b/crates/lib/src/execution_data.rs
@@ -1,16 +1,20 @@
 use std::collections::HashMap;
+use std::convert::TryFrom;
+use std::num::{ParseIntError, TryFromIntError};
+use std::str::FromStr;
 use std::time::Duration;
 
+use qcs_api_client_grpc::models::controller::ReadoutValues;
+use quil_rs::instruction::MemoryReference;
+
 use crate::RegisterData;
 
-/// The result of executing an [`Executable`][crate::Executable] via
-/// [`Executable::execute_on_qvm`][crate::Executable::execute_on_qvm] or
-/// [`Executable::execute_on_qpu`][crate::Executable::execute_on_qpu].
+/// The result of executing an [`Executable`](crate::Executable) via
+/// [`Executable::execute_on_qvm`](crate::Executable::execute_on_qvm).
 #[derive(Debug, Clone, PartialEq)]
-pub struct ExecutionData {
-    /// The data of all registers that were read from
-    /// (via [`Executable::read_from`][crate::Executable::read_from]). Key is the name of the
-    /// register, value is the data of the register after execution.
+pub struct Qvm {
+    /// The readout data that was read from the [`Executable`](crate::Executable).
+    /// Key is the name of the register, value is the data of the register after execution.
     pub registers: HashMap<Box<str>, RegisterData>,
     /// The time it took to execute the program on the QPU, not including any network or queueing
     /// time. If paying for on-demand execution, this is the amount you will be billed for.
@@ -18,3 +22,182 @@ pub struct ExecutionData {
     /// This will always be `None` for QVM execution.
     pub duration: Option<Duration>,
 }
+
+#[derive(Debug, Clone, PartialEq)]
+#[repr(transparent)]
+pub struct ReadoutMap(HashMap<MemoryReference, ReadoutValues>);
+
+impl ReadoutMap {
+    /// Given a known readout field name and index, return the result's [`ReadoutValues`], if any.
+    pub fn get_readout_values(&self, field: String, index: u64) -> Option<ReadoutValues> {
+        let readout_values = self.0.get(&MemoryReference { name: field, index })?;
+
+        Some(readout_values.clone())
+    }
+
+    /// Given a known readout field name, return the result's [`ReadoutValues`] for all indices, if any.
+    pub fn get_readout_values_for_field(
+        &self,
+        field: &str,
+    ) -> Result<Option<Vec<Option<ReadoutValues>>>, TryFromIntError> {
+        let mut readout_values = Vec::new();
+        for (memref, values) in &self.0 {
+            let MemoryReference { name, index } = memref;
+            let index = usize::try_from(*index)?;
+            if name == field {
+                if readout_values.len() <= index {
+                    readout_values.resize(index + 1, None);
+                }
+                readout_values[index] = Some(values.clone());
+            }
+        }
+
+        Ok((!readout_values.is_empty()).then_some(readout_values))
+    }
+
+    /// `readout_values` maps program-defined readout to result-defined readout, e.g.:
+    ///     { "ro[0]": "q0", "ro[1]": "q1" }
+    /// where `ro[0]` is defined in the original program, and `q0` is what comes back in execution results.
+    /// Here we map the result-defined readout values back to their original result-defined names.
+    pub(crate) fn from_mappings_and_values(
+        readout_mappings: &HashMap<String, String>,
+        readout_values: &HashMap<String, ReadoutValues>,
+    ) -> Self {
+        let result = readout_values
+            .iter()
+            .flat_map(|(readout_name, values)| {
+                readout_mappings
+                    .iter()
+                    .filter(|(_, program_alias)| *program_alias == readout_name)
+                    .map(|(program_name, _)| program_name.as_ref())
+                    .map(parse_readout_register)
+                    .map(|reference| Ok((reference?, values.clone())))
+                    .collect::<Result<Vec<_>, MemoryReferenceParseError>>()
+            })
+            .flatten()
+            .collect::<HashMap<_, _>>()
+            .into();
+
+        result
+    }
+}
+
+impl From<HashMap<MemoryReference, ReadoutValues>> for ReadoutMap {
+    fn from(map: HashMap<MemoryReference, ReadoutValues>) -> Self {
+        Self(map)
+    }
+}
+
+/// The result of executing an [`Executable`](crate::Executable) via
+/// [`Executable::execute_on_qpu`](crate::Executable::execute_on_qpu).
+#[derive(Debug, Clone, PartialEq)]
+pub struct Qpu {
+    /// The data of all readout data that were read from
+    /// (via [`Executable::read_from`](crate::Executable::read_from)). Key is the name of the
+    /// register, value is the data of the register after execution.
+    pub readout_data: ReadoutMap,
+    /// The time it took to execute the program on the QPU, not including any network or queueing
+    /// time. If paying for on-demand execution, this is the amount you will be billed for.
+    ///
+    /// This will always be `None` for QVM execution.
+    pub duration: Option<Duration>,
+}
+
+#[derive(Debug, thiserror::Error)]
+pub(crate) enum MemoryReferenceParseError {
+    #[error("Could not parse memory reference: {reason}")]
+    InvalidFormat { reason: String },
+
+    #[error("Could not parse index from reference name: {0}")]
+    InvalidIndex(#[from] ParseIntError),
+}
+
+// Note: MemoryReference may have a from_string in the fututre
+fn parse_readout_register(
+    register_name: &str,
+) -> Result<MemoryReference, MemoryReferenceParseError> {
+    let open_brace =
+        register_name
+            .find('[')
+            .ok_or_else(|| MemoryReferenceParseError::InvalidFormat {
+                reason: "Opening brace not found".into(),
+            })?;
+    let close_brace =
+        register_name
+            .find(']')
+            .ok_or_else(|| MemoryReferenceParseError::InvalidFormat {
+                reason: "Closing brace not found".into(),
+            })?;
+
+    Ok(MemoryReference {
+        name: String::from(&register_name[..open_brace]),
+        index: u64::from_str(&register_name[open_brace + 1..close_brace])?,
+    })
+}
+
+#[cfg(test)]
+mod describe_readout_map {
+    use maplit::hashmap;
+
+    use super::{ReadoutMap, ReadoutValues};
+    use qcs_api_client_grpc::models::controller::readout_values::Values;
+    use qcs_api_client_grpc::models::controller::IntegerReadoutValues;
+
+    fn dummy_readout_values(v: i32) -> ReadoutValues {
+        ReadoutValues {
+            values: Some(Values::IntegerValues(IntegerReadoutValues {
+                values: vec![v],
+            })),
+        }
+    }
+
+    #[test]
+    fn it_converts_from_translation_readout_mappings() {
+        let readout_mappings = hashmap! {
+            String::from("ro[1]") => String::from("qA"),
+            String::from("ro[2]") => String::from("qB"),
+            String::from("ro[0]") => String::from("qC"),
+            String::from("bar[3]") => String::from("qD"),
+            String::from("bar[5]") => String::from("qE"),
+        };
+
+        let readout_values = hashmap! {
+            String::from("qA") => dummy_readout_values(11),
+            String::from("qB") => dummy_readout_values(22),
+            String::from("qD") => dummy_readout_values(33),
+            String::from("qE") => dummy_readout_values(44),
+        };
+
+        let readout_map = ReadoutMap::from_mappings_and_values(&readout_mappings, &readout_values);
+        let ro = readout_map
+            .get_readout_values_for_field("ro")
+            .unwrap()
+            .expect("ReadoutMap should have field `ro`");
+
+        assert_eq!(
+            ro,
+            vec![
+                None,
+                Some(dummy_readout_values(11)),
+                Some(dummy_readout_values(22)),
+            ],
+        );
+
+        let bar = readout_map
+            .get_readout_values_for_field("bar")
+            .unwrap()
+            .expect("ReadoutMap should have field `bar`");
+
+        assert_eq!(
+            bar,
+            vec![
+                None,
+                None,
+                None,
+                Some(dummy_readout_values(33)),
+                None,
+                Some(dummy_readout_values(44))
+            ],
+        );
+    }
+}
diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs
index 9c3aabab7..0830541a8 100644
--- a/crates/lib/src/lib.rs
+++ b/crates/lib/src/lib.rs
@@ -3,6 +3,7 @@
 #![deny(clippy::cargo)]
 #![allow(clippy::multiple_crate_versions)] // This should be enforced by cargo-deny
 #![deny(missing_docs)]
+#![allow(clippy::missing_errors_doc)]
 #![forbid(unsafe_code)]
 
 //! This crate is the primary Rust API for interacting with Rigetti products. Specifically, this
@@ -10,11 +11,10 @@
 //! using [`Executable`].
 
 pub use executable::{Error, Executable, JobHandle, Service};
-pub use execution_data::ExecutionData;
+pub use execution_data::Qvm;
 pub use register_data::RegisterData;
 
 pub mod api;
-pub mod configuration;
 mod executable;
 mod execution_data;
 pub mod qpu;
diff --git a/crates/lib/src/qpu/client.rs b/crates/lib/src/qpu/client.rs
new file mode 100644
index 000000000..726d3c762
--- /dev/null
+++ b/crates/lib/src/qpu/client.rs
@@ -0,0 +1,206 @@
+//! This module provides methods for getting clients for the
+//! desired API (e.g. ``gRPC`` or ``OpenAPI``) and will properly
+//! initialize those clients (e.g. with authentication metadata).
+
+use qcs_api_client_common::ClientConfiguration;
+use qcs_api_client_grpc::{
+    channel::{get_channel, parse_uri, wrap_channel_with, RefreshService},
+    services::{
+        controller::controller_client::ControllerClient,
+        translation::translation_client::TranslationClient,
+    },
+};
+use qcs_api_client_openapi::apis::{
+    configuration::Configuration as OpenApiConfiguration,
+    endpoints_api::{get_default_endpoint, GetDefaultEndpointError},
+    quantum_processors_api::{
+        list_quantum_processor_accessors, ListQuantumProcessorAccessorsError,
+    },
+};
+use qcs_api_client_openapi::models::QuantumProcessorAccessorType;
+use tonic::transport::{Channel, Uri};
+use tonic::Status;
+
+pub use qcs_api_client_common::configuration::LoadError;
+pub use qcs_api_client_grpc::channel::Error as GrpcError;
+pub use qcs_api_client_openapi::apis::Error as OpenApiError;
+
+/// A client providing helper functionality for accessing QCS APIs
+#[derive(Clone, Default)]
+pub struct Qcs {
+    config: ClientConfiguration,
+    /// When enabled, default to Gateway service for execution. Fallback to QPU's default endpoint otherwise.
+    use_gateway: bool,
+}
+
+impl Qcs {
+    /// Create a [`Qcs`] and initialize it with the user's default [`ClientConfiguration`]
+    pub async fn load() -> Result<Self, LoadError> {
+        ClientConfiguration::load().await.map(Self::with_config)
+    }
+
+    /// Create a [`Qcs`] and initialize it with the given [`ClientConfiguration`]
+    #[must_use]
+    pub fn with_config(config: ClientConfiguration) -> Self {
+        Self {
+            config,
+            use_gateway: true,
+        }
+    }
+
+    /// Enable or disable the use of Gateway service for execution
+    #[must_use]
+    pub fn with_use_gateway(mut self, use_gateway: bool) -> Self {
+        self.use_gateway = use_gateway;
+        self
+    }
+
+    pub(crate) fn get_config(&self) -> ClientConfiguration {
+        self.config.clone()
+    }
+
+    pub(crate) async fn get_controller_client(
+        &self,
+        quantum_processor_id: &str,
+    ) -> Result<ControllerClient<RefreshService<Channel>>, GrpcEndpointError> {
+        self.get_controller_endpoint(quantum_processor_id)
+            .await
+            .map(get_channel)
+            .map(|channel| wrap_channel_with(channel, self.get_config()))
+            .map(ControllerClient::new)
+    }
+
+    pub(crate) fn get_openapi_client(&self) -> OpenApiConfiguration {
+        OpenApiConfiguration::with_qcs_config(self.get_config())
+    }
+
+    pub(crate) fn get_translation_client(
+        &self,
+    ) -> Result<TranslationClient<RefreshService<Channel>>, GrpcError> {
+        self.get_translation_client_with_endpoint(self.get_config().grpc_api_url())
+    }
+
+    pub(crate) fn get_translation_client_with_endpoint(
+        &self,
+        translation_grpc_endpoint: &str,
+    ) -> Result<TranslationClient<RefreshService<Channel>>, GrpcError> {
+        parse_uri(translation_grpc_endpoint)
+            .map(get_channel)
+            .map(|channel| wrap_channel_with(channel, self.get_config()))
+            .map(TranslationClient::new)
+    }
+
+    async fn get_controller_endpoint(
+        &self,
+        quantum_processor_id: &str,
+    ) -> Result<Uri, GrpcEndpointError> {
+        if self.use_gateway {
+            let gateway = self.get_gateway_endpoint(quantum_processor_id).await;
+            // when no gateway is available, we should fall through and attempt a direct connection
+            if gateway.is_ok() {
+                return gateway;
+            }
+        }
+        self.get_controller_default_endpoint(quantum_processor_id)
+            .await
+    }
+
+    /// Get address for direction connection to Controller.
+    async fn get_controller_default_endpoint(
+        &self,
+        quantum_processor_id: &str,
+    ) -> Result<Uri, GrpcEndpointError> {
+        let default_endpoint =
+            get_default_endpoint(&self.get_openapi_client(), quantum_processor_id).await?;
+        let addresses = default_endpoint.addresses.as_ref();
+        let grpc_address = addresses.grpc.as_ref();
+        grpc_address
+            .ok_or_else(|| GrpcEndpointError::NoEndpoint(quantum_processor_id.into()))
+            .map(|v| parse_uri(v).map_err(GrpcEndpointError::BadUri))?
+    }
+
+    /// Get address for Gateway assigned to the provided `quantum_processor_id`, if one exists.
+    async fn get_gateway_endpoint(
+        &self,
+        quantum_processor_id: &str,
+    ) -> Result<Uri, GrpcEndpointError> {
+        // TODO: Env override is temporary, to allow for testing. Remove before merge.
+        let gateway_addr = if let Ok(addr) = std::env::var("GATEWAY_ADDR") {
+            addr
+        } else {
+            let accessors = list_quantum_processor_accessors(
+                &self.get_openapi_client(),
+                quantum_processor_id,
+                Some(100),
+                None,
+            )
+            .await?;
+            // TODO: paginate until target found or no more pages
+            let target = accessors
+                .accessors
+                .into_iter()
+                .find(|acc| {
+                    acc.live
+                        && acc.priority.unwrap_or(false)
+                        && acc.access_type.is_some()
+                        && acc.access_type.as_ref().unwrap().as_ref()
+                            == &QuantumProcessorAccessorType::GatewayV1
+                })
+                .ok_or_else(|| GrpcEndpointError::NoEndpoint(quantum_processor_id.into()))?;
+            target.url
+        };
+        parse_uri(&gateway_addr).map_err(GrpcEndpointError::BadUri)
+    }
+}
+
+/// Errors that may occur while trying to resolve a ``gRPC`` endpoint
+#[derive(Debug, thiserror::Error)]
+pub enum GrpcEndpointError {
+    /// Error due to a malformed URI
+    #[error("Malformed URI for endpoint: {0}")]
+    BadUri(#[from] GrpcError),
+
+    /// Error due to failure to get endpoint for quantum processor
+    #[error("Failed to get endpoint for quantum processor: {0}")]
+    RequestFailed(#[from] OpenApiError<GetDefaultEndpointError>),
+
+    /// Error due to failure to get accessors for quantum processor
+    #[error("Failed to get accessors for quantum processor: {0}")]
+    AccessorRequestFailed(#[from] OpenApiError<ListQuantumProcessorAccessorsError>),
+
+    /// Error due to missing gRPC endpoint for quantum processor
+    #[error("Missing gRPC endpoint for quantum processor {0}")]
+    NoEndpoint(String),
+}
+
+/// Errors that may occur while trying to use a ``gRPC`` client
+#[derive(Debug, thiserror::Error)]
+pub enum GrpcClientError {
+    /// Error due to failure to resolve the endpoint
+    #[error("Failed to resolve the gRPC endoint: {0}")]
+    EndpointNotResolved(#[from] GrpcEndpointError),
+
+    /// Error due to failure during request
+    #[error("Call failed during gRPC request: {0}")]
+    RequestFailed(#[from] Status),
+
+    /// Error due to response body missing required data
+    #[error("Response body had missing data: {0}")]
+    ResponseEmpty(String),
+
+    /// Error due to ``gRPC`` error
+    #[error("gRPC error: {0}")]
+    GrpcError(#[from] GrpcError),
+}
+
+/// Errors that may occur while trying to use a [`OpenAPI`] client
+#[derive(Debug, thiserror::Error)]
+pub enum OpenApiClientError<T> {
+    /// Error due to request failure
+    #[error("Call failed during http request: {0}")]
+    RequestFailed(#[from] OpenApiError<T>),
+
+    /// Error due to empty response
+    #[error("Response value was empty: {0}")]
+    ResponseEmpty(String),
+}
diff --git a/crates/lib/src/qpu/engagement.rs b/crates/lib/src/qpu/engagement.rs
deleted file mode 100644
index a7092aa1d..000000000
--- a/crates/lib/src/qpu/engagement.rs
+++ /dev/null
@@ -1,98 +0,0 @@
-use std::time::Duration;
-
-use reqwest::StatusCode;
-
-use qcs_api::apis::configuration::Configuration as ApiConfig;
-use qcs_api::models::{CreateEngagementRequest, EngagementWithCredentials, Error as ErrorPayload};
-
-use crate::configuration::Configuration;
-
-/// Try to get an engagement for a QPU
-pub(crate) async fn get(
-    quantum_processor_id: String,
-    config: &Configuration,
-) -> Result<EngagementWithCredentials, Error> {
-    let request = CreateEngagementRequest {
-        account_id: None,
-        account_type: None,
-        endpoint_id: None,
-        quantum_processor_id: Some(quantum_processor_id),
-        tags: None,
-    };
-    create_engagement(config.as_ref(), request).await
-}
-
-/// Create a new engagement using the specified parameters.
-///
-/// # Manual Implementation
-///
-/// This function is derived from [`qcs-api`] but implemented manually because of limitations in
-/// that generator. Specifically, they provide no access to headers.
-async fn create_engagement(
-    configuration: &ApiConfig,
-    create_engagement_request: CreateEngagementRequest,
-) -> Result<EngagementWithCredentials, Error> {
-    let local_var_configuration = configuration;
-
-    let local_var_client = &local_var_configuration.client;
-
-    let local_var_uri_str = format!("{}/v1/engagements", local_var_configuration.base_path);
-    let mut local_var_req_builder = local_var_client.post(local_var_uri_str.as_str());
-
-    if let Some(ref local_var_user_agent) = local_var_configuration.user_agent {
-        local_var_req_builder =
-            local_var_req_builder.header(reqwest::header::USER_AGENT, local_var_user_agent.clone());
-    }
-    if let Some(ref local_var_token) = local_var_configuration.bearer_access_token {
-        local_var_req_builder = local_var_req_builder.bearer_auth(local_var_token.clone());
-    };
-    local_var_req_builder = local_var_req_builder.json(&create_engagement_request);
-
-    let local_var_req = local_var_req_builder.build().map_err(Error::Internal)?;
-    let local_var_resp = local_var_client
-        .execute(local_var_req)
-        .await
-        .map_err(Error::Connection)?;
-
-    let local_var_status = local_var_resp.status();
-
-    if !local_var_status.is_client_error() && !local_var_status.is_server_error() {
-        local_var_resp
-            .json::<EngagementWithCredentials>()
-            .await
-            .map_err(Error::Schema)
-    } else if local_var_status == StatusCode::SERVICE_UNAVAILABLE {
-        let retry_after = local_var_resp
-            .headers()
-            .get(reqwest::header::RETRY_AFTER)
-            .and_then(|v| v.to_str().ok())
-            .and_then(|v| v.parse::<u64>().ok())
-            .map_or(DEFAULT_RETRY_AFTER, Duration::from_secs);
-        Err(Error::QuantumProcessorUnavailable(retry_after))
-    } else if local_var_status == StatusCode::UNAUTHORIZED
-        || local_var_status == StatusCode::FORBIDDEN
-    {
-        Err(Error::Unauthorized)
-    } else {
-        let local_var_entity: ErrorPayload = local_var_resp.json().await.map_err(Error::Schema)?;
-        Err(Error::Unknown(local_var_entity))
-    }
-}
-
-const DEFAULT_RETRY_AFTER: Duration = Duration::from_secs(15 /* minutes */ * 60);
-
-#[derive(thiserror::Error, Debug)]
-pub enum Error {
-    #[error("the QPU is unavailable, try again after {} seconds", .0.as_secs())]
-    QuantumProcessorUnavailable(Duration),
-    #[error("Received an unauthorized response, try refreshing the token")]
-    Unauthorized,
-    #[error("Could not understand a response from QCS, this is likely a bug in this library")]
-    Schema(#[source] reqwest::Error),
-    #[error("Received an unknown error from QCS: {}", .0.message)]
-    Unknown(ErrorPayload),
-    #[error("A bug in this library prevented the request from being sent")]
-    Internal(#[source] reqwest::Error),
-    #[error("Trouble connecting to QCS, check your network connection")]
-    Connection(#[source] reqwest::Error),
-}
diff --git a/crates/lib/src/qpu/execution.rs b/crates/lib/src/qpu/execution.rs
index 3078c8900..52a56775b 100644
--- a/crates/lib/src/qpu/execution.rs
+++ b/crates/lib/src/qpu/execution.rs
@@ -2,28 +2,25 @@
 
 use std::collections::HashMap;
 use std::convert::TryFrom;
-use std::sync::{Arc, Mutex};
+use std::sync::Arc;
 use std::time::Duration;
 
-use log::{trace, warn};
+use log::trace;
 use quil_rs::program::ProgramError;
 use quil_rs::Program;
 use tokio::task::{spawn_blocking, JoinError};
 
-use crate::configuration::Configuration;
 use crate::executable::Parameters;
-use crate::qpu::rewrite_arithmetic;
-use crate::qpu::runner::JobId;
-use crate::{ExecutionData, RegisterData};
+use crate::execution_data::{MemoryReferenceParseError, Qpu, ReadoutMap};
+use crate::qpu::{rewrite_arithmetic, runner::JobId, translation::translate};
+use crate::JobHandle;
 
-use super::quilc::{self, NativeQuil, NativeQuilProgram, TargetDevice};
+use super::client::{GrpcClientError, Qcs};
+use super::quilc::{self, TargetDevice};
 use super::rewrite_arithmetic::RewrittenProgram;
-use super::rpcq::Client;
-use super::runner::{self, retrieve_results, submit, DecodeError};
-use super::translation::Error as TranslationError;
-use super::{
-    build_executable, engagement, get_isa, organize_ro_sources, process_buffers, IsaError,
-};
+use super::runner::{retrieve_results, submit};
+use super::translation::EncryptedTranslationResult;
+use super::{get_isa, IsaError};
 
 /// Contains all the info needed for a single run of an [`crate::Executable`] against a QPU. Can be
 /// updated with fresh parameters in order to re-run the same program against the same QPU with the
@@ -33,46 +30,29 @@ pub(crate) struct Execution<'a> {
     program: RewrittenProgram,
     pub(crate) quantum_processor_id: &'a str,
     pub(crate) shots: u16,
-    // All the stuff needed to actually make requests to QCS, lazily initialized
-    qcs: Option<Arc<Qcs>>,
+    client: Arc<Qcs>,
 }
 
 #[derive(Debug, thiserror::Error)]
 pub(crate) enum Error {
-    #[error("No engagement for QPU: {0}")]
-    NoEngagement(engagement::Error),
-    #[error("QPU not found")]
-    QpuNotFound,
-    #[error("QPU currently unavailable, retry after {} seconds", .0.as_secs())]
-    QpuUnavailable(Duration),
-    #[error("Received unauthorized response from QCS, try refreshing credentials")]
-    Unauthorized,
-    #[error("Error communicating with QCS, check network connection and QCS status")]
-    QcsCommunication,
-    #[error("QCS returned an unrecognized error: {0}")]
-    Qcs(String),
     #[error("problem processing the provided Quil: {0}")]
     Quil(#[from] ProgramError<Program>),
     #[error("An error that is not expected to occur. If this shows up it may be a bug in this SDK or QCS")]
     Unexpected(#[from] Unexpected),
     #[error("Problem communicating with quilc at {uri}: {details}")]
     Quilc { uri: String, details: String },
+    #[error("Problem using QCS API: {0}")]
+    QcsClient(#[from] GrpcClientError),
+    #[error("Problem fetching ISA: {0}")]
+    IsaError(#[from] IsaError),
+    #[error("Problem parsing memory readout: {0}")]
+    ReadoutParse(#[from] MemoryReferenceParseError),
     #[error("Problem when compiling program: {details}")]
     Compilation { details: String },
     #[error("Program when translating the program: {0}")]
-    Translation(String),
-    #[error("Program when rewriting arithmetic: {0}")]
     RewriteArithmetic(#[from] rewrite_arithmetic::Error),
     #[error("Program when getting substitutions for program: {0}")]
     Substitution(String),
-    #[error(
-        "Program is missing readout sources: did you forget to include a MEASURE instruction?"
-    )]
-    MissingRoSources,
-    #[error(
-        "The program must first be submitted through the same Executable before retrieving results"
-    )]
-    ProgramNotSubmitted,
 }
 
 impl From<quilc::Error> for Error {
@@ -84,38 +64,9 @@ impl From<quilc::Error> for Error {
                 details: format!("{:?}", details),
             },
             quilc::Error::QuilcCompilation(details) => Self::Compilation { details },
-        }
-    }
-}
-
-impl From<engagement::Error> for Error {
-    fn from(source: engagement::Error) -> Self {
-        match source {
-            engagement::Error::QuantumProcessorUnavailable(duration) => {
-                Self::QpuUnavailable(duration)
-            }
-            engagement::Error::Unauthorized => Self::Unauthorized,
-            engagement::Error::Connection(_) => Self::QcsCommunication,
-            engagement::Error::Schema(_)
-            | engagement::Error::Unknown(_)
-            | engagement::Error::Internal(_) => {
-                Self::Unexpected(Unexpected::Qcs(format!("{:?}", source)))
-            }
-        }
-    }
-}
-
-impl From<runner::Error> for Error {
-    fn from(source: runner::Error) -> Self {
-        match source {
-            runner::Error::Engagement(e) => Self::NoEngagement(e),
-            runner::Error::Connection(_) => Self::QcsCommunication,
-            //runner::Error::Engagement(_) => Self::
-            runner::Error::Unexpected(err) => {
-                Self::Unexpected(Unexpected::Qcs(format!("{:?}", err)))
-            }
-            runner::Error::Qpu(err) => Self::Qcs(err),
-            runner::Error::ClientLock(err) => Self::Unexpected(Unexpected::Other(err)),
+            quilc::Error::Parse(details) => Self::Compilation {
+                details: details.to_string(),
+            },
         }
     }
 }
@@ -130,52 +81,6 @@ pub(crate) enum Unexpected {
     },
     #[error("Problem converting QCS ISA to quilc ISA")]
     Isa(String),
-    #[error("Problem understanding QCS")]
-    Qcs(String),
-    #[error("Unknown error")]
-    Other(String),
-}
-
-impl From<IsaError> for Error {
-    fn from(source: IsaError) -> Self {
-        match source {
-            IsaError::QpuNotFound => Self::QpuNotFound,
-            IsaError::Unauthorized => Self::Unauthorized,
-            IsaError::QcsError(source) => {
-                Self::Unexpected(Unexpected::Qcs(format!("{:?}", source)))
-            }
-            IsaError::QcsCommunicationError(_) => Self::QcsCommunication,
-        }
-    }
-}
-
-impl From<TranslationError> for Error {
-    fn from(source: TranslationError) -> Self {
-        match source {
-            TranslationError::ProgramIssue(inner) => Self::Translation(format!("{:?}", inner)),
-            TranslationError::Connection(_) => Self::QcsCommunication,
-            TranslationError::Serialization(inner) => {
-                Self::Unexpected(Unexpected::Qcs(format!("{:?}", inner)))
-            }
-            TranslationError::Unknown(inner) => {
-                Self::Unexpected(Unexpected::Qcs(format!("{:?}", inner)))
-            }
-            TranslationError::Unauthorized => Self::Unauthorized,
-        }
-    }
-}
-
-impl From<DecodeError> for Error {
-    fn from(source: DecodeError) -> Self {
-        Self::Unexpected(Unexpected::Qcs(format!("{:?}", source)))
-    }
-}
-
-struct Qcs {
-    /// A mapping of the register name declared in a program to the list of corresponding Buffer names
-    buffer_names: HashMap<Box<str>, Vec<String>>,
-    rpcq_client: Mutex<Client>,
-    executable: String,
 }
 
 impl<'a> Execution<'a> {
@@ -206,16 +111,16 @@ impl<'a> Execution<'a> {
         quil: Arc<str>,
         shots: u16,
         quantum_processor_id: &'a str,
-        config: Arc<Configuration>,
+        client: Arc<Qcs>,
         compile_with_quilc: bool,
     ) -> Result<Execution<'a>, Error> {
-        let isa = get_isa(quantum_processor_id, &config).await?;
+        let isa = get_isa(quantum_processor_id, &client).await?;
         let target_device = TargetDevice::try_from(isa)?;
 
-        let native_quil = if compile_with_quilc {
+        let program = if compile_with_quilc {
             trace!("Converting to Native Quil");
-            let thread_config = config.clone();
-            spawn_blocking(move || quilc::compile_program(&quil, target_device, &thread_config))
+            let client = client.clone();
+            spawn_blocking(move || quilc::compile_program(&quil, target_device, &client))
                 .await
                 .map_err(|source| {
                     Error::Unexpected(Unexpected::TaskError {
@@ -225,105 +130,65 @@ impl<'a> Execution<'a> {
                 })??
         } else {
             trace!("Skipping conversion to Native Quil");
-            NativeQuil::assume_native_quil(quil.to_string())
+            quil.parse().map_err(Error::Quil)?
         };
 
-        let program = NativeQuilProgram::try_from(native_quil).map_err(Error::Quil)?;
-
         Ok(Self {
             program: RewrittenProgram::try_from(program).map_err(Error::RewriteArithmetic)?,
             quantum_processor_id,
+            client,
             shots,
-            qcs: None,
         })
     }
 
     /// Run on a real QPU and wait for the results.
-    pub(crate) async fn submit(
-        &mut self,
-        params: &Parameters,
-        readouts: &[&str],
-        config: &Configuration,
-    ) -> Result<JobId, Error> {
-        let qcs = self.refresh_qcs(readouts, config).await?;
-        let qcs_for_thread = qcs.clone();
+    pub(crate) async fn submit(&mut self, params: &Parameters) -> Result<JobHandle, Error> {
+        let EncryptedTranslationResult { job, readout_map } = translate(
+            self.quantum_processor_id,
+            &self.program.to_string().0,
+            self.shots.into(),
+            self.client.as_ref(),
+        )
+        .await?;
 
         let patch_values = self
             .get_substitutions(params)
             .map_err(Error::Substitution)?;
 
-        spawn_blocking(move || {
-            let guard = qcs_for_thread.rpcq_client.lock().unwrap();
-            submit(&qcs_for_thread.executable, &patch_values, &guard).map_err(Error::from)
-        })
-        .await
-        .map_err(|source| Unexpected::TaskError {
-            task_name: "qpu",
-            source,
-        })?
-    }
-
-    pub(crate) async fn retrieve_results(&self, job_id: JobId) -> Result<ExecutionData, Error> {
-        let qcs = self.qcs.clone().ok_or(Error::ProgramNotSubmitted)?;
-        let qcs_for_thread = qcs.clone();
+        let job_id = submit(
+            self.quantum_processor_id,
+            job,
+            &patch_values,
+            self.client.as_ref(),
+        )
+        .await?;
 
-        let response = spawn_blocking(move || {
-            let guard = qcs_for_thread.rpcq_client.lock().unwrap();
-            retrieve_results(job_id, &guard).map_err(Error::from)
-        })
-        .await
-        .map_err(|source| Unexpected::TaskError {
-            task_name: "qpu",
-            source,
-        })??;
+        Ok(JobHandle::new(
+            job_id,
+            self.quantum_processor_id,
+            readout_map,
+        ))
+    }
 
-        let registers = process_buffers(response.buffers, &qcs.buffer_names)?;
-        let register_data = RegisterData::try_from_registers(registers, self.shots)?;
-        Ok(ExecutionData {
-            registers: register_data,
+    pub(crate) async fn retrieve_results(
+        &self,
+        job_id: JobId,
+        readout_mappings: HashMap<String, String>,
+    ) -> Result<Qpu, Error> {
+        let response =
+            retrieve_results(job_id, self.quantum_processor_id, self.client.as_ref()).await?;
+
+        Ok(Qpu {
+            readout_data: ReadoutMap::from_mappings_and_values(
+                &readout_mappings,
+                &response.readout_values,
+            ),
             duration: response
                 .execution_duration_microseconds
                 .map(Duration::from_micros),
         })
     }
 
-    /// Take or create a [`Qcs`] for this [`Execution`]. This fetches / updates engagements, builds
-    /// the executable, and prepares (from the executable) the mapping of returned values into what
-    /// the user expects to see.
-    async fn refresh_qcs(
-        &mut self,
-        readouts: &[&str],
-        config: &Configuration,
-    ) -> Result<Arc<Qcs>, Error> {
-        if let Some(qcs) = &self.qcs {
-            return Ok(qcs.clone());
-        }
-
-        let response = build_executable(
-            self.program.to_string(),
-            self.shots,
-            self.quantum_processor_id,
-            config,
-        )
-        .await?;
-        let ro_sources = response.ro_sources.ok_or(Error::MissingRoSources)?;
-        let buffer_names = organize_ro_sources(ro_sources, readouts)?;
-        let engagement = engagement::get(String::from(self.quantum_processor_id), config).await?;
-        let rpcq_client = Client::try_from(&engagement)
-            .map_err(|e| {
-                warn!("Unable to connect to QPU via RPCQ: {:?}", e);
-                Error::QcsCommunication
-            })
-            .map(Mutex::new)?;
-        let qcs = Arc::new(Qcs {
-            buffer_names,
-            rpcq_client,
-            executable: response.program,
-        });
-        self.qcs = Some(qcs.clone());
-        Ok(qcs)
-    }
-
     /// Take the user-provided map of [`Parameters`] and produce the map of substitutions which
     /// should be given to QCS with the executable.
     ///
diff --git a/crates/lib/src/qpu/mod.rs b/crates/lib/src/qpu/mod.rs
index c48d5e1e6..452edd537 100644
--- a/crates/lib/src/qpu/mod.rs
+++ b/crates/lib/src/qpu/mod.rs
@@ -1,30 +1,17 @@
 //! This module contains all the functionality for running Quil programs on a real QPU. Specifically,
 //! the [`Execution`] struct in this module.
 
-use core::mem;
-use std::collections::{HashMap, HashSet};
-use std::convert::TryFrom;
-use std::str::FromStr;
-
-use log::trace;
-use reqwest::StatusCode;
-
+use self::client::OpenApiClientError;
+pub(crate) use client::Qcs;
 pub(crate) use execution::{Error as ExecutionError, Execution};
-use qcs_api::apis::quantum_processors_api::{
-    get_instruction_set_architecture, GetInstructionSetArchitectureError,
+use qcs_api_client_openapi::{
+    apis::quantum_processors_api::{
+        get_instruction_set_architecture, GetInstructionSetArchitectureError,
+    },
+    models::InstructionSetArchitecture,
 };
-use qcs_api::apis::Error as QcsError;
-use qcs_api::models::{InstructionSetArchitecture, TranslateNativeQuilToEncryptedBinaryResponse};
-use runner::Buffer;
-use translation::{translate, Error as TranslationError};
 
-use crate::configuration::Configuration;
-pub(crate) use crate::qpu::rewrite_arithmetic::RewrittenQuil;
-pub(crate) use crate::qpu::runner::DecodeError;
-pub(crate) use crate::qpu::runner::JobId;
-pub(crate) use crate::qpu::runner::Register;
-
-pub(crate) mod engagement;
+pub mod client;
 mod execution;
 pub mod quilc;
 pub(crate) mod rewrite_arithmetic;
@@ -32,254 +19,6 @@ pub(crate) mod rpcq;
 pub(crate) mod runner;
 pub(crate) mod translation;
 
-async fn build_executable(
-    quil: RewrittenQuil,
-    shots: u16,
-    quantum_processor_id: &str,
-    config: &Configuration,
-) -> Result<TranslateNativeQuilToEncryptedBinaryResponse, TranslationError> {
-    let executable = translate(quil, shots, quantum_processor_id, config).await?;
-    trace!("Translation complete.");
-    Ok(executable)
-}
-
-/// Process the buffers that come back from a QPU call and map them to the
-/// `buffer_names` provided by the translation service, then attempt to fit that data into the (generic)
-/// requested structure.
-pub(crate) fn process_buffers(
-    mut buffers: HashMap<String, Buffer>,
-    buffer_names: &HashMap<Box<str>, Vec<String>>,
-) -> Result<HashMap<Box<str>, Vec<Register>>, DecodeError> {
-    buffer_names
-        .iter()
-        .map(|(register_name, buffer_names)| {
-            let registers: Result<Vec<Register>, DecodeError> = buffer_names
-                .iter()
-                .map(|buffer_name| {
-                    buffers
-                        .remove(buffer_name)
-                        .ok_or_else(|| DecodeError::MissingBuffer(buffer_name.clone()))
-                        .and_then(Register::try_from)
-                })
-                .collect();
-            registers.map(|registers| (register_name.clone(), registers))
-        })
-        .collect()
-}
-
-#[derive(Debug, PartialEq, Clone, Eq, Hash)]
-struct BufferName {
-    register_name: String,
-    buffer_name: String,
-    index: usize,
-}
-
-/// The QPU is going to return the data of an execution mapped to its own named buffers.
-/// Translation gives us the info we need to translate those buffers back to the declared memory
-/// space in the Quil program. This function reorganizes the buffer names into a form more useful
-/// for later processing and validates that we have all of the buffers we're expecting.
-///
-/// # Errors
-///
-/// 1. No buffers found for a provided `ro_source`.
-/// 2. There was a gap in the readout memory which must be contiguous.
-///
-/// # Arguments
-///
-/// 1. `ro_sources` is the 2D vec of strings that comes back from translation which we need to decode.
-/// 2. `readouts` is the slice of register names that the user wants to read from
-///
-/// # Returns
-///
-/// A map of the name of a declared register to the vector of buffers that represent it in a
-/// QPU response.
-///
-/// # Example
-///
-/// Declared memory which looks like this:
-/// ```quil
-/// DECLARE first BIT[1]
-/// DECLARE second BIT[2]
-/// ```
-///
-/// Will return a map that looks something like this (in JSON for readability):
-/// ```json
-/// {
-///     "first": ["q1"],
-///     "second": ["q3", "q5"]
-/// }
-/// ```
-///
-/// Where the `q<n>` is the name of a buffer that the QPU will return.
-fn organize_ro_sources(
-    ro_sources: Vec<Vec<String>>,
-    readouts: &[&str],
-) -> Result<HashMap<Box<str>, Vec<String>>, DecodeError> {
-    let readout_set: HashSet<&str> = readouts.iter().copied().collect();
-
-    // First, collect the unordered list of buffers since we have no guarantee of what order
-    // translation returned them in.
-    let mut buffer_names: Vec<BufferName> = ro_sources
-        .into_iter()
-        .filter_map(|mut source| {
-            // There will be buffer names returned that we don't care about, those are filtered out
-            // by returning None from this closure.
-
-            // A source is a tuple (declared_register, name_of_lodgepole_buffer) but it's
-            // deserialized as a Vec with two elements.
-            let buffer_name = source.pop()?;
-            let mut register_name = source.pop()?;
-
-            // A register_name could be a simple name like "ro" which means the same thing as "ro[0]"
-            if readout_set.contains(&register_name.as_str()) {
-                return Some(BufferName {
-                    register_name,
-                    buffer_name,
-                    index: 0,
-                });
-            }
-
-            // parse a string like ro[0] to check the reg name (ro) and index (0)
-            let open_brace = register_name.find('[')?;
-            let close_brace = register_name.find(']')?;
-            let index = usize::from_str(&register_name[open_brace + 1..close_brace]).ok()?;
-            register_name.truncate(open_brace);
-            if !readout_set.contains(&register_name.as_str()) {
-                return None;
-            }
-
-            Some(BufferName {
-                register_name,
-                buffer_name,
-                index,
-            })
-        })
-        .collect();
-
-    if buffer_names.is_empty() {
-        return Err(DecodeError::MissingBuffer(String::from("ro")));
-    }
-
-    // Sort so that we have one register at a time in ascending index order, making the organization
-    // below simpler.
-    buffer_names.sort_by(|first, second| {
-        first
-            .register_name
-            .cmp(&second.register_name)
-            .then_with(|| first.index.cmp(&second.index))
-    });
-
-    // Reorganize and validate all the BufferNames
-    let first = buffer_names.remove(0);
-    if first.index != 0 {
-        return Err(DecodeError::ContiguousMemory {
-            register: first.register_name,
-            index: 0,
-        });
-    }
-    let mut current_index = 1;
-    let mut current_register_name = first.register_name;
-    let mut current_names = vec![first.buffer_name];
-    let mut results = HashMap::new();
-
-    for buffer_name in buffer_names {
-        let BufferName {
-            register_name,
-            buffer_name,
-            index,
-        } = buffer_name;
-        if current_register_name != register_name {
-            // Switching to the next register, store the last register's results
-            let names = mem::take(&mut current_names);
-            let previous_register_name = mem::replace(&mut current_register_name, register_name);
-            results.insert(previous_register_name.into_boxed_str(), names);
-            current_index = 0;
-        }
-
-        if current_index != index {
-            return Err(DecodeError::ContiguousMemory {
-                register: current_register_name,
-                index: current_index,
-            });
-        }
-        current_index += 1;
-        current_names.push(buffer_name);
-    }
-
-    results.insert(current_register_name.into_boxed_str(), current_names);
-
-    for expected_readout in readout_set {
-        if !results.contains_key(expected_readout) {
-            return Err(DecodeError::MissingBuffer(String::from(expected_readout)));
-        }
-    }
-
-    Ok(results)
-}
-
-#[cfg(test)]
-mod describe_organize_ro_sources {
-    use maplit::hashmap;
-
-    use crate::qpu::organize_ro_sources;
-
-    #[test]
-    fn it_converts_from_translation_ro_sources() {
-        let ro_sources = vec![
-            vec![String::from("ro[1]"), String::from("q7")],
-            vec![String::from("blah"), String::from("blah")],
-            vec![String::from("something"), String::from("something")],
-            vec![String::from("ro[0]"), String::from("q6")],
-        ];
-        let readouts = &["ro", "something"];
-        let expected = hashmap! {
-            Box::from(String::from("ro")) => vec![String::from("q6"), String::from("q7")],
-            Box::from(String::from("something")) => vec![String::from("something")]
-        };
-
-        let buffer_names =
-            organize_ro_sources(ro_sources, readouts).expect("Failed to convert ro_sources");
-        assert_eq!(buffer_names, expected);
-    }
-
-    #[test]
-    fn it_errors_on_buffers_not_starting_with_0() {
-        let ro_sources = vec![vec![String::from("ro[1]"), String::from("q7")]];
-
-        let result = organize_ro_sources(ro_sources, &["ro"]);
-        assert!(result.is_err());
-    }
-
-    #[test]
-    fn it_errors_when_no_matching_buffers() {
-        let ro_sources = vec![vec![String::from("blah[0]"), String::from("blah")]];
-
-        let result = organize_ro_sources(ro_sources, &[]);
-        assert!(result.is_err());
-    }
-
-    #[test]
-    fn it_errors_when_missing_a_buffer() {
-        let ro_sources = vec![vec![String::from("blah[0]"), String::from("blah")]];
-
-        let result = organize_ro_sources(ro_sources, &["blah", "other"]);
-        assert!(result.is_err());
-    }
-
-    #[test]
-    fn it_errors_when_gaps_in_buffers() {
-        let ro_sources = vec![
-            vec![String::from("ro[0]"), String::from("q6")],
-            vec![String::from("a[0]"), String::from("a0")],
-            vec![String::from("a[1]"), String::from("a1")],
-            vec![String::from("ro[2]"), String::from("q7")],
-        ];
-
-        let result = organize_ro_sources(ro_sources, &["ro", "a"]);
-        assert!(result.is_err());
-    }
-}
-
 /// Query QCS for the ISA of the provided `quantum_processor_id`.
 ///
 /// # Errors
@@ -289,40 +28,12 @@ mod describe_organize_ro_sources {
 /// 3. Expired token
 pub(crate) async fn get_isa(
     quantum_processor_id: &str,
-    config: &Configuration,
+    client: &Qcs,
 ) -> Result<InstructionSetArchitecture, IsaError> {
-    get_instruction_set_architecture(config.as_ref(), quantum_processor_id)
+    get_instruction_set_architecture(&client.get_openapi_client(), quantum_processor_id)
         .await
-        .map_err(IsaError::from)
+        .map_err(OpenApiClientError::RequestFailed)
 }
 
-/// The errors that can occur when fetching the ISA of a QPU.
-#[derive(Debug, thiserror::Error)]
-pub(crate) enum IsaError {
-    #[error("QPU not found")]
-    QpuNotFound,
-    #[error("Unauthorized, refresh credentials and try again.")]
-    Unauthorized,
-    #[error("Problem understanding QCS, this is likely a bug")]
-    QcsError(#[source] QcsError<GetInstructionSetArchitectureError>),
-    #[error("Problem communicating with QCS")]
-    QcsCommunicationError(#[from] std::io::Error),
-}
-
-impl From<QcsError<GetInstructionSetArchitectureError>> for IsaError {
-    fn from(error: QcsError<GetInstructionSetArchitectureError>) -> Self {
-        match error {
-            QcsError::ResponseError(content) if content.status == StatusCode::NOT_FOUND => {
-                IsaError::QpuNotFound
-            }
-            QcsError::ResponseError(content)
-                if content.status == StatusCode::UNAUTHORIZED
-                    || content.status == StatusCode::FORBIDDEN =>
-            {
-                IsaError::Unauthorized
-            }
-            QcsError::Io(io_error) => io_error.into(),
-            error => IsaError::QcsError(error),
-        }
-    }
-}
+/// Error raised due to failure to get an ISA
+pub type IsaError = OpenApiClientError<GetInstructionSetArchitectureError>;
diff --git a/crates/lib/src/qpu/quilc/isa/edge.rs b/crates/lib/src/qpu/quilc/isa/edge.rs
index 2fa88b009..98ff1a243 100644
--- a/crates/lib/src/qpu/quilc/isa/edge.rs
+++ b/crates/lib/src/qpu/quilc/isa/edge.rs
@@ -4,8 +4,7 @@ use std::fmt::{Display, Formatter};
 
 use serde::{Deserialize, Serialize, Serializer};
 
-use qcs_api::models;
-use qcs_api::models::Characteristic;
+use qcs_api_client_openapi::models::{self, Characteristic};
 
 use super::operator::{wildcard, Argument, Operator, Parameter};
 
@@ -210,7 +209,7 @@ pub(crate) fn convert_edges(edges: &[models::Edge]) -> Result<HashMap<Id, Edge>,
 
 #[cfg(test)]
 mod describe_convert_edges {
-    use qcs_api::models;
+    use qcs_api_client_openapi::models;
 
     use crate::qpu::quilc::isa::edge::{convert_edges, Id};
 
diff --git a/crates/lib/src/qpu/quilc/isa/mod.rs b/crates/lib/src/qpu/quilc/isa/mod.rs
index 4d35276bd..df23b3aa7 100644
--- a/crates/lib/src/qpu/quilc/isa/mod.rs
+++ b/crates/lib/src/qpu/quilc/isa/mod.rs
@@ -4,7 +4,7 @@ use std::convert::TryFrom;
 use serde::{Deserialize, Serialize};
 
 use edge::{convert_edges, Edge, Id};
-use qcs_api::models::InstructionSetArchitecture;
+use qcs_api_client_openapi::models::InstructionSetArchitecture;
 use qubit::Qubit;
 
 use crate::qpu::quilc::isa::qubit::FrbSim1q;
@@ -95,7 +95,7 @@ mod describe_compiler_isa {
     use std::{convert::TryFrom, fs::read_to_string};
 
     use float_cmp::{approx_eq, F64Margin};
-    use qcs_api::models::InstructionSetArchitecture;
+    use qcs_api_client_openapi::models::InstructionSetArchitecture;
     use serde_json::Value;
 
     use super::Compiler;
diff --git a/crates/lib/src/qpu/quilc/isa/operator.rs b/crates/lib/src/qpu/quilc/isa/operator.rs
index 47b3e330a..f655e11b1 100644
--- a/crates/lib/src/qpu/quilc/isa/operator.rs
+++ b/crates/lib/src/qpu/quilc/isa/operator.rs
@@ -1,4 +1,4 @@
-use serde::{Deserialize, Serialize};
+use serde::{Deserialize, Deserializer, Serialize};
 
 pub(crate) fn wildcard(node_id: Option<i32>) -> Operator {
     let arg = node_id.map_or_else(|| Argument::String("_".to_owned()), Argument::Int);
@@ -18,37 +18,68 @@ pub(crate) fn wildcard(node_id: Option<i32>) -> Operator {
 pub(crate) enum Operator {
     Gate {
         operator: String,
+        #[serde(deserialize_with = "deserialize_duration_null_default")]
         duration: f64,
+        #[serde(deserialize_with = "deserialize_fidelity_null_default")]
         fidelity: f64,
         parameters: Vec<Parameter>,
         arguments: Vec<Argument>,
     },
     Measure {
         operator: String,
+        #[serde(deserialize_with = "deserialize_duration_null_default")]
         duration: f64,
+        #[serde(deserialize_with = "deserialize_fidelity_null_default")]
         fidelity: f64,
-        qubit: i32,
+        qubit: Qubit,
         target: Option<String>,
     },
 }
 
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
+#[serde(untagged)]
+pub(crate) enum Qubit {
+    Int(i32),
+    String(String),
+}
+
+/// Swap a ``null`` duration value with [`PERFECT_DURATION`]
+fn deserialize_duration_null_default<'de, D>(d: D) -> Result<f64, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    Deserialize::deserialize(d).map(|x: Option<_>| x.unwrap_or(PERFECT_DURATION))
+}
+
+/// Swap a ``null`` fidelity value with [`PERFECT_FIDELITY`]
+fn deserialize_fidelity_null_default<'de, D>(d: D) -> Result<f64, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    Deserialize::deserialize(d).map(|x: Option<_>| x.unwrap_or(PERFECT_FIDELITY))
+}
+
 #[cfg(test)]
 mod describe_operator {
-    use crate::qpu::quilc::isa::operator::{Argument, Operator, Parameter};
+    use serde_json::json;
+
+    use crate::qpu::quilc::isa::operator::{
+        Argument, Operator, Parameter, Qubit, PERFECT_DURATION, PERFECT_FIDELITY,
+    };
 
     /// This test copies some JSON data from the pyQuil ISA integration test to
     /// validate that [`Operator::Gate`] is serialized correctly.
     #[test]
     fn it_serializes_gates_like_pyquil() {
         let gate_op = Operator::Gate {
-            arguments: vec![Argument::Int(1)],
+            arguments: vec![Argument::String("_".to_string())],
             duration: 0.5,
             fidelity: 0.5,
             operator: "RZ".to_string(),
             parameters: vec![Parameter::String("_".to_owned())],
         };
         let expected = serde_json::json!({
-            "arguments": [1],
+            "arguments": ["_"],
             "duration": 0.5,
             "fidelity": 0.5,
             "operator": "RZ",
@@ -60,6 +91,32 @@ mod describe_operator {
         assert_eq!(serialized, expected);
     }
 
+    #[test]
+    fn it_deserializes_gates_like_quilc() {
+        let expected = Operator::Gate {
+            arguments: vec![
+                Argument::String("_".to_string()),
+                Argument::String("_".to_string()),
+            ],
+            duration: 0.5,
+            fidelity: 0.5,
+            operator: "RZ".to_string(),
+            parameters: vec![Parameter::String("_".to_owned())],
+        };
+        let input = json!({
+            "arguments": ["_", "_"],
+            "duration": 0.5,
+            "fidelity": 0.5,
+            "operator": "RZ",
+            "operator_type": "gate",
+            "parameters": ["_"]
+        });
+        let input = serde_json::to_string(&input).expect("It should serialize to string");
+        let actual =
+            serde_json::from_str::<Operator>(&input).expect("It should deserialize successfully");
+        assert_eq!(actual, expected);
+    }
+
     /// This test copies some JSON data from the pyQuil ISA integration test to validate that
     /// [`Operator::Measure`] is serialized correctly.
     #[test]
@@ -67,7 +124,7 @@ mod describe_operator {
         let measure = Operator::Measure {
             duration: 0.5,
             fidelity: 0.5,
-            qubit: 1,
+            qubit: Qubit::Int(1),
             operator: "MEASURE".to_string(),
             target: Some("_".to_string()),
         };
@@ -91,7 +148,7 @@ mod describe_operator {
         let measure = Operator::Measure {
             duration: 0.5,
             fidelity: 0.5,
-            qubit: 1,
+            qubit: Qubit::Int(1),
             operator: "MEASURE".to_string(),
             target: None,
         };
@@ -107,6 +164,33 @@ mod describe_operator {
             serde_json::to_value(&measure).expect("Could not serialize Operation::Gate");
         assert_eq!(serialized, expected);
     }
+
+    #[test]
+    fn it_deserializes_null_fidelity_as_perfect_fidelity() {
+        let input = json!({
+            "duration": null,
+            "fidelity": null,
+            "operator": "MEASURE",
+            "operator_type": "measure",
+            "qubit": 1,
+            "target": null
+
+        });
+        let expected = json!({
+            "duration": PERFECT_DURATION,
+            "fidelity": PERFECT_FIDELITY,
+            "operator": "MEASURE",
+            "operator_type": "measure",
+            "qubit": 1,
+            "target": null
+
+        });
+        let gate: Operator = serde_json::from_value(input).expect("Operator should deserialize");
+        assert_eq!(
+            serde_json::to_value(gate).expect("Can convert Operator to Value"),
+            expected
+        );
+    }
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
@@ -116,17 +200,6 @@ pub(crate) enum Parameter {
     Float(f64),
 }
 
-#[cfg(test)]
-mod test_parameters_deser {
-    use super::Parameter;
-
-    #[test]
-    fn it_deserializes_parameters() {
-        let s = r##"[""]"##;
-        serde_json::from_str::<Vec<Parameter>>(s).expect("yo");
-    }
-}
-
 #[cfg(test)]
 mod describe_parameters {
     use crate::qpu::quilc::isa::operator::Parameter;
@@ -157,6 +230,8 @@ pub(crate) enum Argument {
 
 #[cfg(test)]
 mod describe_arguments {
+    use serde_json::json;
+
     use crate::qpu::quilc::isa::operator::Argument;
 
     #[test]
@@ -174,6 +249,14 @@ mod describe_arguments {
         let expected = serde_json::json!([1]);
         assert_eq!(expected, serialized);
     }
+
+    #[test]
+    fn it_deserializes_underscore_arguments() {
+        let expected = Argument::String("_".to_string());
+        let actual =
+            serde_json::from_value::<Argument>(json!("_")).expect("Should deserialize from Value");
+        assert_eq!(actual, expected);
+    }
 }
 
 pub(crate) const PERFECT_FIDELITY: f64 = 1.0;
diff --git a/crates/lib/src/qpu/quilc/isa/qubit.rs b/crates/lib/src/qpu/quilc/isa/qubit.rs
index c54afffef..302ce6bd9 100644
--- a/crates/lib/src/qpu/quilc/isa/qubit.rs
+++ b/crates/lib/src/qpu/quilc/isa/qubit.rs
@@ -4,10 +4,10 @@ use std::f64::consts::{FRAC_PI_2, PI};
 
 use serde::{Deserialize, Serialize};
 
-use qcs_api::models::{Characteristic, Node, Operation};
+use qcs_api_client_openapi::models::{Characteristic, Node, Operation};
 
 use super::operator::{
-    wildcard, Argument, Operator, Parameter, PERFECT_DURATION, PERFECT_FIDELITY,
+    self, wildcard, Argument, Operator, Parameter, PERFECT_DURATION, PERFECT_FIDELITY,
 };
 
 /// Represents a single Qubit on a QPU and its capabilities. Needed by quilc for optimization.
@@ -198,7 +198,7 @@ fn rx_gates(node_id: i32, frb_sim_1q: &FrbSim1q) -> Result<Vec<Operator>, Error>
 mod describe_rx_gates {
     use std::f64::consts::{FRAC_PI_2, PI};
 
-    use qcs_api::models::Characteristic;
+    use qcs_api_client_openapi::models::Characteristic;
 
     use crate::qpu::quilc::isa::{
         operator::{Argument, Operator, Parameter},
@@ -316,14 +316,14 @@ fn measure(node_id: i32, characteristics: &[Characteristic]) -> Vec<Operator> {
             operator: "MEASURE".to_string(),
             duration: MEASURE_DEFAULT_DURATION,
             fidelity,
-            qubit: node_id,
+            qubit: operator::Qubit::Int(node_id),
             target: Some("_".to_string()),
         },
         Operator::Measure {
             operator: "MEASURE".to_string(),
             duration: MEASURE_DEFAULT_DURATION,
             fidelity,
-            qubit: node_id,
+            qubit: operator::Qubit::Int(node_id),
             target: None,
         },
     ]
@@ -331,9 +331,9 @@ fn measure(node_id: i32, characteristics: &[Characteristic]) -> Vec<Operator> {
 
 #[cfg(test)]
 mod describe_measure {
-    use qcs_api::models::Characteristic;
+    use qcs_api_client_openapi::models::Characteristic;
 
-    use crate::qpu::quilc::isa::operator::Operator;
+    use crate::qpu::quilc::isa::operator::{self, Operator};
 
     use super::measure;
 
@@ -354,14 +354,14 @@ mod describe_measure {
                 operator: "MEASURE".to_string(),
                 duration: 2000.0,
                 fidelity: 0.981_000_006_198_883_1,
-                qubit: 0,
+                qubit: operator::Qubit::Int(0),
                 target: Some("_".to_string()),
             },
             Operator::Measure {
                 operator: "MEASURE".to_string(),
                 duration: 2000.0,
                 fidelity: 0.981_000_006_198_883_1,
-                qubit: 0,
+                qubit: operator::Qubit::Int(0),
                 target: None,
             },
         ];
diff --git a/crates/lib/src/qpu/quilc/mod.rs b/crates/lib/src/qpu/quilc/mod.rs
index 955dd7e4b..957484784 100644
--- a/crates/lib/src/qpu/quilc/mod.rs
+++ b/crates/lib/src/qpu/quilc/mod.rs
@@ -3,17 +3,14 @@
 
 use std::collections::HashMap;
 use std::convert::TryFrom;
-use std::str::FromStr;
 
-use quil_rs::Program;
+use quil_rs::program::{Program, ProgramError};
 use serde::{Deserialize, Serialize};
 
 use isa::Compiler;
-use qcs_api::models::InstructionSetArchitecture;
+use qcs_api_client_openapi::models::InstructionSetArchitecture;
 
-use crate::configuration::Configuration;
-
-use super::rpcq;
+use super::{rpcq, Qcs};
 
 mod isa;
 
@@ -25,7 +22,7 @@ mod isa;
 /// * `isa`: The [`InstructionSetArchitecture`] of the targeted platform. Get this using
 ///     [`super::get_isa`].
 ///
-/// returns: `eyre::Result<NativeQuil>`
+/// returns: `eyre::Result<quil_rs::Program>`
 ///
 /// # Errors
 ///
@@ -36,16 +33,21 @@ mod isa;
 pub(crate) fn compile_program(
     quil: &str,
     isa: TargetDevice,
-    config: &Configuration,
-) -> Result<NativeQuil, Error> {
-    let endpoint = &config.quilc_url;
+    client: &Qcs,
+) -> Result<quil_rs::Program, Error> {
+    let config = client.get_config();
+    let endpoint = config.quilc_url();
     let params = QuilcParams::new(quil, isa);
     let request = rpcq::RPCRequest::new("quil_to_native_quil", &params);
-    rpcq::Client::new(endpoint)
-        .map_err(|source| Error::from_quilc_error(endpoint.clone(), source))?
-        .run_request::<_, QuilcResponse>(&request)
-        .map(|response| NativeQuil(response.quil))
-        .map_err(|source| Error::from_quilc_error(endpoint.clone(), source))
+    let rpcq_client = rpcq::Client::new(endpoint)
+        .map_err(|source| Error::from_quilc_error(endpoint.into(), source))?;
+    match rpcq_client.run_request::<_, QuilcResponse>(&request) {
+        Ok(response) => response
+            .quil
+            .parse::<quil_rs::Program>()
+            .map_err(Error::Parse),
+        Err(source) => Err(Error::from_quilc_error(endpoint.into(), source)),
+    }
 }
 
 /// All of the errors that can occur within this module.
@@ -60,6 +62,9 @@ pub enum Error {
     /// An error when trying to compile using quilc.
     #[error("Problem compiling quil program: {0}")]
     QuilcCompilation(String),
+    /// An error when trying to parse the compiled program.
+    #[error("Problem when trying to parse the compiled program: {0}")]
+    Parse(ProgramError<Program>),
 }
 
 impl Error {
@@ -71,50 +76,6 @@ impl Error {
     }
 }
 
-/// A wrapper around a [`String`] which indicates the string contains valid Native Quil. That is,
-/// Quil which has been processed through `quilc`.
-#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-pub struct NativeQuil(String);
-
-impl NativeQuil {
-    /// Cast a String to `NativeQuil` without checking or transforming it via `quilc`.
-    pub(super) fn assume_native_quil(quil: String) -> Self {
-        NativeQuil(quil)
-    }
-}
-
-impl From<NativeQuil> for String {
-    fn from(native_quil: NativeQuil) -> String {
-        native_quil.0
-    }
-}
-
-impl AsRef<str> for NativeQuil {
-    fn as_ref(&self) -> &str {
-        &self.0
-    }
-}
-
-/// A wrapper around [`Program`] which indicates it has been converted to `NativeQuil` (has been run
-/// through `quilc` and therefore is suitable to use on QPUs.
-#[derive(Debug, PartialEq, Clone)]
-pub(super) struct NativeQuilProgram(Program);
-
-impl TryFrom<NativeQuil> for NativeQuilProgram {
-    type Error = <Program as FromStr>::Err;
-
-    fn try_from(native_quil: NativeQuil) -> Result<Self, Self::Error> {
-        let program = Program::from_str(&String::from(native_quil))?;
-        Ok(Self(program))
-    }
-}
-
-impl From<NativeQuilProgram> for Program {
-    fn from(program: NativeQuilProgram) -> Program {
-        program.0
-    }
-}
-
 #[derive(Clone, Deserialize, Debug, Eq, PartialEq, Ord, PartialOrd)]
 struct QuilcResponse {
     pub quil: String,
@@ -176,11 +137,10 @@ impl TryFrom<InstructionSetArchitecture> for TargetDevice {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use qcs_api::models::InstructionSetArchitecture;
+    use qcs_api_client_openapi::models::InstructionSetArchitecture;
     use std::fs::File;
 
-    const EXPECTED_H0_OUTPUT: &str =
-        "MEASURE 0                               # Entering/exiting rewiring: (#(0 1) . #(0 1))\n";
+    const EXPECTED_H0_OUTPUT: &str = "MEASURE 0\n";
 
     fn aspen_9_isa() -> InstructionSetArchitecture {
         serde_json::from_reader(File::open("tests/aspen_9_isa.json").unwrap()).unwrap()
@@ -190,15 +150,15 @@ mod tests {
         serde_json::from_reader(File::open("tests/qvm_isa.json").unwrap()).unwrap()
     }
 
-    #[test]
-    fn compare_native_quil_to_expected_output() {
+    #[tokio::test]
+    async fn compare_native_quil_to_expected_output() {
         let output = compile_program(
             "MEASURE 0",
             TargetDevice::try_from(qvm_isa()).expect("Couldn't build target device from ISA"),
-            &Configuration::default(),
+            &Qcs::load().await.unwrap_or_default(),
         )
         .expect("Could not compile");
-        assert_eq!(String::from(output), EXPECTED_H0_OUTPUT);
+        assert_eq!(output.to_string(true), EXPECTED_H0_OUTPUT);
     }
 
     const BELL_STATE: &str = r##"DECLARE ro BIT[2]
@@ -212,16 +172,16 @@ MEASURE 1 ro[1]
 
     #[tokio::test]
     async fn run_compiled_bell_state_on_qvm() {
-        let config = Configuration::load().await.unwrap_or_default();
+        let client = Qcs::load().await.unwrap_or_default();
         let output = compile_program(
             BELL_STATE,
             TargetDevice::try_from(aspen_9_isa()).expect("Couldn't build target device from ISA"),
-            &config,
+            &client,
         )
         .expect("Could not compile");
-        let mut results = crate::qvm::Execution::new(&String::from(output))
+        let mut results = crate::qvm::Execution::new(&output.to_string(true))
             .unwrap()
-            .run(10, &["ro"], &HashMap::default(), &config)
+            .run(10, &["ro"], &HashMap::default(), &client.get_config())
             .await
             .expect("Could not run program on QVM");
         for shot in results
diff --git a/crates/lib/src/qpu/rewrite_arithmetic.rs b/crates/lib/src/qpu/rewrite_arithmetic.rs
index 2b6261079..7d4626d0b 100644
--- a/crates/lib/src/qpu/rewrite_arithmetic.rs
+++ b/crates/lib/src/qpu/rewrite_arithmetic.rs
@@ -12,7 +12,7 @@ use quil_rs::{
     Program,
 };
 
-use crate::{executable::Parameters, qpu::quilc::NativeQuilProgram};
+use crate::executable::Parameters;
 
 /// A function for converting `program` into a form that Rigetti QPUs can understand.
 ///
@@ -337,11 +337,11 @@ impl From<RewrittenQuil> for String {
     }
 }
 
-impl TryFrom<NativeQuilProgram> for RewrittenProgram {
+impl TryFrom<quil_rs::Program> for RewrittenProgram {
     type Error = Error;
 
-    fn try_from(program: NativeQuilProgram) -> Result<Self, Self::Error> {
-        let (inner, substitutions) = rewrite_arithmetic(program.into())?;
+    fn try_from(program: quil_rs::Program) -> Result<Self, Self::Error> {
+        let (inner, substitutions) = rewrite_arithmetic(program)?;
         Ok(Self {
             inner,
             substitutions,
diff --git a/crates/lib/src/qpu/rpcq.rs b/crates/lib/src/qpu/rpcq.rs
index 98b5e7ebb..ce8276c7f 100644
--- a/crates/lib/src/qpu/rpcq.rs
+++ b/crates/lib/src/qpu/rpcq.rs
@@ -2,7 +2,7 @@ use log::{trace, warn};
 use std::collections::HashMap;
 use std::convert::TryFrom;
 
-use qcs_api::models::EngagementWithCredentials;
+use qcs_api_client_openapi::models::EngagementWithCredentials;
 use rmp_serde::Serializer;
 use serde::de::DeserializeOwned;
 use serde::{Deserialize, Serialize};
diff --git a/crates/lib/src/qpu/runner.rs b/crates/lib/src/qpu/runner.rs
index 6e9a31d7f..fed18d0c5 100644
--- a/crates/lib/src/qpu/runner.rs
+++ b/crates/lib/src/qpu/runner.rs
@@ -1,6 +1,3 @@
-//! Interface to the QPU's API.
-
-use std::collections::HashMap;
 use std::convert::{TryFrom, TryInto};
 use std::fmt::{Display, Formatter};
 
@@ -9,132 +6,127 @@ use num::complex::Complex32;
 use serde::{Deserialize, Serialize};
 use serde_bytes::ByteBuf;
 
+use qcs_api_client_grpc::{
+    models::controller::{
+        data_value::Value, ControllerJobExecutionResult, DataValue, EncryptedControllerJob,
+        JobExecutionConfiguration, RealDataValue,
+    },
+    services::controller::{
+        execute_controller_job_request, get_controller_job_results_request,
+        ExecuteControllerJobRequest, GetControllerJobResultsRequest,
+    },
+};
+
 use crate::executable::Parameters;
 
-use super::engagement;
-use super::rpcq::{Client, Error as RPCQError, RPCRequest};
+use super::client::{GrpcClientError, Qcs};
+
+pub(crate) fn params_into_job_execution_configuration(
+    params: &Parameters,
+) -> JobExecutionConfiguration {
+    let memory_values = params
+        .iter()
+        .map(|(str, value)| {
+            (
+                str.as_ref().into(),
+                DataValue {
+                    value: Some(Value::Real(RealDataValue {
+                        data: value.clone(),
+                    })),
+                },
+            )
+        })
+        .collect();
+
+    JobExecutionConfiguration { memory_values }
+}
 
 /// The QCS Job ID. Useful for debugging or retrieving results later.
 #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
 pub struct JobId(pub(crate) String);
 
 /// Execute compiled program on a QPU.
-pub(crate) fn submit(
-    program: &str,
+pub(crate) async fn submit(
+    quantum_processor_id: &str,
+    program: EncryptedControllerJob,
     patch_values: &Parameters,
-    client: &Client,
-) -> Result<JobId, Error> {
-    let params = QPUParams::from_program(program, patch_values);
-    let request = RPCRequest::from(&params);
-    let job_id: String = client.run_request(&request)?;
-    Ok(JobId(job_id))
+    client: &Qcs,
+) -> Result<JobId, GrpcClientError> {
+    let request = ExecuteControllerJobRequest {
+        execution_configurations: vec![params_into_job_execution_configuration(patch_values)],
+        job: Some(execute_controller_job_request::Job::Encrypted(program)),
+        target: Some(execute_controller_job_request::Target::QuantumProcessorId(
+            quantum_processor_id.into(),
+        )),
+    };
+
+    // we expect exactly one job ID since we only submit one execution configuration
+    let job_execution_id = client
+        .get_controller_client(quantum_processor_id)
+        .await?
+        .execute_controller_job(request)
+        .await?
+        .into_inner()
+        .job_execution_ids
+        .pop();
+
+    job_execution_id
+        .map(JobId)
+        .ok_or_else(|| GrpcClientError::ResponseEmpty("Job Execution ID".into()))
 }
 
-pub(crate) fn retrieve_results(
+pub(crate) async fn retrieve_results(
     job_id: JobId,
-    client: &Client,
-) -> Result<GetExecutionResultsResponse, Error> {
-    let get_buffers_request = GetExecutionResultsRequest::new(job_id.0);
+    quantum_processor_id: &str,
+    client: &Qcs,
+) -> Result<ControllerJobExecutionResult, GrpcClientError> {
+    let request = GetControllerJobResultsRequest {
+        job_execution_id: Some(job_id.0),
+        target: Some(
+            get_controller_job_results_request::Target::QuantumProcessorId(
+                quantum_processor_id.into(),
+            ),
+        ),
+    };
+
     client
-        .run_request(&RPCRequest::from(&get_buffers_request))
-        .map_err(Error::from)
+        .get_controller_client(quantum_processor_id)
+        .await?
+        .get_controller_job_results(request)
+        .await?
+        .into_inner()
+        .result
+        .ok_or_else(|| GrpcClientError::ResponseEmpty("Job Execution Results".into()))
 }
 
-/// All of the possible errors for this module
+/// Errors that can occur when decoding the results from the QPU
 #[derive(Debug, thiserror::Error)]
-pub enum Error {
-    #[error("Error connecting to the QPU")]
-    Connection(#[source] RPCQError),
-    #[error("Error getting engagement for QPU")]
-    Engagement(#[source] engagement::Error),
-    #[error("An error not expected to occur—if encountered it may indicate bug in this library")]
-    Unexpected(#[source] RPCQError),
-    #[error("An error was returned from the QPU: {0}")]
-    Qpu(String),
-    #[error("An error was encountered when claiming the client lock: {0}")]
-    ClientLock(String),
-}
-
-impl From<RPCQError> for Error {
-    fn from(err: RPCQError) -> Self {
-        match err {
-            RPCQError::SocketCreation(_)
-            | RPCQError::Communication(_)
-            | RPCQError::ResponseIdMismatch => Self::Connection(err),
-            RPCQError::AuthSetup(_)
-            | RPCQError::Serialization(_)
-            | RPCQError::Deserialization(_) => Self::Unexpected(err),
-            RPCQError::Response(message) => Self::Qpu(message),
-        }
-    }
-}
-
-#[derive(Serialize, Debug, Clone, PartialEq)]
-struct QPUParams<'request> {
-    request: QPURequest<'request>,
-    priority: u8,
+pub(crate) enum DecodeError {
+    #[error("Only 1-dimensional buffer shapes are currently supported")]
+    InvalidShape,
+    #[error("Expected buffer length {expected}, got {actual}")]
+    BufferLength { expected: usize, actual: usize },
 }
 
-impl<'request> QPUParams<'request> {
-    fn from_program(program: &'request str, patch_values: &'request Parameters) -> Self {
-        Self {
-            request: QPURequest::from_program(program, patch_values),
-            priority: 1,
+impl Buffer {
+    fn check_shape(&self) -> Result<(), DecodeError> {
+        if self.shape.len() == 1 {
+            Ok(())
+        } else {
+            Err(DecodeError::InvalidShape)
         }
     }
-}
-
-impl<'params> From<&'params QPUParams<'params>> for RPCRequest<'params, QPUParams<'params>> {
-    fn from(params: &'params QPUParams) -> Self {
-        RPCRequest::new("execute_qpu_request", params)
-    }
-}
-
-#[derive(Serialize, Debug, PartialEq, Clone)]
-#[serde(tag = "_type")]
-struct QPURequest<'request> {
-    id: String,
-    program: &'request str,
-    patch_values: &'request Parameters,
-}
 
-impl<'request> QPURequest<'request> {
-    fn from_program(program: &'request str, patch_values: &'request Parameters) -> Self {
-        Self {
-            id: uuid::Uuid::new_v4().to_string(),
-            program,
-            patch_values,
+    fn assert_len(&self, expected: usize) -> Result<(), DecodeError> {
+        let actual = self.data.len();
+        if expected == actual {
+            Ok(())
+        } else {
+            Err(DecodeError::BufferLength { expected, actual })
         }
     }
 }
 
-#[derive(Serialize, Debug, Clone, Eq, PartialEq, Hash)]
-struct GetExecutionResultsRequest {
-    job_id: String,
-    wait: bool,
-}
-
-impl GetExecutionResultsRequest {
-    fn new(job_id: String) -> Self {
-        Self { job_id, wait: true }
-    }
-}
-
-impl<'request> From<&'request GetExecutionResultsRequest>
-    for RPCRequest<'request, GetExecutionResultsRequest>
-{
-    fn from(req: &'request GetExecutionResultsRequest) -> Self {
-        RPCRequest::new("get_execution_results", req)
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)]
-pub struct GetExecutionResultsResponse {
-    pub buffers: HashMap<String, Buffer>,
-    #[serde(default)]
-    pub execution_duration_microseconds: Option<u64>,
-}
-
 /// The raw form of the data which comes back from an execution.
 ///
 /// Generally this should not be used directly, but converted into an appropriate
@@ -166,40 +158,6 @@ impl Display for DataType {
     }
 }
 
-/// Errors that can occur when decoding the results from the QPU
-#[derive(Debug, thiserror::Error)]
-pub(crate) enum DecodeError {
-    #[error("Only 1-dimensional buffer shapes are currently supported")]
-    InvalidShape,
-    #[error("Expected buffer length {expected}, got {actual}")]
-    BufferLength { expected: usize, actual: usize },
-    #[error("Missing expected buffer named {0}, did you forget to MEASURE?")]
-    MissingBuffer(String),
-    #[error("This SDK expects contiguous memory, but {register}[{index}] was missing.")]
-    ContiguousMemory { register: String, index: usize },
-    #[error("A single register should have the same type for all shots, got mixed types.")]
-    MixedTypes,
-}
-
-impl Buffer {
-    fn check_shape(&self) -> Result<(), DecodeError> {
-        if self.shape.len() == 1 {
-            Ok(())
-        } else {
-            Err(DecodeError::InvalidShape)
-        }
-    }
-
-    fn assert_len(&self, expected: usize) -> Result<(), DecodeError> {
-        let actual = self.data.len();
-        if expected == actual {
-            Ok(())
-        } else {
-            Err(DecodeError::BufferLength { expected, actual })
-        }
-    }
-}
-
 /// Data from an individual register. Each variant contains a vector with the expected data type
 /// where each value in the vector corresponds to a shot.
 #[derive(Debug, PartialEq, EnumAsInner)]
@@ -289,114 +247,3 @@ impl TryFrom<Buffer> for Register {
         })
     }
 }
-
-#[cfg(test)]
-mod describe_buffer {
-    use std::{collections::HashMap, convert::TryInto};
-
-    use num::complex::Complex32;
-    use serde_bytes::ByteBuf;
-
-    use crate::qpu::{
-        rpcq::RPCResponse,
-        runner::{Buffer, DataType},
-        Register,
-    };
-
-    #[test]
-    fn it_converts_numpy_int16() {
-        // Data extracted from Python's NumPy
-        let data = [1, 0, 2, 0, 3, 0];
-        let buf = Buffer {
-            shape: vec![3],
-            data: ByteBuf::from(data),
-            dtype: DataType::Int16,
-        };
-        let expected = Register::I16(vec![1, 2, 3]);
-
-        let actual: Register = buf.try_into().expect("Failed to convert into u16s");
-
-        assert_eq!(expected, actual);
-    }
-
-    #[test]
-    fn it_converts_numpy_int8() {
-        // Data extracted from Python's NumPy
-        let data = [255, 0, 1];
-        let buf = Buffer {
-            shape: vec![3],
-            data: ByteBuf::from(data),
-            dtype: DataType::Int8,
-        };
-        let expected = Register::I8(vec![-1, 0, 1]);
-
-        let actual: Register = buf.try_into().expect("Failed to convert into u16s");
-
-        assert_eq!(expected, actual);
-    }
-
-    #[test]
-    fn it_converts_numpy_float64() {
-        // Data extracted from Python's NumPy
-        let data = [
-            0, 0, 0, 0, 0, 0, 240, 63, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 8, 64,
-        ];
-        let buf = Buffer {
-            shape: vec![3],
-            data: ByteBuf::from(data),
-            dtype: DataType::Float64,
-        };
-        let expected = Register::F64(vec![1.0, 2.0, 3.0]);
-
-        let actual: Register = buf.try_into().expect("Failed to convert into f64s");
-
-        assert_eq!(expected, actual);
-    }
-
-    #[test]
-    fn it_converts_numpy_complex64() {
-        // Data extracted from Python's NumPy
-        let data = [
-            0, 0, 128, 63, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 64, 64, 0, 0, 0, 0,
-        ];
-        let buf = Buffer {
-            shape: vec![3],
-            data: ByteBuf::from(data),
-            dtype: DataType::Complex64,
-        };
-        let expected = Register::Complex32(vec![
-            Complex32::new(1.0, 0.0),
-            Complex32::new(2.0, 0.0),
-            Complex32::new(3.0, 0.0),
-        ]);
-
-        let actual: Register = buf.try_into().expect("Failed to convert into Complex32s");
-
-        assert_eq!(expected, actual);
-    }
-
-    #[test]
-    fn it_deserializes_properly() {
-        // Real response data for a Bell State program retrieved from Aspen 9
-        let hex = std::fs::read_to_string("tests/bell_state_response_data.hex")
-            .expect("Unable to load Aspen-9 Bell State raw response");
-        let data = hex::decode(hex).unwrap();
-        let resp: RPCResponse<HashMap<String, Buffer>> =
-            rmp_serde::from_read(data.as_slice()).unwrap();
-        let mut buffers = match resp {
-            RPCResponse::RPCReply { result, .. } => result,
-            RPCResponse::RPCError { .. } => unreachable!(),
-        };
-        let q0 = buffers.remove("q0").expect("Could not find buffer q0");
-        let q1 = buffers.remove("q1").expect("Could not find buffer q1");
-
-        assert_eq!(q0, q1);
-        assert!(matches!(q0.dtype, DataType::Int8));
-
-        let q0_data: Register = q0.try_into().unwrap();
-        let q1_data: Register = q1.try_into().unwrap();
-        let expected_data = Register::I8(vec![0, 1]);
-        assert_eq!(q0_data, expected_data);
-        assert_eq!(q1_data, expected_data);
-    }
-}
diff --git a/crates/lib/src/qpu/translation.rs b/crates/lib/src/qpu/translation.rs
index c393d902e..306411a01 100644
--- a/crates/lib/src/qpu/translation.rs
+++ b/crates/lib/src/qpu/translation.rs
@@ -1,75 +1,45 @@
-use reqwest::StatusCode;
+use std::collections::HashMap;
 
-use qcs_api::apis::translation_api as translation;
-use qcs_api::apis::translation_api::TranslateNativeQuilToEncryptedBinaryError;
-use qcs_api::apis::Error as UntypedApiError;
-use qcs_api::models::{
-    Error as QcsError, TranslateNativeQuilToEncryptedBinaryRequest,
-    TranslateNativeQuilToEncryptedBinaryResponse,
+use qcs_api_client_grpc::{
+    models::controller::EncryptedControllerJob,
+    services::translation::{
+        translate_quil_to_encrypted_controller_job_request::NumShots,
+        TranslateQuilToEncryptedControllerJobRequest,
+    },
 };
 
-use crate::configuration::Configuration;
-use crate::qpu::rewrite_arithmetic::RewrittenQuil;
+use super::client::{GrpcClientError, Qcs};
 
-type ApiError = UntypedApiError<TranslateNativeQuilToEncryptedBinaryError>;
+pub(crate) struct EncryptedTranslationResult {
+    pub(crate) job: EncryptedControllerJob,
+    pub(crate) readout_map: HashMap<String, String>,
+}
 
 pub(crate) async fn translate(
-    quil: RewrittenQuil,
-    shots: u16,
     quantum_processor_id: &str,
-    config: &Configuration,
-) -> Result<TranslateNativeQuilToEncryptedBinaryResponse, Error> {
-    let translation_request = TranslateNativeQuilToEncryptedBinaryRequest {
-        num_shots: shots.into(),
-        quil: quil.into(),
-        settings_timestamp: None,
+    quil_program: &str,
+    num_shots: u32,
+    client: &Qcs,
+) -> Result<EncryptedTranslationResult, GrpcClientError> {
+    let request = TranslateQuilToEncryptedControllerJobRequest {
+        quantum_processor_id: Some(quantum_processor_id.to_owned()),
+        num_shots: Some(NumShots::NumShotsValue(num_shots)),
+        quil_program: Some(quil_program.to_owned()),
     };
-    translation::translate_native_quil_to_encrypted_binary(
-        config.as_ref(),
-        quantum_processor_id,
-        translation_request,
-    )
-    .await
-    .map_err(Error::from)
-}
 
-/// Errors that can occur during Translation
-#[derive(Debug, thiserror::Error)]
-pub enum Error {
-    #[error("Provided program could not be translated: {}", .0.message)]
-    ProgramIssue(QcsError),
-    #[error("Problem connecting to QCS")]
-    Connection(#[source] ApiError),
-    #[error("Serialization failed, this is likely a bug in this library")]
-    Serialization(#[from] serde_json::Error),
-    #[error("Unauthorized request")]
-    Unauthorized,
-    #[error("An unknown error occurred, this is likely a bug in this library: {0}")]
-    Unknown(String),
-}
+    let response = client
+        .get_translation_client()?
+        .translate_quil_to_encrypted_controller_job(request)
+        .await?
+        .into_inner();
 
-impl From<ApiError> for Error {
-    fn from(error: ApiError) -> Self {
-        match error {
-            ApiError::Reqwest(_) | ApiError::Io(_) => Self::Connection(error),
-            ApiError::Serde(inner) => inner.into(),
-            ApiError::ResponseError(inner) => {
-                if matches!(
-                    inner.status,
-                    StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN
-                ) {
-                    Self::Unauthorized
-                } else {
-                    match inner.entity {
-                        Some(TranslateNativeQuilToEncryptedBinaryError::Status400(detail))
-                            if inner.status == StatusCode::BAD_REQUEST =>
-                        {
-                            Self::ProgramIssue(detail)
-                        }
-                        _ => Self::Unknown(inner.content),
-                    }
-                }
-            }
-        }
-    }
+    Ok(EncryptedTranslationResult {
+        job: response
+            .job
+            .ok_or_else(|| GrpcClientError::ResponseEmpty("Encrypted Job".into()))?,
+        readout_map: response
+            .metadata
+            .ok_or_else(|| GrpcClientError::ResponseEmpty("Job Metadata".into()))?
+            .readout_mappings,
+    })
 }
diff --git a/crates/lib/src/qvm/execution.rs b/crates/lib/src/qvm/execution.rs
index f3cfe3f91..2f3715cad 100644
--- a/crates/lib/src/qvm/execution.rs
+++ b/crates/lib/src/qvm/execution.rs
@@ -1,13 +1,13 @@
 use std::collections::HashMap;
 use std::str::FromStr;
 
+use qcs_api_client_common::ClientConfiguration;
 use quil_rs::{
     instruction::{ArithmeticOperand, Instruction, MemoryReference, Move},
     program::ProgramError,
     Program,
 };
 
-use crate::configuration::Configuration;
 use crate::executable::Parameters;
 use crate::RegisterData;
 
@@ -60,7 +60,7 @@ impl Execution {
         shots: u16,
         readouts: &[&str],
         params: &Parameters,
-        config: &Configuration,
+        config: &ClientConfiguration,
     ) -> Result<HashMap<Box<str>, RegisterData>, Error> {
         if shots == 0 {
             return Err(Error::ShotsMustBePositive);
@@ -109,24 +109,24 @@ impl Execution {
         &self,
         shots: u16,
         readouts: &[&str],
-        config: &Configuration,
+        config: &ClientConfiguration,
     ) -> Result<HashMap<Box<str>, RegisterData>, Error> {
         let request = Request::new(&self.program.to_string(true), shots, readouts);
 
         let client = reqwest::Client::new();
         let response = client
-            .post(&config.qvm_url)
+            .post(config.qvm_url())
             .json(&request)
             .send()
             .await
             .map_err(|source| Error::QvmCommunication {
-                qvm_url: config.qvm_url.clone(),
+                qvm_url: config.qvm_url().into(),
                 source,
             })?;
 
         match response.json::<Response>().await {
             Err(source) => Err(Error::QvmCommunication {
-                qvm_url: config.qvm_url.clone(),
+                qvm_url: config.qvm_url().into(),
                 source,
             }),
             Ok(Response::Success(response)) => Ok(response
@@ -167,7 +167,7 @@ pub(crate) enum Error {
 
 #[cfg(test)]
 mod describe_execution {
-    use super::{Configuration, Execution, Parameters};
+    use super::{ClientConfiguration, Execution, Parameters};
 
     #[tokio::test]
     async fn it_errs_on_excess_parameters() {
@@ -176,7 +176,9 @@ mod describe_execution {
         let mut params = Parameters::new();
         params.insert("doesnt_exist".into(), vec![0.0]);
 
-        let result = exe.run(1, &[], &params, &Configuration::default()).await;
+        let result = exe
+            .run(1, &[], &params, &ClientConfiguration::default())
+            .await;
         if let Err(e) = result {
             assert!(e.to_string().contains("doesnt_exist"));
         } else {
@@ -191,7 +193,9 @@ mod describe_execution {
         let mut params = Parameters::new();
         params.insert("ro".into(), vec![0.0]);
 
-        let result = exe.run(1, &[], &params, &Configuration::default()).await;
+        let result = exe
+            .run(1, &[], &params, &ClientConfiguration::default())
+            .await;
         if let Err(e) = result {
             let err_string = e.to_string();
             assert!(err_string.contains("ro"));
diff --git a/crates/lib/src/register_data.rs b/crates/lib/src/register_data.rs
index 31a824d2c..e29f62942 100644
--- a/crates/lib/src/register_data.rs
+++ b/crates/lib/src/register_data.rs
@@ -1,17 +1,13 @@
-use std::collections::HashMap;
-
 use enum_as_inner::EnumAsInner;
 use num::complex::Complex32;
 use serde::{Deserialize, Serialize};
 
-use crate::qpu::{DecodeError, Register};
-
-/// Data resulting from [`Executable::execute_on_qvm`][`crate::Executable::execute_on_qvm`] or
-/// [`Executable::execute_on_qpu`][`crate::Executable::execute_on_qpu`].
+/// Data resulting from [`Executable::execute_on_qvm`](`crate::Executable::execute_on_qvm`) or
+/// [`Executable::execute_on_qpu`](`crate::Executable::execute_on_qpu`).
 ///
 /// This represents a single vector (or "register") of typed memory across some number of shots.
 /// The register corresponds to the usage of a `DECLARE` instruction in Quil, and the name of that
-/// register should be provided with [`Executable::read_from`][`crate::Executable::read_from`].
+/// register should be provided with [`Executable::read_from`](`crate::Executable::read_from`).
 ///
 /// There is a variant of this enum for each type of data that a register could hold.
 /// Any variant of an instance of `ExecutionResult` will contain a `Vec` with one entry for each shot,
@@ -35,188 +31,3 @@ pub enum RegisterData {
     #[serde(skip)]
     Complex32(Vec<Vec<Complex32>>),
 }
-
-impl RegisterData {
-    pub(crate) fn try_from_registers(
-        registers_by_name: HashMap<Box<str>, Vec<Register>>,
-        shots: u16,
-    ) -> Result<HashMap<Box<str>, Self>, DecodeError> {
-        registers_by_name
-            .into_iter()
-            .map(|(register_name, registers)| {
-                let first_register = registers
-                    .get(0)
-                    .ok_or_else(|| DecodeError::MissingBuffer(register_name.to_string()))?;
-                match first_register {
-                    Register::I8(_) => Self::try_from_i8_registers(registers, shots),
-                    Register::I16(_) => Self::try_from_i16_registers(registers, shots),
-                    Register::F64(_) => Self::try_from_f64_registers(registers, shots),
-                    Register::Complex32(_) => Self::try_from_complex32_registers(registers, shots),
-                }
-                .map(|execution_result| (register_name, execution_result))
-            })
-            .collect()
-    }
-
-    fn try_from_i8_registers(
-        registers: Vec<Register>,
-        number_of_shots: u16,
-    ) -> Result<Self, DecodeError> {
-        let registers = registers
-            .into_iter()
-            .map(|register| register.into_i8().map_err(|_| DecodeError::MixedTypes))
-            .collect::<Result<Vec<Vec<i8>>, DecodeError>>()?;
-
-        Ok(Self::I8(transpose(registers, number_of_shots)))
-    }
-
-    fn try_from_i16_registers(
-        registers: Vec<Register>,
-        number_of_shots: u16,
-    ) -> Result<Self, DecodeError> {
-        let registers = registers
-            .into_iter()
-            .map(|register| register.into_i16().map_err(|_| DecodeError::MixedTypes))
-            .collect::<Result<Vec<Vec<i16>>, DecodeError>>()?;
-
-        Ok(Self::I16(transpose(registers, number_of_shots)))
-    }
-
-    fn try_from_f64_registers(
-        registers: Vec<Register>,
-        number_of_shots: u16,
-    ) -> Result<Self, DecodeError> {
-        let registers = registers
-            .into_iter()
-            .map(|register| register.into_f64().map_err(|_| DecodeError::MixedTypes))
-            .collect::<Result<Vec<Vec<f64>>, DecodeError>>()?;
-
-        Ok(Self::F64(transpose(registers, number_of_shots)))
-    }
-
-    fn try_from_complex32_registers(
-        registers: Vec<Register>,
-        number_of_shots: u16,
-    ) -> Result<Self, DecodeError> {
-        let registers = registers
-            .into_iter()
-            .map(|register| {
-                register
-                    .into_complex32()
-                    .map_err(|_| DecodeError::MixedTypes)
-            })
-            .collect::<Result<Vec<Vec<Complex32>>, DecodeError>>()?;
-
-        Ok(Self::Complex32(transpose(registers, number_of_shots)))
-    }
-}
-
-fn transpose<T>(mut data: Vec<Vec<T>>, len: u16) -> Vec<Vec<T>> {
-    let mut results = Vec::with_capacity(len as usize);
-
-    for _ in 0..len {
-        let mut new_inner = Vec::with_capacity(data.len());
-        for old_inner in &mut data {
-            new_inner.push(old_inner.remove(0));
-        }
-        results.push(new_inner);
-    }
-    results
-}
-
-#[cfg(test)]
-mod describe_program_results {
-    use maplit::hashmap;
-    use num::complex::Complex32;
-
-    use crate::{qpu::Register, RegisterData};
-
-    #[test]
-    fn it_converts_from_i8_registers() {
-        let registers = hashmap! {
-            Box::from(String::from("ro")) => vec![Register::I8(vec![1, 2, 3]), Register::I8(vec![4, 5, 6])]
-        };
-        let results = RegisterData::try_from_registers(registers, 3).unwrap();
-        let expected = hashmap! {
-            Box::from(String::from("ro")) => RegisterData::I8(vec![vec![1, 4], vec![2, 5], vec![3, 6]])
-        };
-        assert_eq!(results, expected);
-    }
-
-    #[test]
-    fn it_converts_from_i16_registers() {
-        let registers = hashmap! {
-            Box::from(String::from("ro")) => vec![Register::I16(vec![1, 2, 3]), Register::I16(vec![4, 5, 6])]
-        };
-        let results = RegisterData::try_from_registers(registers, 3).unwrap();
-        let expected = hashmap! {
-            Box::from(String::from("ro")) => RegisterData::I16(vec![vec![1, 4], vec![2, 5], vec![3, 6]])
-        };
-        assert_eq!(results, expected);
-    }
-
-    #[test]
-    fn it_converts_from_f64_registers() {
-        let registers = hashmap! {
-            Box::from(String::from("ro")) => vec![
-                Register::F64(vec![1.0, 2.0, 3.0]),
-                Register::F64(vec![4.0, 5.0, 6.0]),
-            ]
-        };
-        let results = RegisterData::try_from_registers(registers, 3).unwrap();
-        let expected = hashmap! {
-            Box::from(String::from("ro")) => RegisterData::F64(vec![vec![1.0, 4.0], vec![2.0, 5.0], vec![3.0, 6.0]])
-        };
-        assert_eq!(results, expected);
-    }
-
-    #[test]
-    fn it_converts_from_complex32_registers() {
-        let registers = hashmap! {
-            Box::from(String::from("ro")) => vec![
-                Register::Complex32(vec![
-                    Complex32::from(1.0),
-                    Complex32::from(2.0),
-                    Complex32::from(3.0),
-                ]),
-                Register::Complex32(vec![
-                    Complex32::from(4.0),
-                    Complex32::from(5.0),
-                    Complex32::from(6.0),
-                ]),
-            ]
-        };
-        let results = RegisterData::try_from_registers(registers, 3).unwrap();
-        let expected = hashmap! {
-            Box::from(String::from("ro")) => RegisterData::Complex32(vec![
-                vec![Complex32::from(1.0), Complex32::from(4.0)],
-                vec![Complex32::from(2.0), Complex32::from(5.0)],
-                vec![Complex32::from(3.0), Complex32::from(6.0)],
-            ])
-        };
-        assert_eq!(results, expected);
-    }
-
-    #[test]
-    fn it_errors_on_mismatch_types() {
-        let registers = hashmap! {
-            Box::from(String::from("ro")) => vec![Register::I8(vec![1, 2, 3]), Register::I16(vec![4, 5, 6])]
-        };
-        let results = RegisterData::try_from_registers(registers, 3);
-        assert!(results.is_err());
-    }
-
-    #[test]
-    fn it_handles_mixed_types() {
-        let registers = hashmap! {
-            Box::from(String::from("first")) => vec![Register::I8(vec![1, 2, 3]), Register::I8(vec![4, 5, 6])],
-            Box::from(String::from("second")) => vec![Register::I16(vec![1, 2, 3]), Register::I16(vec![4, 5, 6])],
-        };
-        let results = RegisterData::try_from_registers(registers, 3).unwrap();
-        let expected = hashmap! {
-            Box::from(String::from("first")) => RegisterData::I8(vec![vec![1, 4], vec![2, 5], vec![3, 6]]),
-            Box::from(String::from("second")) => RegisterData::I16(vec![vec![1, 4], vec![2, 5], vec![3, 6]]),
-        };
-        assert_eq!(results, expected);
-    }
-}
diff --git a/crates/python/Cargo.toml b/crates/python/Cargo.toml
index 47cbe4f8d..4940b0aa0 100644
--- a/crates/python/Cargo.toml
+++ b/crates/python/Cargo.toml
@@ -17,9 +17,11 @@ crate-type = ["cdylib"]
 
 [dependencies]
 qcs = { path = "../lib" }
+qcs-api-client-common = "0.2.3"
 pyo3 = { version = "0.17", features = ["extension-module"] }
 pyo3-asyncio = { version = "0.17", features = ["tokio-runtime"] }
 pythonize = "0.17"
+quil-rs = "0.14"
 tokio = "1.21"
 qcs-api = "0.2.1"
 serde_json = "1.0.86"
diff --git a/crates/python/src/lib.rs b/crates/python/src/lib.rs
index 2ea3152ae..0d3f8b93b 100644
--- a/crates/python/src/lib.rs
+++ b/crates/python/src/lib.rs
@@ -1,25 +1,31 @@
-use ::qcs::configuration::Configuration;
 use pythonize::pythonize;
 use qcs::api;
+use qcs::qpu::client::Qcs;
 use qcs::qpu::quilc::TargetDevice;
 use std::collections::HashMap;
 
-use pyo3::{create_exception, exceptions::PyRuntimeError, prelude::*};
+use pyo3::{
+    create_exception,
+    exceptions::{PyRuntimeError, PyValueError},
+    prelude::*,
+};
 
 create_exception!(qcs, InvalidConfigError, PyRuntimeError);
 create_exception!(qcs, ExecutionError, PyRuntimeError);
 create_exception!(qcs, TranslationError, PyRuntimeError);
 create_exception!(qcs, CompilationError, PyRuntimeError);
+create_exception!(qcs, RewriteArithmeticError, PyRuntimeError);
+create_exception!(qcs, DeviceIsaError, PyValueError);
 
 #[pyfunction]
 fn compile(py: Python<'_>, quil: String, target_device: String) -> PyResult<&PyAny> {
-    let target_device: TargetDevice = serde_json::from_str(&target_device)
-        .map_err(|e| CompilationError::new_err(e.to_string()))?;
+    let target_device: TargetDevice =
+        serde_json::from_str(&target_device).map_err(|e| DeviceIsaError::new_err(e.to_string()))?;
     pyo3_asyncio::tokio::future_into_py(py, async move {
-        let config = Configuration::load()
+        let client = Qcs::load()
             .await
             .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
-        let result = api::compile(&quil, target_device, &config)
+        let result = api::compile(&quil, target_device, &client)
             .map_err(|e| CompilationError::new_err(e.to_string()))?;
         Ok(Python::with_gil(|_py| result))
     })
@@ -27,8 +33,11 @@ fn compile(py: Python<'_>, quil: String, target_device: String) -> PyResult<&PyA
 
 #[pyfunction]
 fn rewrite_arithmetic(py: Python<'_>, native_quil: String) -> PyResult<PyObject> {
-    let result = api::rewrite_arithmetic(&native_quil)
+    let native_program = native_quil
+        .parse::<quil_rs::Program>()
         .map_err(|e| TranslationError::new_err(e.to_string()))?;
+    let result = api::rewrite_arithmetic(native_program)
+        .map_err(|e| RewriteArithmeticError::new_err(e.to_string()))?;
     let pyed = pythonize(py, &result).map_err(|e| TranslationError::new_err(e.to_string()))?;
     Ok(pyed)
 }
@@ -58,10 +67,10 @@ fn translate(
     quantum_processor_id: String,
 ) -> PyResult<&PyAny> {
     pyo3_asyncio::tokio::future_into_py(py, async move {
-        let config = Configuration::load()
+        let client = Qcs::load()
             .await
             .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
-        let result = api::translate(&native_quil, num_shots, &quantum_processor_id, &config)
+        let result = api::translate(&native_quil, num_shots, &quantum_processor_id, &client)
             .await
             .map_err(|e| TranslationError::new_err(e.to_string()))?;
         let result = Python::with_gil(|py| {
@@ -77,12 +86,14 @@ fn submit(
     program: String,
     patch_values: HashMap<String, Vec<f64>>,
     quantum_processor_id: String,
+    use_gateway: bool,
 ) -> PyResult<&PyAny> {
     pyo3_asyncio::tokio::future_into_py(py, async move {
-        let config = Configuration::load()
+        let client = Qcs::load()
             .await
-            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
-        let job_id = api::submit(&program, patch_values, &quantum_processor_id, &config)
+            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?
+            .with_use_gateway(use_gateway);
+        let job_id = api::submit(&program, patch_values, &quantum_processor_id, &client)
             .await
             .map_err(|e| ExecutionError::new_err(e.to_string()))?;
         Ok(Python::with_gil(|_py| job_id))
@@ -94,12 +105,14 @@ fn retrieve_results(
     py: Python<'_>,
     job_id: String,
     quantum_processor_id: String,
+    use_gateway: bool,
 ) -> PyResult<&PyAny> {
     pyo3_asyncio::tokio::future_into_py(py, async move {
-        let config = Configuration::load()
+        let client = Qcs::load()
             .await
-            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?;
-        let results = api::retrieve_results(&job_id, &quantum_processor_id, &config)
+            .map_err(|e| InvalidConfigError::new_err(e.to_string()))?
+            .with_use_gateway(use_gateway);
+        let results = api::retrieve_results(&job_id, &quantum_processor_id, &client)
             .await
             .map_err(|e| ExecutionError::new_err(e.to_string()))?;
         let results = Python::with_gil(|py| {


Test patch:
diff --git a/crates/lib/tests/basic_qvm.rs b/crates/lib/tests/basic_qvm.rs
index 59728a3c1..c107c7591 100644
--- a/crates/lib/tests/basic_qvm.rs
+++ b/crates/lib/tests/basic_qvm.rs
@@ -2,6 +2,7 @@
 //! In order to run them, QVM's web server must be running at localhost:5000.
 
 use qcs::Executable;
+use qcs_api_client_openapi::common::ClientConfiguration;
 
 const PROGRAM: &str = r##"
 DECLARE first BIT
@@ -19,6 +20,7 @@ async fn test_bell_state() {
     const SHOTS: u16 = 10;
 
     let mut data = Executable::from_quil(PROGRAM)
+        .with_config(ClientConfiguration::default())
         .with_shots(SHOTS)
         .read_from("first")
         .read_from("second")
diff --git a/crates/lib/tests/mocked_qpu.rs b/crates/lib/tests/mocked_qpu.rs
index da4d601ea..6fa5b7676 100644
--- a/crates/lib/tests/mocked_qpu.rs
+++ b/crates/lib/tests/mocked_qpu.rs
@@ -1,13 +1,13 @@
 //! Use some local servers to stub out real requests to QCS in order to test the end to end flow of
 //! the `qcs` crate.
 
-use std::thread;
 use std::time::Duration;
 
-use maplit::hashmap;
-
-use qcs::configuration::{SECRETS_PATH_VAR, SETTINGS_PATH_VAR};
-use qcs::{Executable, RegisterData};
+use qcs::Executable;
+use qcs_api_client_common::configuration::{SECRETS_PATH_VAR, SETTINGS_PATH_VAR};
+use qcs_api_client_grpc::models::controller::{
+    readout_values::Values, IntegerReadoutValues, ReadoutValues,
+};
 
 const BELL_STATE: &str = r#"
 DECLARE ro BIT[2]
@@ -30,8 +30,23 @@ async fn successful_bell_state() {
         .await
         .expect("Failed to run program that should be successful");
     assert_eq!(
-        result.registers,
-        hashmap! {Box::from(String::from("ro")) => RegisterData::I8(vec![vec![0, 0], vec![1, 1]])}
+        result
+            .readout_data
+            .get_readout_values_for_field("ro")
+            .expect("should have values for `ro`")
+            .unwrap(),
+        vec![
+            Some(ReadoutValues {
+                values: Some(Values::IntegerValues(IntegerReadoutValues {
+                    values: vec![0, 0],
+                })),
+            }),
+            Some(ReadoutValues {
+                values: Some(Values::IntegerValues(IntegerReadoutValues {
+                    values: vec![1, 1],
+                })),
+            }),
+        ],
     );
     assert_eq!(result.duration, Some(Duration::from_micros(8675)));
 }
@@ -40,7 +55,8 @@ async fn setup() {
     simple_logger::init_with_env().unwrap();
     std::env::set_var(SETTINGS_PATH_VAR, "tests/settings.toml");
     std::env::set_var(SECRETS_PATH_VAR, "tests/secrets.toml");
-    thread::spawn(qpu::run);
+    tokio::spawn(qpu::run());
+    tokio::spawn(translation::run());
     tokio::spawn(auth_server::run());
     tokio::spawn(mock_qcs::run());
 }
@@ -80,10 +96,10 @@ mod auth_server {
 #[allow(dead_code)]
 mod mock_qcs {
     use serde::{Deserialize, Serialize};
+    use serde_json::json;
     use warp::Filter;
 
-    use qcs_api::models::{
-        CreateEngagementRequest, EngagementCredentials, EngagementWithCredentials,
+    use qcs_api_client_openapi::models::{
         InstructionSetArchitecture, TranslateNativeQuilToEncryptedBinaryRequest,
         TranslateNativeQuilToEncryptedBinaryResponse,
     };
@@ -131,162 +147,177 @@ mod mock_qcs {
                     settings_timestamp: None,
                 })
             });
-        let quantum_processors = warp::path("quantumProcessors").and(isa.or(translate));
 
-        let engagements = warp::path("engagements")
-            .and(warp::post())
-            .and(warp::body::json())
-            .map(|_request: CreateEngagementRequest| {
-                warp::reply::json(&EngagementWithCredentials {
-                    account_id: None,
-                    account_type: None,
-                    address: "tcp://localhost:8002".to_string(),
-                    credentials: Box::new(EngagementCredentials {
-                        client_public: String::new(),
-                        client_secret: String::new(),
-                        server_public: String::new(),
-                    }),
-                    endpoint_id: "".to_string(),
-                    expires_at: "".to_string(),
-                    minimum_priority: None,
-                    quantum_processor_ids: None,
-                    tags: None,
-                    user_id: "".to_string(),
-                })
+        let default_endpoint = warp::path(QPU_ID)
+            .and(warp::path("endpoints:getDefault"))
+            .and(warp::get())
+            .map(|| {
+                let endpoint = json!({
+                    "address": "",
+                    "addresses": {
+                        "grpc": "http://127.0.0.1:8002",
+                    },
+                    "datacenter": "west-1",
+                    "healthy": true,
+                    "id": QPU_ID.to_string(),
+                    "mock": true,
+                    "quantumProcessorIds": [QPU_ID.to_string()],
+                });
+                warp::reply::json(&endpoint)
             });
 
-        warp::serve(warp::path("v1").and(quantum_processors.or(engagements)))
+        let quantum_processors =
+            warp::path("quantumProcessors").and(isa.or(translate).or(default_endpoint));
+
+        warp::serve(warp::path("v1").and(quantum_processors))
             .run(([127, 0, 0, 1], 8000))
             .await;
     }
 }
 
-#[allow(dead_code)]
-mod qpu {
+mod translation {
     use std::collections::HashMap;
 
-    use log::{debug, error};
-    use rmp_serde::Serializer;
-    use serde::{Deserialize, Serialize};
-
-    #[derive(Deserialize)]
-    #[serde(tag = "_type")]
-    #[allow(dead_code)]
-    pub struct RPCRequest {
-        method: String,
-        params: Params,
-        id: String,
-        jsonrpc: String,
-        client_timeout: u8,
-        client_key: Option<String>,
+    use qcs_api_client_grpc::models::controller::EncryptedControllerJob;
+    use qcs_api_client_grpc::models::translation::QuilTranslationMetadata;
+    use qcs_api_client_grpc::services::translation::translation_server::{
+        Translation, TranslationServer,
+    };
+    use qcs_api_client_grpc::services::translation::{
+        TranslateQuilToEncryptedControllerJobRequest, TranslateQuilToEncryptedControllerJobResponse,
+    };
+    use tonic::{transport::Server, Request};
+    use tonic::{Response, Status};
+
+    #[derive(Default, Debug)]
+    pub struct TranslationService {}
+
+    #[tonic::async_trait]
+    impl Translation for TranslationService {
+        async fn translate_quil_to_encrypted_controller_job(
+            &self,
+            _request: Request<TranslateQuilToEncryptedControllerJobRequest>,
+        ) -> Result<Response<TranslateQuilToEncryptedControllerJobResponse>, Status> {
+            Ok(Response::new(
+                TranslateQuilToEncryptedControllerJobResponse {
+                    job: Some(EncryptedControllerJob {
+                        job: None,
+                        encryption: None,
+                    }),
+                    metadata: Some(QuilTranslationMetadata {
+                        readout_mappings: HashMap::from([
+                            ("ro[0]".to_string(), "q0".to_string()),
+                            ("ro[1]".to_string(), "q1".to_string()),
+                        ]),
+                    }),
+                },
+            ))
+        }
     }
 
-    #[derive(Deserialize, Debug)]
-    #[serde(untagged)]
-    enum Params {
-        QPUParams { request: QPURequest, priority: u8 },
-        GetExecutionResultsRequest { job_id: String, wait: bool },
+    pub(crate) async fn run() {
+        let service = TranslationService::default();
+        Server::builder()
+            .add_service(TranslationServer::new(service))
+            .serve("127.0.0.1:8003".parse().expect("address can be parsed"))
+            .await
+            .expect("service runs without errors");
     }
+}
 
-    #[derive(Deserialize, Debug)]
-    #[serde(tag = "_type")]
-    struct QPURequest {
-        id: String,
-        program: String,
-        patch_values: HashMap<String, Vec<f64>>,
-    }
+mod qpu {
+    use std::collections::HashMap;
 
-    #[derive(Serialize, Debug)]
-    #[serde(tag = "_type")]
-    #[allow(dead_code)]
-    pub enum RPCResponse<T> {
-        RPCReply { id: String, result: T },
-        RPCError { id: String, error: String },
-    }
+    use qcs_api_client_grpc::{
+        models::controller::{
+            readout_values::Values, ControllerJobExecutionResult, IntegerReadoutValues,
+            ReadoutValues,
+        },
+        services::controller::{
+            controller_server::{Controller, ControllerServer},
+            BatchExecuteControllerJobsRequest, BatchExecuteControllerJobsResponse,
+            CancelControllerJobsRequest, CancelControllerJobsResponse, ExecuteControllerJobRequest,
+            ExecuteControllerJobResponse, GetControllerJobResultsRequest,
+            GetControllerJobResultsResponse, GetControllerJobStatusRequest,
+            GetControllerJobStatusResponse,
+        },
+    };
+    use tonic::{transport::Server, Request, Response, Status};
+
+    #[derive(Default, Debug)]
+    pub struct ControllerService {}
+
+    #[tonic::async_trait]
+    impl Controller for ControllerService {
+        async fn execute_controller_job(
+            &self,
+            _request: Request<ExecuteControllerJobRequest>,
+        ) -> Result<Response<ExecuteControllerJobResponse>, Status> {
+            Ok(Response::new(ExecuteControllerJobResponse {
+                job_execution_ids: vec!["job-id".to_string()],
+            }))
+        }
 
-    #[derive(Serialize, Debug, PartialEq, Eq)]
-    pub struct Buffer {
-        shape: (usize,),
-        data: [u8; 2],
-        dtype: String,
-    }
+        async fn batch_execute_controller_jobs(
+            &self,
+            _request: Request<BatchExecuteControllerJobsRequest>,
+        ) -> Result<Response<BatchExecuteControllerJobsResponse>, Status> {
+            unimplemented!()
+        }
 
-    #[derive(Serialize, Debug, PartialEq, Eq)]
-    pub struct GetExecutionResultsResponse {
-        buffers: HashMap<&'static str, Buffer>,
-        execution_duration_microseconds: u64,
-    }
+        async fn get_controller_job_results(
+            &self,
+            _request: Request<GetControllerJobResultsRequest>,
+        ) -> Result<Response<GetControllerJobResultsResponse>, Status> {
+            Ok(Response::new(GetControllerJobResultsResponse {
+                result: Some(ControllerJobExecutionResult {
+                    memory_values: HashMap::new(),
+                    readout_values: HashMap::from([
+                        (
+                            "q0".to_string(),
+                            ReadoutValues {
+                                values: Some(Values::IntegerValues(IntegerReadoutValues {
+                                    values: vec![0, 0],
+                                })),
+                            },
+                        ),
+                        (
+                            "q1".to_string(),
+                            ReadoutValues {
+                                values: Some(Values::IntegerValues(IntegerReadoutValues {
+                                    values: vec![1, 1],
+                                })),
+                            },
+                        ),
+                    ]),
+                    status: Some(0),
+                    status_message: Some("success".to_string()),
+                    execution_duration_microseconds: Some(8675),
+                }),
+            }))
+        }
 
-    pub(crate) fn run() {
-        let ctx = zmq::Context::new();
-
-        let server = ctx.socket(zmq::ROUTER).unwrap();
-        server.bind("tcp://127.0.0.1:8002").unwrap();
-
-        loop {
-            let (identity, data) = match server.recv_multipart(0) {
-                Ok(mut parts) => {
-                    if parts.len() != 2 {
-                        error!("Invalid multipart data: {:?}", parts);
-                        break;
-                    }
-                    let data = parts.pop().unwrap();
-                    let identity = parts.pop().unwrap();
-                    (identity, data)
-                }
-                Err(e) => {
-                    error!("{}", e);
-                    break;
-                }
-            };
-            let response: Box<dyn erased_serde::Serialize> =
-                match rmp_serde::from_read::<_, RPCRequest>(data.as_slice()) {
-                    Ok(qpu_request) => match qpu_request.params {
-                        Params::QPUParams { .. } => Box::new(RPCResponse::RPCReply {
-                            id: qpu_request.id,
-                            result: "1",
-                        }),
-                        Params::GetExecutionResultsRequest { .. } => {
-                            let mut buffers = HashMap::new();
-                            buffers.insert(
-                                "q0",
-                                Buffer {
-                                    shape: (2,),
-                                    data: [0, 1],
-                                    dtype: "int8".to_string(),
-                                },
-                            );
-                            buffers.insert(
-                                "q1",
-                                Buffer {
-                                    shape: (2,),
-                                    data: [0, 1],
-                                    dtype: "int8".to_string(),
-                                },
-                            );
-                            let resp = GetExecutionResultsResponse {
-                                buffers,
-                                execution_duration_microseconds: 8675,
-                            };
-                            Box::new(RPCResponse::RPCReply {
-                                id: qpu_request.id,
-                                result: resp,
-                            })
-                        }
-                    },
-                    Err(e) => {
-                        debug!("{:?} could not be decoded with MsgPack:\n\t{}", data, e);
-                        continue;
-                    }
-                };
-
-            let mut response_buffer = vec![];
-            response
-                .serialize(&mut Serializer::new(&mut response_buffer).with_struct_map())
-                .unwrap();
-            server
-                .send_multipart([identity, response_buffer], 0)
-                .unwrap();
+        async fn cancel_controller_jobs(
+            &self,
+            _request: Request<CancelControllerJobsRequest>,
+        ) -> Result<Response<CancelControllerJobsResponse>, Status> {
+            unimplemented!()
         }
+
+        async fn get_controller_job_status(
+            &self,
+            _request: Request<GetControllerJobStatusRequest>,
+        ) -> Result<Response<GetControllerJobStatusResponse>, Status> {
+            unimplemented!()
+        }
+    }
+
+    pub(crate) async fn run() {
+        let service = ControllerService::default();
+        Server::builder()
+            .add_service(ControllerServer::new(service))
+            .serve("127.0.0.1:8002".parse().expect("address can be parsed"))
+            .await
+            .expect("service can be awaited");
     }
 }
diff --git a/crates/lib/tests/parametric_compilation.rs b/crates/lib/tests/parametric_compilation.rs
index 4abe205c6..f6ffcd7b2 100644
--- a/crates/lib/tests/parametric_compilation.rs
+++ b/crates/lib/tests/parametric_compilation.rs
@@ -1,9 +1,10 @@
-//! This integration test implements the [Parametric Compilation example from pyQuil][example].
+//! This integration test implements the [Parametric Compilation example from pyQuil](example).
 //! [example]: https://pyquil-docs.rigetti.com/en/stable/basics.html?highlight=parametric#parametric-compilation
 
 use std::f64::consts::PI;
 
 use qcs::Executable;
+use qcs_api_client_openapi::common::ClientConfiguration;
 
 const BASIC_SUBSTITUTION: &str = r#"
 DECLARE ro BIT
@@ -18,7 +19,8 @@ MEASURE 0 ro[0]
 
 #[tokio::test]
 async fn basic_substitution() {
-    let mut exe = Executable::from_quil(BASIC_SUBSTITUTION);
+    let mut exe =
+        Executable::from_quil(BASIC_SUBSTITUTION).with_config(ClientConfiguration::default());
     let mut parametric_measurements = Vec::with_capacity(200);
 
     let step = 2.0 * PI / 200.0;
diff --git a/crates/lib/tests/settings.toml b/crates/lib/tests/settings.toml
index 7c53b0867..93df2b6f3 100644
--- a/crates/lib/tests/settings.toml
+++ b/crates/lib/tests/settings.toml
@@ -5,6 +5,7 @@ default_profile_name = "default"
 auth_server_name = "default"
 credentials_name = "default"
 api_url = "http://localhost:8000"
+grpc_api_url = "http://localhost:8003"
 
 [auth_servers]
 [auth_servers.default]