cURL
HOST=https://api.paper.novig.com
KEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0
PEM=desk-1.pem
ID=6f9619ff-8b86-d011-b42d-00c04fc964ff
REQ_PATH="/v3/catalog/markets/$ID/book"
QUERY=""
BODY=''
TS=$(date +%s000)
HASH=$(printf %s "$BODY" \
| openssl dgst -sha256 -r | cut -d" " -f1)
# openssl signs a file, not a pipe. base64 -A never wraps.
printf 'NOVIG-V3\n%s\nGET\n%s\n%s\n%s' \
"$TS" "$REQ_PATH" "$QUERY" "$HASH" > canon.bin
SIG=$(openssl pkeyutl -sign -rawin -inkey "$PEM" \
-in canon.bin | openssl base64 -A)
curl -s -X GET "$HOST$REQ_PATH${QUERY:+?$QUERY}" \
-H "Novig-Key-Id: $KEY_ID" \
-H "Novig-Timestamp: $TS" \
-H "Novig-Signature: $SIG"use base64::prelude::*;
use ed25519_dalek::pkcs8::DecodePrivateKey;
use ed25519_dalek::{Signer, SigningKey};
use reqwest::blocking::{Client, Request};
use reqwest::{Method, Url};
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
const HOST: &str = "https://api.paper.novig.com";
const KEY_ID: &str =
"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0";
const PEM: &str = "desk-1.pem";
const ID: &str = "6f9619ff-8b86-d011-b42d-00c04fc964ff";
fn main() -> anyhow::Result<()> {
let key = Key::load(KEY_ID, PEM)?;
let client = Client::new();
let path = format!("/v3/catalog/markets/{ID}/book");
let url = format!("{HOST}{path}");
let req = client
.get(url)
.build()?
.sign(&key)?;
println!("{}", client.execute(req)?.text()?);
Ok(())
}
/// An API key: the id the server looks up, and the
/// private half that signs.
struct Key {
id: &'static str,
signer: SigningKey,
}
impl Key {
fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {
let signer =
SigningKey::read_pkcs8_pem_file(pem)?;
Ok(Self { id, signer })
}
}
/// Signs a built request over the method, path, query and
/// body it will send, so the two can never disagree.
trait Sign: Sized {
fn sign(self, key: &Key) -> anyhow::Result<Self>;
}
impl Sign for Request {
fn sign(mut self, key: &Key) -> anyhow::Result<Self> {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_millis()
.to_string();
let body = self.body().and_then(|b| b.as_bytes());
let body = body.unwrap_or_default();
let canon = Canonical::new(
&ts,
self.method(),
self.url(),
body,
);
let sig = key.signer.sign(canon.0.as_bytes());
let sig = BASE64_STANDARD.encode(sig.to_bytes());
let headers = self.headers_mut();
headers.insert("Novig-Key-Id", key.id.parse()?);
headers.insert("Novig-Timestamp", ts.parse()?);
headers.insert("Novig-Signature", sig.parse()?);
Ok(self)
}
}
/// The six NOVIG-V3 lines, joined by LF.
struct Canonical(String);
impl Canonical {
fn new(
ts: &str,
method: &Method,
url: &Url,
body: &[u8],
) -> Self {
let query =
Query::from(url.query().unwrap_or(""));
let hash = format!("{:x}", Sha256::digest(body));
let method = method.as_str();
let path = url.path();
let lines = [
"NOVIG-V3", ts, method, path, &query.0, &hash,
];
Self(lines.join("\n"))
}
}
/// Each part decoded and re-encoded, then sorted by
/// name and value.
struct Query(String);
impl From<&str> for Query {
fn from(raw: &str) -> Self {
let mut pairs = raw
.split('&')
.filter(|pair| !pair.is_empty())
.map(|pair| {
pair.split_once('=').unwrap_or((pair, ""))
})
.map(|(k, v)| {
(Self::encode(k), Self::encode(v))
})
.collect::<Vec<_>>();
pairs.sort();
let pairs =
pairs.iter().map(|(k, v)| format!("{k}={v}"));
Self(pairs.collect::<Vec<_>>().join("&"))
}
}
impl Query {
fn encode(part: &str) -> String {
Self::decode(part)
.iter()
.map(|&b| match b {
b'-' | b'.' | b'_' | b'~' => {
(b as char).to_string()
}
_ if b.is_ascii_alphanumeric() => {
(b as char).to_string()
}
_ => format!("%{b:02X}"),
})
.collect()
}
/// Only `%XX` decodes. A bare `+` stays a `+`.
fn decode(part: &str) -> Vec<u8> {
let raw = part.as_bytes();
let mut out = Vec::with_capacity(raw.len());
let mut i = 0;
while i < raw.len() {
let escape =
raw.get(i + 1..i + 3).and_then(Self::hex);
match (raw[i], escape) {
(b'%', Some(byte)) => {
out.push(byte);
i += 3;
}
(byte, _) => {
out.push(byte);
i += 1;
}
}
}
out
}
fn hex(pair: &[u8]) -> Option<u8> {
let hi = (pair[0] as char).to_digit(16)?;
let lo = (pair[1] as char).to_digit(16)?;
Some((hi * 16 + lo) as u8)
}
}
import base64, hashlib, time, urllib.request
from cryptography.hazmat.primitives.serialization import (
load_pem_private_key)
HOST = "https://api.paper.novig.com"
KEY_ID = "8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0"
PEM = "desk-1.pem"
ID = "6f9619ff-8b86-d011-b42d-00c04fc964ff"
path = f"/v3/catalog/markets/{ID}/book"
query = ""
body = b""
ts = str(int(time.time() * 1000))
canon = "\n".join(["NOVIG-V3", ts, "GET", path, query,
hashlib.sha256(body).hexdigest()])
key = load_pem_private_key(open(PEM, "rb").read(), None)
headers = {
"Novig-Key-Id": KEY_ID,
"Novig-Timestamp": ts,
"Novig-Signature": base64.b64encode(
key.sign(canon.encode())).decode(),
}
url = f"{HOST}{path}"
req = urllib.request.Request(
url, data=body or None,
method="GET", headers=headers)
print(urllib.request.urlopen(req).read().decode())
import {
createHash, createPrivateKey, sign,
} from "node:crypto";
import { readFileSync } from "node:fs";
const HOST = "https://api.paper.novig.com";
const KEY_ID = "8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0";
const PEM = "desk-1.pem";
const ID = "6f9619ff-8b86-d011-b42d-00c04fc964ff";
const path = `/v3/catalog/markets/${ID}/book`;
const query = "";
const body = "";
const ts = Date.now().toString();
const hash =
createHash("sha256").update(body).digest("hex");
const canon = [
"NOVIG-V3", ts, "GET", path, query, hash,
].join("\n");
const key = createPrivateKey(readFileSync(PEM));
const headers: Record<string, string> = {
"Novig-Key-Id": KEY_ID,
"Novig-Timestamp": ts,
"Novig-Signature": sign(null, Buffer.from(canon), key)
.toString("base64"),
};
const url = `${HOST}${path}`;
const r = await fetch(url, {
method: "GET",
headers,
});
console.log(await r.text());
{
"marketId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"seq": 123,
"orders": {}
}{
"code": "BOOK_DEPTH_OUT_OF_RANGE",
"message": "`depth` must be between 1 and 20."
}{
"code": "SIGNATURE_REJECTED",
"message": "signature verification failed"
}{
"code": "SIGNATURE_REJECTED",
"message": "api key scope is insufficient for this route"
}{
"code": "MARKET_NOT_FOUND",
"message": "market not found"
}{
"code": "ACCOUNT_LOCKED",
"message": "the account is locked out of trading"
}{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Please wait before retrying."
}{
"code": "GEOLOCATION_EXPIRED",
"message": "no geolocation in the last 3 days"
}Get the order book
| Key | trading trading::read |
| Throttle | read |
| Cost | 1 /request |
| Answers | 200 304 400 401 403 404 423 429 451 |
| Idempotent | true |
Also served without a signature at GET /v3/public/catalog/markets/{id}/book. The edge throttles it per IP.
GET
/
v3
/
catalog
/
markets
/
{id}
/
book
cURL
HOST=https://api.paper.novig.com
KEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0
PEM=desk-1.pem
ID=6f9619ff-8b86-d011-b42d-00c04fc964ff
REQ_PATH="/v3/catalog/markets/$ID/book"
QUERY=""
BODY=''
TS=$(date +%s000)
HASH=$(printf %s "$BODY" \
| openssl dgst -sha256 -r | cut -d" " -f1)
# openssl signs a file, not a pipe. base64 -A never wraps.
printf 'NOVIG-V3\n%s\nGET\n%s\n%s\n%s' \
"$TS" "$REQ_PATH" "$QUERY" "$HASH" > canon.bin
SIG=$(openssl pkeyutl -sign -rawin -inkey "$PEM" \
-in canon.bin | openssl base64 -A)
curl -s -X GET "$HOST$REQ_PATH${QUERY:+?$QUERY}" \
-H "Novig-Key-Id: $KEY_ID" \
-H "Novig-Timestamp: $TS" \
-H "Novig-Signature: $SIG"use base64::prelude::*;
use ed25519_dalek::pkcs8::DecodePrivateKey;
use ed25519_dalek::{Signer, SigningKey};
use reqwest::blocking::{Client, Request};
use reqwest::{Method, Url};
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
const HOST: &str = "https://api.paper.novig.com";
const KEY_ID: &str =
"8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0";
const PEM: &str = "desk-1.pem";
const ID: &str = "6f9619ff-8b86-d011-b42d-00c04fc964ff";
fn main() -> anyhow::Result<()> {
let key = Key::load(KEY_ID, PEM)?;
let client = Client::new();
let path = format!("/v3/catalog/markets/{ID}/book");
let url = format!("{HOST}{path}");
let req = client
.get(url)
.build()?
.sign(&key)?;
println!("{}", client.execute(req)?.text()?);
Ok(())
}
/// An API key: the id the server looks up, and the
/// private half that signs.
struct Key {
id: &'static str,
signer: SigningKey,
}
impl Key {
fn load(id: &'static str, pem: &str) -> anyhow::Result<Self> {
let signer =
SigningKey::read_pkcs8_pem_file(pem)?;
Ok(Self { id, signer })
}
}
/// Signs a built request over the method, path, query and
/// body it will send, so the two can never disagree.
trait Sign: Sized {
fn sign(self, key: &Key) -> anyhow::Result<Self>;
}
impl Sign for Request {
fn sign(mut self, key: &Key) -> anyhow::Result<Self> {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)?
.as_millis()
.to_string();
let body = self.body().and_then(|b| b.as_bytes());
let body = body.unwrap_or_default();
let canon = Canonical::new(
&ts,
self.method(),
self.url(),
body,
);
let sig = key.signer.sign(canon.0.as_bytes());
let sig = BASE64_STANDARD.encode(sig.to_bytes());
let headers = self.headers_mut();
headers.insert("Novig-Key-Id", key.id.parse()?);
headers.insert("Novig-Timestamp", ts.parse()?);
headers.insert("Novig-Signature", sig.parse()?);
Ok(self)
}
}
/// The six NOVIG-V3 lines, joined by LF.
struct Canonical(String);
impl Canonical {
fn new(
ts: &str,
method: &Method,
url: &Url,
body: &[u8],
) -> Self {
let query =
Query::from(url.query().unwrap_or(""));
let hash = format!("{:x}", Sha256::digest(body));
let method = method.as_str();
let path = url.path();
let lines = [
"NOVIG-V3", ts, method, path, &query.0, &hash,
];
Self(lines.join("\n"))
}
}
/// Each part decoded and re-encoded, then sorted by
/// name and value.
struct Query(String);
impl From<&str> for Query {
fn from(raw: &str) -> Self {
let mut pairs = raw
.split('&')
.filter(|pair| !pair.is_empty())
.map(|pair| {
pair.split_once('=').unwrap_or((pair, ""))
})
.map(|(k, v)| {
(Self::encode(k), Self::encode(v))
})
.collect::<Vec<_>>();
pairs.sort();
let pairs =
pairs.iter().map(|(k, v)| format!("{k}={v}"));
Self(pairs.collect::<Vec<_>>().join("&"))
}
}
impl Query {
fn encode(part: &str) -> String {
Self::decode(part)
.iter()
.map(|&b| match b {
b'-' | b'.' | b'_' | b'~' => {
(b as char).to_string()
}
_ if b.is_ascii_alphanumeric() => {
(b as char).to_string()
}
_ => format!("%{b:02X}"),
})
.collect()
}
/// Only `%XX` decodes. A bare `+` stays a `+`.
fn decode(part: &str) -> Vec<u8> {
let raw = part.as_bytes();
let mut out = Vec::with_capacity(raw.len());
let mut i = 0;
while i < raw.len() {
let escape =
raw.get(i + 1..i + 3).and_then(Self::hex);
match (raw[i], escape) {
(b'%', Some(byte)) => {
out.push(byte);
i += 3;
}
(byte, _) => {
out.push(byte);
i += 1;
}
}
}
out
}
fn hex(pair: &[u8]) -> Option<u8> {
let hi = (pair[0] as char).to_digit(16)?;
let lo = (pair[1] as char).to_digit(16)?;
Some((hi * 16 + lo) as u8)
}
}
import base64, hashlib, time, urllib.request
from cryptography.hazmat.primitives.serialization import (
load_pem_private_key)
HOST = "https://api.paper.novig.com"
KEY_ID = "8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0"
PEM = "desk-1.pem"
ID = "6f9619ff-8b86-d011-b42d-00c04fc964ff"
path = f"/v3/catalog/markets/{ID}/book"
query = ""
body = b""
ts = str(int(time.time() * 1000))
canon = "\n".join(["NOVIG-V3", ts, "GET", path, query,
hashlib.sha256(body).hexdigest()])
key = load_pem_private_key(open(PEM, "rb").read(), None)
headers = {
"Novig-Key-Id": KEY_ID,
"Novig-Timestamp": ts,
"Novig-Signature": base64.b64encode(
key.sign(canon.encode())).decode(),
}
url = f"{HOST}{path}"
req = urllib.request.Request(
url, data=body or None,
method="GET", headers=headers)
print(urllib.request.urlopen(req).read().decode())
import {
createHash, createPrivateKey, sign,
} from "node:crypto";
import { readFileSync } from "node:fs";
const HOST = "https://api.paper.novig.com";
const KEY_ID = "8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0";
const PEM = "desk-1.pem";
const ID = "6f9619ff-8b86-d011-b42d-00c04fc964ff";
const path = `/v3/catalog/markets/${ID}/book`;
const query = "";
const body = "";
const ts = Date.now().toString();
const hash =
createHash("sha256").update(body).digest("hex");
const canon = [
"NOVIG-V3", ts, "GET", path, query, hash,
].join("\n");
const key = createPrivateKey(readFileSync(PEM));
const headers: Record<string, string> = {
"Novig-Key-Id": KEY_ID,
"Novig-Timestamp": ts,
"Novig-Signature": sign(null, Buffer.from(canon), key)
.toString("base64"),
};
const url = `${HOST}${path}`;
const r = await fetch(url, {
method: "GET",
headers,
});
console.log(await r.text());
{
"marketId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"seq": 123,
"orders": {}
}{
"code": "BOOK_DEPTH_OUT_OF_RANGE",
"message": "`depth` must be between 1 and 20."
}{
"code": "SIGNATURE_REJECTED",
"message": "signature verification failed"
}{
"code": "SIGNATURE_REJECTED",
"message": "api key scope is insufficient for this route"
}{
"code": "MARKET_NOT_FOUND",
"message": "market not found"
}{
"code": "ACCOUNT_LOCKED",
"message": "the account is locked out of trading"
}{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Please wait before retrying."
}{
"code": "GEOLOCATION_EXPIRED",
"message": "no geolocation in the last 3 days"
}Authorizations
The key's UUID.
Unix milliseconds. ±30 s.
Standard padded base64 of the NOVIG-V3 signature.
Headers
A prior response's ETag. A match answers 304.
Path Parameters
Market ID.
Query Parameters
Number of price levels per outcome, best price first. Every order at a kept level is included. Defaults to 20. 1 to 20. A value outside that range answers 400.
Required range:
1 <= x <= 20Response
Resting orders by outcome.
The order book of one market.
The book's sequence number at this snapshot. The next book delta on the websocket follows it.
Resting orders by outcome ID, best price first, earlier order first within a price.
Limited to depth price levels per outcome.
Show child attributes
Show child attributes

