cURL
HOST=https://api.paper.novig.com
KEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0
PEM=desk-1.pem
REQ_PATH="/v3/orders"
QUERY="status=OPEN"
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";
fn main() -> anyhow::Result<()> {
let key = Key::load(KEY_ID, PEM)?;
let client = Client::new();
let path = "/v3/orders";
let url = format!("{HOST}{path}?status=OPEN");
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"
path = "/v3/orders"
query = "status=OPEN"
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}?{query}"
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 path = "/v3/orders";
const query = "status=OPEN";
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}?${query}`;
const r = await fetch(url, {
method: "GET",
headers,
});
console.log(await r.text());
{
"items": [
{
"orderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"marketId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"outcomeId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"price": "0.665",
"qty": 110,
"remaining": 123,
"tif": "GTC",
"status": "PENDING",
"createdTs": 123,
"clientId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"expiresTs": 123
}
],
"next": "<string>"
}{
"code": "INVALID_REQUEST",
"message": "the request is malformed"
}{
"code": "SIGNATURE_REJECTED",
"message": "signature verification failed"
}{
"code": "SIGNATURE_REJECTED",
"message": "api key scope is insufficient for this route"
}{
"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"
}List orders
| Key | trading trading::read |
| Throttle | read or history |
| Cost | 1 /request open, 4 + 1 per 100 rows settled |
| Answers | 200 400 401 403 423 429 451 |
| Idempotent | true |
GET
/
v3
/
orders
cURL
HOST=https://api.paper.novig.com
KEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0
PEM=desk-1.pem
REQ_PATH="/v3/orders"
QUERY="status=OPEN"
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";
fn main() -> anyhow::Result<()> {
let key = Key::load(KEY_ID, PEM)?;
let client = Client::new();
let path = "/v3/orders";
let url = format!("{HOST}{path}?status=OPEN");
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"
path = "/v3/orders"
query = "status=OPEN"
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}?{query}"
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 path = "/v3/orders";
const query = "status=OPEN";
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}?${query}`;
const r = await fetch(url, {
method: "GET",
headers,
});
console.log(await r.text());
{
"items": [
{
"orderId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"marketId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"outcomeId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"price": "0.665",
"qty": 110,
"remaining": 123,
"tif": "GTC",
"status": "PENDING",
"createdTs": 123,
"clientId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"expiresTs": 123
}
],
"next": "<string>"
}{
"code": "INVALID_REQUEST",
"message": "the request is malformed"
}{
"code": "SIGNATURE_REJECTED",
"message": "signature verification failed"
}{
"code": "SIGNATURE_REJECTED",
"message": "api key scope is insufficient for this route"
}{
"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.
Query Parameters
Every market of one event.
One market. With event, the market must belong to that event.
One outcome. With market, the outcome must belong to that market.
Filter by status. Default OPEN.
A partly filled order stays OPEN. Track remaining, not the status.
Available options:
PENDING, OPEN, FILLED, CANCELED, REJECTED Page size. Defaults to 500, not the maximum. 1 to 5000. A value outside that range answers 400.
Required range:
1 <= x <= 5000The next cursor from the previous page. Opaque.

