No.01API · Sandbox open

A consent rail with a docs page,
not a sales call.

REST + signed webhooks today. Typed SDKs for Node, Python and Swift in Q3 2026. Detached Ed25519 signatures verifiable offline. Sandbox keys issued in under a minute.

Base URL · https://api.inkanbio.com/v1 · OpenAPI 3.1

No.02Install

Two lines of shell. One curl to verify.

SDK packages are pre-release while the spec finalises. The REST API is stable on /v1 and ships with idempotency keys, exponential-backoff guidance, and rate-limit headers.

Node 22 / Workers
# Node — SDK preview, ships Q3 2026
npm install @inkanbio/node@next

# Or use the public REST API today
curl https://api.inkanbio.com/v1/health \
  -H "Authorization: Bearer sk_live_3K9v2x..."
Python 3.11+
# Python — SDK preview, ships Q3 2026
pip install --pre inkanbio

# Or use the public REST API today
import urllib.request, json
req = urllib.request.Request(
    "https://api.inkanbio.com/v1/health",
    headers={"Authorization": "Bearer sk_live_3K9v2x..."},
)
print(json.load(urllib.request.urlopen(req)))
No.03Authenticate

Bearer keys. HMAC-signed webhooks.

Two key tiers. Live keys are scoped per environment and rotatable without downtime. Webhooks are HMAC-SHA-256 signed; the signature covers a unix-millisecond timestamp concatenated with the payload — replay attacks fail outside a five-minute window.

Verifying a webhook
// Verify a webhook signature (Node 22 / Workers / Edge)
import { webcrypto } from "node:crypto";

const secret = process.env.INKAN_WEBHOOK_SECRET!;
const enc = new TextEncoder();

export async function verify(req: Request) {
  const sig = req.headers.get("inkan-signature") ?? "";
  const ts = req.headers.get("inkan-timestamp") ?? "";
  const body = await req.text();

  const key = await webcrypto.subtle.importKey(
    "raw", enc.encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false, ["verify"]
  );
  const ok = await webcrypto.subtle.verify(
    "HMAC", key,
    Uint8Array.from(atob(sig), (c) => c.charCodeAt(0)),
    enc.encode(`${ts}.${body}`)
  );
  if (!ok) throw new Error("invalid_signature");
  return JSON.parse(body);
}
No.04Capture consent

One call. One receipt. One ledger leaf.

POST /v1/consent. The response carries the record ID, an Ed25519 signature, and the Merkle leaf coordinates so any third party can prove inclusion without round-tripping back to us.

Node SDK preview
// Capture a consent receipt
import { Inkan } from "@inkanbio/node";

const ink = new Inkan({ apiKey: process.env.INKAN_API_KEY! });

const receipt = await ink.consent.create({
  subject: { id: "tlt_8f3a72b", display: "Lucia Vance" },
  biometric: {
    template_hash: "sha256:8c1e4...e2",
    liveness_score: 0.987,
    captured_at: new Date().toISOString(),
  },
  jurisdiction: ["UK", "EU", "ES"],
  scope: { category: ["editorial", "brand"], term_months: 24 },
  royalty: {
    currency: "GBP",
    per_event_minor: 4000,
    remit_to: "stripe_acct_1NkAn...",
  },
});

// receipt.id           → "rec_INK-0X4F.A12C.7B19"
// receipt.signature    → { alg, kid, value }
// receipt.verify_url   → public verification page
No.05Query royalties

Every event. Filterable. Statementable.

Cursor-paginated. Filter by record, licensee, jurisdiction, or date-range. Aggregate totals are precomputed at the epoch boundary so month-end reporting is one call, not a hundred.

Python SDK preview
# List royalty events for a record
import inkanbio

ink = inkanbio.Client(api_key=os.environ["INKAN_API_KEY"])

events = ink.royalty.list(
    record="rec_INK-0X4F.A12C.7B19",
    starting_after="2026-04-01",
    limit=100,
)

total_pence = sum(e.amount_minor for e in events.data)
print(f"YTD: £{total_pence / 100:,.2f} across {len(events.data)} events")

# Filter by jurisdiction or licensee
brand_only = ink.royalty.list(
    record="rec_INK-0X4F.A12C.7B19",
    licensee_type="brand",
)
Next

Sandbox access in under a minute.

Email Sam directly for sandbox credentials. Production access is gated behind a short technical call to confirm jurisdiction scope.