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 capBodyTooLarge413headers present?KeyIdRequired | TimestampRequired | SignatureRequired401Uuid::parse(key_id)MalformedKeyId401i64::parse(timestamp)MalformedTimestamp401|now − ts| ≤ 30 sTimestampTooOld | TimestampTooFarInFuture401key rowKeyNotFound401the read itselfDatabaseError500live && !expiredKeyRevoked | KeyExpired401base64::decode(signature)MalformedSignature401key.verify(canonical, sig)VerificationFailed401scope.grants::<Route>()ScopeInsufficient403Principalthe handler runsSignatureRejection variant.
The code in the response depends only on the status, so branch on the code, not on the variant.
VerificationFailedis the only rejection that means your canonical string is wrong.- A
403means your signature verified. We check scope last. - A
500isn’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. GETworks, butPOSTfails. WithoutContent-Type: application/json, the server hashed zero bytes. Send that header on every request with a body.- Every
POSTfails, 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‖ssignature. 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 livemanagementkey first.
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_LOCKEDmeans the exchange has halted trading. It blocks every trading and catalog route, reads included.ACCOUNT_LOCKEDandACCOUNT_EXCLUDEDblock every trading and catalog route, reads included.ACCOUNT_LOCKEDalso blocks creating a key, opening a subaccount, and moving funds, while the account is locked or excluded.SELF_EXCLUDEDblocks placing orders, creating a key, opening a subaccount, and moving funds while the key holder is self-excluded.EMPLOYEE_ACCOUNTblocks 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 acode 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_REQUIREDapplies 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 nocode. See423. - A
rejectfrom the matching engine isn’t an HTTP error. It arrives on the private stream after the 201.

