Skip to main content

How we check a signed request

We run these checks on each signed request, top to bottom. The first check that fails decides the error.
signing_api::verify(request)
body within capBodyTooLarge413
headers present?KeyIdRequired | TimestampRequired | SignatureRequired401
Uuid::parse(key_id)MalformedKeyId401
i64::parse(timestamp)MalformedTimestamp401
|now − ts| ≤ 30 sTimestampTooOld | TimestampTooFarInFuture401
key rowKeyNotFound401
the read itselfDatabaseError500
live && !expiredKeyRevoked | KeyExpired401
base64::decode(signature)MalformedSignature401
key.verify(canonical, sig)VerificationFailed401
scope.grants::<Route>()ScopeInsufficient403
Principalthe handler runs
Only a request that passes every check reaches the route’s handler. Each rejection is a SignatureRejection variant. The code in the response depends only on the status, so branch on the code, not on the variant.
  • VerificationFailed is the only rejection that means your canonical string is wrong.
  • A 403 means your signature verified. We check scope last.
  • A 500 isn’t caused by your request. Retry it unchanged.

400 Bad Request

To export a public key as SPKI, run openssl pkey -pubout and send the output with its newlines. Generate an Ed25519 or P-256 keypair, and set algorithm to match the key material. Address a subaccount by one of its trading key IDs. A subaccount is permanent, so relabel an existing one instead of opening another. A transfer id is unique per trader.

401 Unauthorized

When novig-timestamp or novig-signature is missing, the first message names that header instead. We accept a timestamp within ±30 s of our clock, so check your clock too. api key not found means a wrong key ID, the wrong environment, or a key revoked more than seconds ago. api key has been revoked means you revoked the key less than seconds ago. For signature verification failed, sign a test vector. Diff your canonical string against its string_to_sign. If you still get a 401, find your symptom below.
  • A 401 on every call. A header, the clock, the key, or the canonical string is wrong. Read message, then sign a published vector and diff your string against it on Sign a request.
  • GET works, but POST fails. Without Content-Type: application/json, the server hashed zero bytes. Send that header on every request with a body.
  • Every POST fails, and the body looks right. Your client re-serialized the JSON after you hashed it. Send the exact bytes you hashed.
  • Every path fails. You signed the URL, or dropped the mount prefix. Sign the path only, with the mount prefix included.
  • A P-256 signature never verifies. You sent a raw r‖s signature. Send DER instead, as Algorithms describes.
  • api key not found, but the key exists. You’re on the wrong environment, since a key works only in its own. Check the host on Environments.

403 Forbidden

A scope error means the key has the wrong family or level. See scopes. KYC is our identity check. You need it to create a key, to open or fund a subaccount, or to place an order. Cancels still work without it. A trading or trading::read key that names a sibling subaccount gets a 404 SUBACCOUNT_NOT_FOUND. To read a sibling, use a key with management::read.

404 Not Found

We answer Key not found., Subaccount not found., or Transfer not found. when we can’t find the ID. An ID that belongs to another trader gets the same reply.

409 Conflict

  • A live key with this public key already exists. A public key is unique across Novig.
  • This trader already has a live management key… Revoke the live management key first.
A declined transfer is not an error. The route answers 202, and the transfer then reads status: "Rejected". See Funding.

413 Content Too Large

The edge is a filter in front of our servers. It refuses any body over 8 KiB (8,192 bytes) with a 403 and an HTML page, with no JSON error body. The request never reaches the exchange. A batch hits this cap first, at about 90 orders. The exchange buffers each request body before it parses it. It refuses a body over 100 KiB with a 413 and PAYLOAD_TOO_LARGE, before it reads the signature. Through our edge you’ll see the 403 first, since its cap is smaller. To fix either error, split the batch. Both caps count the raw bytes you send.

423 Locked

A 423 doesn’t clear on its own. Contact support.
  • SYSTEM_LOCKED means the exchange has halted trading. It blocks every trading and catalog route, reads included.
  • ACCOUNT_LOCKED and ACCOUNT_EXCLUDED block every trading and catalog route, reads included.
  • ACCOUNT_LOCKED also blocks creating a key, opening a subaccount, and moving funds, while the account is locked or excluded.
  • SELF_EXCLUDED blocks placing orders, creating a key, opening a subaccount, and moving funds while the key holder is self-excluded.
  • EMPLOYEE_ACCOUNT blocks placing and canceling when the key belongs to a Novig employee.

429 Too Many Requests

RATE_LIMIT_EXCEEDED means you sent requests too fast. Wait Retry-After seconds before you retry. See Throttling.

451 Geolocation Unavailable

Geolocation is the location check the Novig app runs for the key holder. A 451 means the request, or the key holder’s location, didn’t pass it. ANONYMIZED_NETWORK means the request came over a VPN, a proxy, or a Tor exit. A data-center address is fine. Creating a key, opening a subaccount, and issuing a subaccount key also screen the request’s network when you sign in with a session, so each can answer ANONYMIZED_NETWORK or RESTRICTED_NETWORK_REGION. The first two codes judge the request’s network. The rest judge the key holder’s device. For those, the key holder must open the app. A cancel never returns a 451. A read admits a stale geolocation, so only a placement returns GEOLOCATION_EXPIRED.

Trading and catalog errors

Every trading and catalog error has a code and a message. Branch on the code. The message is for a human, and it can change. A body that doesn’t parse answers INVALID_BODY. Its message names the field and the reason, like qty: invalid value: integer `-1`, expected u32.
  • 403 KYC_REQUIRED applies to placing only. The key holder’s KYC lapsed after the key was created, so finish verification in the app.
  • 423 means SELF_EXCLUDED, EMPLOYEE_ACCOUNT, or a lock with no code. See 423.
  • A reject from the matching engine isn’t an HTTP error. It arrives on the private stream after the 201.