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.
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 — 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 — 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))) 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.
// 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);
} 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.
// 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 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.
# 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",
) 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.