Seal your first payload in four calls
Register a key-pair, attest a peer, seal an envelope, collect the receipt. Everything else in this reference is refinement on those four movements. Base URL https://kryptapi.com/v1 · API version 2026-05-01.
Four calls, three languages
Sealing happens in your process, not ours. The SDK derives the hop key locally and posts ciphertext; the raw HTTP below shows exactly what crosses the wire if you would rather implement it yourself.
# 1 · register a key-pair (public material only — the private key never leaves you)
curl -X POST https://kryptapi.com/v1/keys \
-H "Authorization: Bearer sk_live_2f8a…" \
-H "X-Krypt-Api-Version: 2026-05-01" \
-H "Content-Type: application/json" \
-d '{
"label": "ops-dashboard",
"curve": "P-256",
"public_jwk": { "kty":"EC","crv":"P-256","x":"8f2a…","y":"d901…" },
"rotation_days": 90
}'
# 2 · attest the peer you are about to send to
curl -X POST https://kryptapi.com/v1/attest \
-H "Authorization: Bearer sk_live_2f8a…" \
-d '{ "route": "rt_2d55", "peer": "ops-dashboard", "challenge": "9f42a1c7b3d40e11" }'
# 3 · seal and relay — "ct" is ciphertext you produced locally
curl -X POST https://kryptapi.com/v1/envelopes \
-H "Authorization: Bearer sk_live_2f8a…" \
-H "Idempotency-Key: 7c4a1f90-2b33-4e55-9a11-0d8e6b2f5c31" \
-d '{
"route": "rt_2d55",
"hdr": { "hop":1, "kid":"key_form_1d90", "alg":"ECDH-ES+A256GCM", "iv":"9pQm1xTf0Ad2" },
"epk": { "kty":"EC","crv":"P-256","x":"b2c8…","y":"7d19…" },
"aad": "hop=1;route=rt_2d55;ts=1786553527",
"ct": "k9RtQ2b…"
}'
# -> 201 Created
# { "env":"env_9f42a1c7b3d4c1a7", "state":"relayed", "bytes":41208,
# "commit":"0x8c31…f19d", "attested":true }
# 4 · collect the receipt (proof of open, hash-chained)
curl https://kryptapi.com/v1/envelopes/env_9f42a1c7b3d4c1a7/receipt \
-H "Authorization: Bearer sk_live_2f8a…"
import { Krypt } from "@kryptapi/node";
const krypt = new Krypt({
apiKey: process.env.KRYPT_API_KEY,
apiVersion: "2026-05-01",
// the private JWK stays on this machine; the SDK never transmits it
identity: JSON.parse(process.env.KRYPT_IDENTITY_JWK)
});
// 1 · register the destination's key-pair once, at provisioning time
const key = await krypt.keys.create({
label: "ops-dashboard",
curve: "P-256",
rotationDays: 90
});
// 2 · attest the peer — throws AttestationError if the peer cannot prove itself
await krypt.attest({ route: "rt_2d55", peer: "ops-dashboard" });
// 3 · seal locally, relay the ciphertext
const envelope = await krypt.envelopes.seal({
route: "rt_2d55",
to: key.fingerprint, // "8f2a…d901"
content: Buffer.from(JSON.stringify(intake)),
aad: { retain: "7y", classification: "pii" },
idempotencyKey: intake.id
});
console.log(envelope.env); // env_9f42a1c7b3d4c1a7
console.log(envelope.commit); // 0x8c31…f19d
// 4 · receipt of open, whenever the destination collects it
const receipt = await krypt.envelopes.receipt(envelope.env);
if (receipt.state === "opened") {
console.log("opened at", receipt.opened_at, "by", receipt.opened_by_fp);
}
import os, json
from kryptapi import Krypt, AttestationError
krypt = Krypt(
api_key=os.environ["KRYPT_API_KEY"],
api_version="2026-05-01",
identity=json.loads(os.environ["KRYPT_IDENTITY_JWK"]), # stays local
)
# 1 · register the destination key-pair
key = krypt.keys.create(
label="ops-dashboard",
curve="P-256",
rotation_days=90,
)
# 2 · attest before you send. No attestation, no envelope.
try:
krypt.attest(route="rt_2d55", peer="ops-dashboard")
except AttestationError as e:
# amber, never crimson: this is an operational state, not a catastrophe
log.warning("peer refused attestation: %s", e.code)
raise
# 3 · seal locally and relay
envelope = krypt.envelopes.seal(
route="rt_2d55",
to=key.fingerprint,
content=json.dumps(intake).encode(),
aad={"retain": "7y", "classification": "pii"},
idempotency_key=intake["id"],
)
print(envelope.env, envelope.commit) # env_9f42a1c7b3d4c1a7 0x8c31…f19d
# 4 · receipt
receipt = krypt.envelopes.receipt(envelope.env)
print(receipt.state, receipt.opened_at)
Where the sealing happens
Every SDK performs the ECDH agreement and AES-GCM sealing in your process before the HTTP call is made. If a KryptAPI SDK ever asked for your private key material, that would be the bug report of the year — and it is why every SDK is source-available and reproducibly built.
The same primitives, running in your browser right now
Two real ECDH P-256 key-pairs, one derived AES-256-GCM key, one SHA-256 commit. No network calls — open your devtools and check.
this is everything we can see
{}Core routes
Expand any route for its request and response bodies. All requests require Authorization: Bearer and X-Krypt-Api-Version; all mutating requests accept Idempotency-Key.
REQUEST
{
"route": "rt_2d55",
"hdr": { "hop": 1, "kid": "key_form_1d90",
"alg": "ECDH-ES+A256GCM", "iv": "9pQm1xTf0Ad2" },
"epk": { "kty": "EC", "crv": "P-256", "x": "b2c8…", "y": "7d19…" },
"aad": "hop=1;route=rt_2d55;ts=1786553527;retain=7y",
"ct": "k9RtQ2b…"
}
RESPONSE · 201
{
"env": "env_9f42a1c7b3d4c1a7",
"state": "relayed",
"bytes": 41208,
"attested": true,
"commit": "0x8c31…f19d",
"parent": "0x2ab7…0c41",
"expires_at":"2026-08-15T14:32:07Z"
}RESPONSE · 200
{
"env": "env_9f42a1c7b3d4c1a7",
"route": "rt_2d55",
"hop": 1,
"state": "relayed",
"bytes": 41208,
"origin_fp": "8f2a…d901",
"dest_fp": "5f1c…a740",
"sealed_at": "2026-08-12T14:32:07Z",
"opened_at": null,
"ct": null
}
Note: "ct" is always null on this route. Ciphertext is
collected exactly once, by the destination, on /open.REQUEST
{ "attestation": "att_5f1ca740b2c80f55" }
RESPONSE · 200
{
"env": "env_9f42a1c7b3d4c1a7",
"hdr": { "hop": 1, "kid": "key_form_1d90",
"alg": "ECDH-ES+A256GCM", "iv": "9pQm1xTf0Ad2" },
"epk": { "kty": "EC", "crv": "P-256", "x": "b2c8…", "y": "7d19…" },
"aad": "hop=1;route=rt_2d55;ts=1786553527;retain=7y",
"ct": "k9RtQ2b…",
"state":"opened"
}
Decryption happens in your process. A second call returns
409 envelope_already_opened — collection is single-shot.RESPONSE · 200
{
"env": "env_9f42a1c7b3d4c1a7",
"state": "opened",
"sealed_at": "2026-08-12T14:32:07Z",
"opened_at": "2026-08-12T14:32:09Z",
"opened_by_fp": "5f1c…a740",
"ct_sha256": "9ab4c0…7e02",
"commit": "0x1e77…d5b8",
"parent": "0x8c31…f19d",
"attestations": [
{ "peer": "web-form.edge", "sig": "d0f2…771b", "ok": true },
{ "peer": "ops-dashboard.client", "sig": "7c02…e441", "ok": true }
]
}REQUEST
{
"label": "ops-dashboard",
"curve": "P-256",
"public_jwk": { "kty":"EC","crv":"P-256","x":"8f2a…","y":"d901…" },
"rotation_days": 90,
"custody": "hsm" // "local" | "hsm" | "escrow"
}
RESPONSE · 201
{
"kid": "key_ops_8f2a",
"fingerprint": "8f2a…d901",
"curve": "P-256",
"custody": "hsm",
"created_at": "2026-08-12T09:04:11Z",
"rotates_at": "2026-11-10T09:04:11Z",
"state": "active"
}REQUEST
{
"new_public_jwk": { "kty":"EC","crv":"P-256","x":"5f1c…","y":"a740…" },
"quorum": ["custodian_a","custodian_c"],
"grace_hours": 72
}
RESPONSE · 200
{
"kid": "key_ops_8f2a",
"rotation": 47,
"quorum": "2-of-3",
"retiring_fp": "8f2a…d901",
"incoming_fp": "5f1c…a740",
"grace_until": "2026-08-15T09:04:11Z",
"commit": "0x3fa9…aa14",
"metered_as": "attestation_event"
}GET /v1/audit?from=2026-08-01&to=2026-08-12&cursor=…
RESPONSE · 200
{
"entries": [
{ "ts":"2026-08-12T14:32:07Z", "env":"env_9f42…c1a7",
"event":"envelope.sealed", "commit":"0x8c31…f19d",
"parent":"0x2ab7…0c41" },
{ "ts":"2026-08-12T14:32:09Z", "env":"env_9f42…c1a7",
"event":"envelope.opened", "commit":"0x1e77…d5b8",
"parent":"0x8c31…f19d" }
],
"next_cursor": "cur_9c07f21a",
"chain_head": "0x1e77…d5b8",
"verifier": "krypt-audit-verify --head 0x1e77…d5b8"
}REQUEST
{
"route": "rt_2d55",
"peer": "ops-dashboard",
"challenge": "9f42a1c7b3d40e11"
}
RESPONSE · 200
{
"att": "att_5f1ca740b2c80f55",
"peer": "ops-dashboard.client",
"peer_fp": "5f1c…a740",
"signature": "7c02…e441",
"counter_challenge":"41ba0d77e5c29a30",
"suite": "Ed25519 / P-256 ECDH / AES-256-GCM",
"expires_in": 300,
"ok": true
}Four events worth waking someone for
Deliveries are signed with your webhook key and retried with exponential backoff for 24 hours. Payloads carry metadata only — an event never contains content.
{
"event": "envelope.sealed",
"env": "env_9f42a1c7b3d4c1a7",
"route": "rt_2d55",
"hop": 1,
"bytes": 41208,
"origin_fp": "8f2a…d901",
"commit": "0x8c31…f19d",
"ts": "2026-08-12T14:32:07Z"
}
{
"event": "envelope.opened",
"env": "env_9f42a1c7b3d4c1a7",
"opened_by_fp": "5f1c…a740",
"latency_ms": 2140,
"ct_sha256": "9ab4c0…7e02",
"commit": "0x1e77…d5b8",
"ts": "2026-08-12T14:32:09Z"
}
{
"event": "key.rotated",
"kid": "key_ops_8f2a",
"rotation": 47,
"quorum": "2-of-3",
"retiring_fp": "8f2a…d901",
"incoming_fp": "5f1c…a740",
"grace_until": "2026-08-15T09:04:11Z",
"ts": "2026-08-12T09:04:11Z"
}
{
"event": "attestation.failed",
"route": "rt_2d55",
"peer": "agent-runtime.client",
"reason": "policy_digest_mismatch",
"expected": "pol_intake_v4",
"observed": "pol_intake_v3",
"envelope_refused": true,
"billed": false,
"ts": "2026-08-12T11:18:44Z"
}
Failures are amber. Crimson is ceremonial.
In KryptAPI's visual and semantic system, crimson belongs to seals, signatures and statements. Errors — in the dashboard, the portal, and every SDK's log formatter — render amber. It sounds cosmetic. It stops people from reading a wax seal as a failure.
| HTTP | Code | Meaning & remedy |
|---|---|---|
| 400 | envelope_malformed | Required envelope field missing or the AAD is not a well-formed hop string. Nothing was relayed and nothing was metered. |
| 401 | attestation_required | No valid attestation for this route and peer, or the attestation expired (300s TTL). Re-run POST /v1/attest. |
| 403 | key_not_authorized | The key ID is not permitted to originate on this route, or its grace period has closed. Check route policy before rotating again. |
| 409 | envelope_already_opened | Ciphertext collection is single-shot by design. Fetch the receipt instead; the envelope's state is terminal. |
| 409 | idempotency_conflict | Same Idempotency-Key, different body. We return the original result rather than guessing which you meant. |
| 413 | payload_too_large | Ciphertext exceeds the route's ceiling (default 32 MB). Chunk into multiple envelopes on the same route ID. |
| 422 | cipher_suite_unsupported | The negotiated suite is not in the accepted set. We do not silently downgrade; we refuse. |
| 422 | envelope_tag_invalid | GCM authentication tag mismatch — the envelope was altered or the AAD does not match. Hard failure, never a fallback. |
| 429 | hop_rate_exceeded | Per-keypair hop ceiling reached. Retry-After is always present. Ceilings are raised on request, not by tier. |
| 503 | relay_draining | A relay node is shedding for maintenance. Retry the same Idempotency-Key; the envelope has not been accepted. |
{
"error": {
"code": "attestation_required",
"message": "No valid attestation for route rt_2d55 and peer ops-dashboard.",
"route": "rt_2d55",
"remedy": "POST /v1/attest, then retry with the returned att_ id.",
"request_id":"req_7c4a1f902b33",
"billed": false
}
}
Boring guarantees, stated plainly
Idempotency-Key
Send a UUID on any mutating request. We store the first result for 24 hours and replay it verbatim on retry. Same key with a different body returns 409 idempotency_conflict rather than a quiet second send. Retries are never double-metered.
Per-keypair rate ceilings
Ceilings are per key-pair, not per organisation, so one noisy integration cannot starve another. Defaults: 240 seals/sec, 60 attestations/sec, 12 rotations/hour. Retry-After is always present on 429. Raised on request — there is no tier to buy.
Sandbox
Sandbox key-pairs are prefixed sk_test_, route through an isolated relay, and are never metered. Sandbox envelopes expire after 24 hours and cannot address production peers, in either direction.
Versioning
The API version is pinned per request via X-Krypt-Api-Version. Versions are dated, additive within a version, and supported for 24 months after supersession. Breaking changes get a new date and a 90-day overlap.
Source-available, reproducibly built
Each SDK ships the sealing primitives, the audit verifier, and a conformance test suite you can run against your own relay.
Sandbox traffic is unmetered, permanently. You should be able to break things without reading a statement afterwards.