Node
The reference client nk-partner.mjs — one zero-dependency file for Node 20 or newer, a signRequest function and a CLI for every endpoint. Other languages in prose.
nk-partner.mjs is a single file: Node ≥ 20, zero dependencies (only node: modules). Download nk-partner.mjs and save it into your project as nk-partner.mjs, then import signRequest or run it as a command-line tool. It is also the tool we use to test the API end to end.
curl -fsSL -o nk-partner.mjs https://developers.novikvadrat.rs/downloads/nk-partner
node nk-partner.mjs --helpEnvironment#
| Variable | Meaning |
|---|---|
NK_PARTNER_KEY_ID |
your key id, e.g. nkp_sandbox_7f3k9q2m4x8c1v6b |
NK_PARTNER_PRIVATE_KEY_PEM |
the Ed25519 private key, PKCS#8 PEM text |
NK_PARTNER_PRIVATE_KEY_FILE |
the path of that PEM file instead; read when NK_PARTNER_PRIVATE_KEY_PEM is unset |
NK_PARTNER_BASE_URL |
the API base; default https://novikvadrat.rs/api/partners/v1 |
Keep the private key in your secret store; never commit it.
signRequest#
import { signRequest } from './nk-partner.mjs'
const url = 'https://novikvadrat.rs/api/partners/v1/connections/9b1d6c2e-4f3a-4c8e-9a71-2d5e8f0b3c44/listings/REAA-12345'
const body = JSON.stringify(listing)
const headers = signRequest({
keyId: process.env.NK_PARTNER_KEY_ID,
privateKeyPem: process.env.NK_PARTNER_PRIVATE_KEY_PEM,
method: 'PUT',
url,
body,
})
const res = await fetch(url, { method: 'PUT', headers: { ...headers, 'Content-Type': 'application/json' }, body })signRequest({ keyId, privateKeyPem, method, url, body }) returns the five headers (NK-Key-Id, NK-Timestamp, NK-Nonce, Content-Digest, NK-Signature) with a fresh nonce and the current time. It signs the target of new URL(url).href — the path exactly as fetch will send it and the canonical query — so send the request to that same url, and send exactly the body string you signed. Leave body out for GET and DELETE.
The command line#
Every API command signs its request with the key from the environment, prints the response body (JSON) on stdout and one line on stderr with the HTTP status and NK-Request-Id. Exit code 0 on a 2xx, 1 otherwise, 2 on a usage or environment error.
| Command | What it does |
|---|---|
node nk-partner.mjs keygen --out=<prefix> |
writes a new Ed25519 key pair (<prefix>.pem, <prefix>.pub) and prints the public key to register (local, no network) |
node nk-partner.mjs ping |
GET /ping: proves your signature works and shows your app, key and limits |
node nk-partner.mjs redeem <code> |
POST /connections: redeems an agency's connect code |
node nk-partner.mjs connections |
GET /connections: lists your connections (--status=, --cursor=, --limit=) |
node nk-partner.mjs connection <id> |
GET /connections/{id}: one connection |
node nk-partner.mjs disconnect <id> --reason=<text> |
DELETE /connections/{id}?reason=…: ends the connection; its listings are released to the agency |
node nk-partner.mjs put-listing <connection> --file=<json> |
PUT …/listings/{external_id}: creates or replaces one listing from a JSON file |
node nk-partner.mjs batch <connection> --files=<a.json,b.json> |
POST …/batches: pushes several listings in one request (--idempotency-key=) |
node nk-partner.mjs batches <batch_id> |
GET /batches/{id}: a batch's status and each item's push-time result (final outcomes: list, webhooks) |
node nk-partner.mjs remove <connection> <external_id> --reason=<sold|rented|withdrawn|expired|error> |
POST …/listings/{external_id}/remove |
node nk-partner.mjs list <connection> |
GET …/listings: the reconcile view (--status=, --cursor=, --limit=) |
node nk-partner.mjs reconcile <connection> --ids=<a,b,…> |
POST …/reconcile (--confirm-mass-removal, --idempotency-key=) |
node nk-partner.mjs validate --file=<json> |
POST /validate: a dry run, stores nothing (--connection= adds the agency's checks) |
node nk-partner.mjs vocab |
GET /vocabularies |
node nk-partner.mjs places --q=<text> |
GET /places (--type=city|municipality) |
node nk-partner.mjs place <id> |
GET /places/{id} |
node nk-partner.mjs echo --port=<n> [--public-key=<pem>] |
a local server that prints the signed requests it receives, and verifies them when given your public key (without --public-key= every result is not_checked) — point --base= at it to see your own base and headers (local, no private key needed) |
Flags on every API command: --base=<url> (instead of NK_PARTNER_BASE_URL), --dry-run (print the request and its signature base without sending), --json, and --expect=<status>[:<code>] (exit 0 when the answer matches). Test flags for the certification checklist: --skew=<seconds> shifts the timestamp, --replay sends the same signed request twice, --tamper=body changes the body after signing.
Other languages#
We publish Node only, until a partner contributes a verified sample. Any language with Ed25519 and SHA-256 can sign NK-SIG-1; the steps are on NK-SIG-1 signing and the expected bytes on Worked example and test vector.
- PHP: the sodium extension —
sodium_crypto_sign_detached($base, $secretKey)with a 64-byte secret key fromsodium_crypto_sign_seed_keypair($seed);hash('sha256', $body, true)for the digest. - Python: the
cryptographypackage —Ed25519PrivateKey(load_pem_private_keyorfrom_private_bytes) and.sign(base);hashlib.sha256for the digest;base64.urlsafe_b64encode(...).rstrip(b'=')for the signature. - Java 15+:
Signature.getInstance("Ed25519")with aPrivateKeyfromKeyFactory.getInstance("Ed25519")and a PKCS#8 key spec;MessageDigest.getInstance("SHA-256");Base64.getUrlEncoder().withoutPadding(). - .NET: the runtime has no built-in Ed25519 signer; use a maintained Ed25519 library (for example the NSec or BouncyCastle packages) and
SHA256.HashDatafor the digest.
Whatever you use, sign the exact UTF-8 bytes of the base (not a hash of it), and check your output against the test vector before calling the sandbox. Send a working sample to partners@novikvadrat.rs.
Last updated