> ## Documentation Index
> Fetch the complete documentation index at: https://docs.novig.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Place an order

> | | |
| --- | --- |
| **Key** | `trading` |
| **Throttle** | `place` |
| **Cost** | `1 /request` |
| **Answers** | <span class="st st-ok">201</span> <span class="st st-warn">400</span> <span class="st st-warn">401</span> <span class="st st-warn">403</span> <span class="st st-warn">404</span> <span class="st st-warn">413</span> <span class="st st-warn">422</span> <span class="st st-hold">423</span> <span class="st st-hold">429</span> <span class="st st-warn">451</span> |
| **Idempotent** | `false` |

`201` means queued, not resting. The `open` event on the private stream confirms the order rests.



## OpenAPI

````yaml /api-reference/spec-files/openapi-v3-target.json post /v3/orders
openapi: 3.1.0
info:
  title: Novig API
  version: 3.0.0
  description: >-
    Place orders over signed REST. Watch the book and your fills on one
    websocket.
servers:
  - url: https://api.paper.novig.com
    description: Paper
security:
  - keyId: []
    timestamp: []
    signature: []
tags:
  - name: Catalog
  - name: History
  - name: Public
  - name: Authentication
  - name: Accounts
  - name: Execution
  - name: Streaming
  - name: Throttle
