cURL
HOST=https://api.paper.novig.com
KEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24
PEM=novig-api-key-mgmt-1.pem
REQ_PATH="/v3/account/transactions"
QUERY="kind=FILL"
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 =
"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24";
const PEM: &str = "novig-api-key-mgmt-1.pem";
fn main() -> anyhow::Result<()> {
let key = Key::load(KEY_ID, PEM)?;
let client = Client::new();
let path = "/v3/account/transactions";
let url = format!("{HOST}{path}?kind=FILL");
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 = "2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24"
PEM = "novig-api-key-mgmt-1.pem"
path = "/v3/account/transactions"
query = "kind=FILL"
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 = "2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24";
const PEM = "novig-api-key-mgmt-1.pem";
const path = "/v3/account/transactions";
const query = "kind=FILL";
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": [
{
"transactionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"kind": "FILL",
"amount": "<string>",
"ts": 123,
"ref": "<string>"
}
],
"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": "WALLET_NOT_FOUND",
"message": "wallet 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"
}List account transactions
| Key | management management::read |
| Throttle | history |
| Cost | 6 + 1 per 100 rows |
| Answers | 200 400 401 403 404 423 429 451 |
| Idempotent | true |
Every movement of your primary wallet’s balance. A fund of a subaccount reads TRANSFER_OUT, and a defund reads TRANSFER_IN.
GET
/
v3
/
account
/
transactions
cURL
HOST=https://api.paper.novig.com
KEY_ID=2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24
PEM=novig-api-key-mgmt-1.pem
REQ_PATH="/v3/account/transactions"
QUERY="kind=FILL"
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 =
"2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24";
const PEM: &str = "novig-api-key-mgmt-1.pem";
fn main() -> anyhow::Result<()> {
let key = Key::load(KEY_ID, PEM)?;
let client = Client::new();
let path = "/v3/account/transactions";
let url = format!("{HOST}{path}?kind=FILL");
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 = "2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24"
PEM = "novig-api-key-mgmt-1.pem"
path = "/v3/account/transactions"
query = "kind=FILL"
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 = "2c9a7e1d-5b3f-4e8a-a6d0-9f1b3c5e7a24";
const PEM = "novig-api-key-mgmt-1.pem";
const path = "/v3/account/transactions";
const query = "kind=FILL";
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": [
{
"transactionId": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
"kind": "FILL",
"amount": "<string>",
"ts": 123,
"ref": "<string>"
}
],
"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": "WALLET_NOT_FOUND",
"message": "wallet 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.
Query Parameters
One kind. What moved the balance.
Available options:
FILL, FEE, MAKER_CREDIT, SETTLEMENT, TRANSFER_IN, TRANSFER_OUT, DEPOSIT, WITHDRAWAL, CREDIT, CREDIT_RECLAIM, ADJUSTMENT, FORWARDING Exclusive lower bound on the event's scheduled start. Unix milliseconds.
Exclusive upper bound on the event's scheduled start. Unix milliseconds.
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.

