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/history/positions/$ID"
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/history/positions/{ID}");
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/history/positions/{ID}"
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/history/positions/${ID}`;
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());
{
"cost": "<string>",
"marketId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"outcomeId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"positionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"qty": 1,
"resettled": true,
"result": "<string>",
"status": "OPEN",
"payout": "<string>",
"pnl": "<string>",
"settledTs": 123
}{
"code": "SIGNATURE_REJECTED",
"message": "signature verification failed"
}{
"code": "SCOPE_INSUFFICIENT",
"message": "api key scope is insufficient for this route"
}{
"code": "POSITION_NOT_FOUND",
"message": "Position 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": "ANONYMIZED_NETWORK",
"message": "The request came over a VPN, a proxy, or a Tor exit."
}Get a position from history
| Key | trading trading::read |
| Throttle | history |
| Cost | 1 /request |
| Answers | 200 401 403 404 423 429 451 |
| Idempotent | true |
GET
/
v3
/
history
/
positions
/
{id}
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/history/positions/$ID"
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/history/positions/{ID}");
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/history/positions/{ID}"
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/history/positions/${ID}`;
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());
{
"cost": "<string>",
"marketId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"outcomeId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"positionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"qty": 1,
"resettled": true,
"result": "<string>",
"status": "OPEN",
"payout": "<string>",
"pnl": "<string>",
"settledTs": 123
}{
"code": "SIGNATURE_REJECTED",
"message": "signature verification failed"
}{
"code": "SCOPE_INSUFFICIENT",
"message": "api key scope is insufficient for this route"
}{
"code": "POSITION_NOT_FOUND",
"message": "Position 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": "ANONYMIZED_NETWORK",
"message": "The request came over a VPN, a proxy, or a Tor exit."
}Authorizations
The key's UUID.
Standard padded base64 of the NOVIG-V3 signature.
Unix milliseconds. ±30 s.
Path Parameters
Position ID.
Response
The position.
A position of the caller, with the status of its market and the grade of its outcome.
Net dollars the wallet paid for them. The average entry price is 100 × cost / qty.
Position ID.
Contracts held.
Required range:
x >= 0true if the market was graded again after it settled.
The grade of the outcome. It's TBD until the market settles.
Lifecycle status of a market.
Available options:
OPEN, CLOSED, SETTLED What the contracts are worth at result, in dollars. Absent while result is TBD.
payout minus cost, in dollars. Absent while result is TBD.
When the market settled. Absent until the market settles.