paths:
  /v3/orders:
    post:
      tags:
        - Execution
      summary: Place an order
      description: >-
        | | |

        | --- | --- |

        | **Key** | `trading` |

        | **Throttle** | `place` |

        | **Cost** | `1 /request` |

        | **Answers** | <span class="st st-ok">201</span> <span class="st
        st-warn">400</span> <span class="st st-warn">401</span> <span class="st
        st-warn">403</span> <span class="st st-warn">404</span> <span class="st
        st-warn">413</span> <span class="st st-warn">422</span> <span class="st
        st-hold">423</span> <span class="st st-hold">429</span> <span class="st
        st-warn">451</span> |

        | **Idempotent** | `false` |


        `201` means queued, not resting. The `open` event on the private stream
        confirms the order rests.
      operationId: placeOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlaceOrder'
            example:
              outcomeId: 3f2504e0-4f89-11d3-9a0c-0305e82c3301
              price: '0.665'
              qty: 110
              tif: GTC
              clientId: 0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90
      responses:
        '201':
          description: Accepted and queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderAccepted'
              example:
                orderId: 7c9e6679-7425-40de-944b-e07fc1f90ae7
                clientId: 0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90
        '400':
          description: The request is malformed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                code: INVALID_REQUEST
                message: the request is malformed
        '401':
          description: >-
            The signature is absent or invalid, or the key is unknown, revoked,
            or expired.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                code: SIGNATURE_REJECTED
                message: signature verification failed
        '403':
          description: >-
            The key's scope does not grant this route, or the trader has not
            passed KYC. The code says which.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                code: KYC_REQUIRED
                message: KYC verification required
        '404':
          description: No outcome carries that ID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                code: OUTCOME_NOT_FOUND
                message: outcome not found
        '413':
          description: >-
            The body is too large. The edge refuses most oversized bodies before
            the origin sees them. The edge answers a plain `403` with no error
            body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                code: PAYLOAD_TOO_LARGE
                message: request entity too large
        '422':
          description: >-
            The bound wallet does not cover the order, or a position cap refused
            it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                code: INSUFFICIENT_BALANCE
                message: Insufficient balance
        '423':
          description: >-
            The account is locked out of trading, or the key holder is
            self-excluded. The code says which.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                code: SELF_EXCLUDED
                message: You have been self-excluded from trading.
        '429':
          description: >-
            The throttle is empty. Wait the number of seconds in `Retry-After`.
            Then retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                code: RATE_LIMIT_EXCEEDED
                message: Rate limit exceeded. Please wait before retrying.
        '451':
          description: >-
            Geolocation refused the request: an anonymized network, a restricted
            region, or, for a placement, no device geolocation in the last 3
            days.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorBody'
              example:
                code: GEOLOCATION_EXPIRED
                message: no geolocation in the last 3 days
      x-codeSamples:
        - lang: bash
          source: |
            HOST=https://api.paper.novig.com
            KEY_ID=8f14e45f-ceea-467a-9b1c-3f2a51c8d7e0
            PEM=desk-1.pem

            REQ_PATH="/v3/orders"
            QUERY=""
            BODY='{
              "outcomeId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
              "price": "0.665",
              "qty": 110,
              "tif": "GTC",
              "clientId": "0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90"
            }'
            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\nPOST\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 POST "$HOST$REQ_PATH${QUERY:+?$QUERY}" \
              -H "Content-Type: application/json" \
              -H "Novig-Key-Id: $KEY_ID" \
              -H "Novig-Timestamp: $TS" \
              -H "Novig-Signature: $SIG" \
              --data-binary "$BODY"
        - lang: rust
          source: |
            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 BODY: &[u8] = br#"{
              "outcomeId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
              "price": "0.665",
              "qty": 110,
              "tif": "GTC",
              "clientId": "0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90"
            }"#;

            fn main() -> anyhow::Result<()> {
                let key = Key::load(KEY_ID, PEM)?;
                let client = Client::new();
                let path = "/v3/orders";
                let url = format!("{HOST}{path}");
                let req = client
                    .post(url)
                    .header("Content-Type", "application/json")
                    .body(BODY)
                    .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)
                }
            }
        - lang: python
          source: |
            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 = ""
            body = b"""{
              "outcomeId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
              "price": "0.665",
              "qty": 110,
              "tif": "GTC",
              "clientId": "0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90"
            }"""

            ts = str(int(time.time() * 1000))
            canon = "\n".join(["NOVIG-V3", ts, "POST", 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(),
                "Content-Type": "application/json",
            }
            url = f"{HOST}{path}"
            req = urllib.request.Request(
                url, data=body or None,
                method="POST", headers=headers)
            print(urllib.request.urlopen(req).read().decode())
        - lang: typescript
          source: |
            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 = "";
            const body = `{
              "outcomeId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
              "price": "0.665",
              "qty": 110,
              "tif": "GTC",
              "clientId": "0b3f5f20-9c1b-4d6e-8f3a-5c2d1e7b4a90"
            }`;

            const ts = Date.now().toString();
            const hash =
              createHash("sha256").update(body).digest("hex");
            const canon = [
              "NOVIG-V3", ts, "POST", 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"),
              "Content-Type": "application/json",
            };
            const url = `${HOST}${path}`;
            const r = await fetch(url, {
              method: "POST",
              headers,
              body,
            });
            console.log(await r.text());
components:
  schemas:
    PlaceOrder:
      type: object
      properties:
        outcomeId:
          type: string
          format: uuid
          description: The side you are buying. Never a market ID.
        price:
          type: string
          description: >-
            Decimal probability on the submittable grid, as a string. Three
            bands: `0.001`–`0.050` in steps of `0.001`, `0.055`–`0.945` in steps
            of `0.005`, and `0.950`–`0.999` in steps of `0.001`. The exchange
            refuses a price off the grid with `INVALID_PRICE`.
          examples:
            - '0.665'
        qty:
          type: integer
          format: int32
          minimum: 1
          description: Number of contracts. A winning contract pays full value, 1¢.
          examples:
            - 110
        tif:
          $ref: '#/components/schemas/TimeInForce'
        ttl:
          type: integer
          format: int64
          description: >-
            Milliseconds. Required for `GTT`, optional for `PO`, forbidden
            otherwise.
        clientId:
          type: string
          format: uuid
          description: >-
            Your label for the order. Echoed on the answer, on `open`, and on
            every `fill`. The exchange never checks it for uniqueness, so a
            replay places a second order.
      required:
        - outcomeId
        - price
        - qty
        - tif
    OrderAccepted:
      type: object
      properties:
        orderId:
          type: string
          format: uuid
        clientId:
          type: string
          format: uuid
          description: Echoed when sent.
      required:
        - orderId
      description: >-
        Accepted and queued, not resting. The order rests when the `open` event
        arrives on the private stream.
    ErrorBody:
      type: object
      properties:
        code:
          type: string
          description: Stable identifier. The only field to branch on.
        message:
          type: string
          description: Human-readable. Not stable. Never match on it.
        nonce:
          type:
            - integer
            - 'null'
          format: int64
          description: The websocket request that failed. Absent on REST.
        rejected:
          type: array
          items:
            $ref: '#/components/schemas/BatchRejection'
          description: >-
            One entry per refused order in a batch. Present only on an error
            that a batch caused.
      required:
        - code
        - message
      description: Every error on the surface carries this body.
    TimeInForce:
      type: string
      enum:
        - GTC
        - GTT
        - IOC
        - FOK
        - PO
      description: '`ttl` is required for `GTT`, optional for `PO`, forbidden otherwise.'
    BatchRejection:
      type: object
      properties:
        index:
          type: integer
          description: Zero-based position in the request.
        outcomeId:
          type: string
          format: uuid
        reason:
          type: string
          description: >-
            Human-readable. Not stable. Branch on the envelope's `code` and on
            `index`.
      required:
        - index
        - outcomeId
        - reason
      description: One order a batch refused.
  securitySchemes:
    keyId:
      type: apiKey
      in: header
      name: Novig-Key-Id
      description: The key's UUID.
    timestamp:
      type: apiKey
      in: header
      name: Novig-Timestamp
      description: Unix milliseconds. ±30 s.
    signature:
      type: apiKey
      in: header
      name: Novig-Signature
      description: Standard padded base64 of the NOVIG-V3 signature.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.