Skip to main content

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.

sh
curl -fsSL -o nk-partner.mjs https://developers.novikvadrat.rs/downloads/nk-partner
node nk-partner.mjs --help

Environment#

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#

js
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 from sodium_crypto_sign_seed_keypair($seed); hash('sha256', $body, true) for the digest.
  • Python: the cryptography package — Ed25519PrivateKey (load_pem_private_key or from_private_bytes) and .sign(base); hashlib.sha256 for the digest; base64.urlsafe_b64encode(...).rstrip(b'=') for the signature.
  • Java 15+: Signature.getInstance("Ed25519") with a PrivateKey from KeyFactory.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.HashData for 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