Signing API overview
The Signing API is Kryard's Turnkey-wire-compatible surface for creating keys, signing, and exporting. It speaks the exact Turnkey shapes — same endpoint paths, same activity envelope, same X-Stamp auth — over a KMS-encrypted key store and an isolated signer that the API service can never decrypt.
Beyond Turnkey's secp256k1 EVM baseline, Kryard's signer is multi-curve (secp256k1, ed25519, P-256) and derives addresses across 38 address formats (EVM, the full Bitcoin script-type matrix, Cosmos, Solana, Sui, TON, and more), with HPKE key export.
Base URL
Production: https://api.kryard.com. If you already speak Turnkey, cutover is a single TURNKEY_BASE_URL swap — see Turnkey compatibility.
Endpoints
Every signing operation is a POST to /public/v1/submit/<activity>. The request is an activity; the response is the single-nested activity envelope.
| Endpoint | Activity type | Page |
|---|---|---|
POST /public/v1/submit/create_private_keys | ACTIVITY_TYPE_CREATE_PRIVATE_KEYS_V2 | Creating keys |
POST /public/v1/submit/sign_raw_payload | ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2 | Signing |
POST /public/v1/submit/sign_transaction | ACTIVITY_TYPE_SIGN_TRANSACTION_V2 | Signing |
POST /public/v1/submit/export_private_key | ACTIVITY_TYPE_EXPORT_PRIVATE_KEY | Key export |
Query endpoints (also POST) read activities, keys, wallets, and accounts: /public/v1/query/get_activity, /public/v1/query/list_private_keys, and friends.
The activity envelope
Each submit returns the single-nested Turnkey envelope. The per-type result is keyed by the activity (createPrivateKeysResult, signTransactionResult, …), and timestamps are { seconds, nanos }:
{
"activity": {
"id": "9b1f…",
"organizationId": "org-…",
"status": "ACTIVITY_STATUS_COMPLETED",
"type": "ACTIVITY_TYPE_SIGN_TRANSACTION_V2",
"intent": { "signTransactionIntent": { "signWith": "0xabc…" } },
"result": { "signTransactionResult": { "signedTransaction": "02f8…" } },
"votes": [],
"fingerprint": "sha256:…",
"canApprove": false,
"canReject": true,
"createdAt": { "seconds": "1748956800", "nanos": "0" },
"updatedAt": { "seconds": "1748956800", "nanos": "0" },
"failure": null
}
}Match the shape, not REST aesthetics
The result is nested under activity.result.<type>Result. Query endpoints use POST. signedTransaction is returned hex without a 0x prefix. These are frozen wire-contract details — see tools/compat-harness/wire-contract.md.
Status
status is one of:
ACTIVITY_STATUS_COMPLETED— done; readresult.<type>Result.ACTIVITY_STATUS_FAILED— terminal; readfailure.{code,message}.ACTIVITY_STATUS_PENDING— retryable (a transient signer error). Re-submit the same body to collapse onto the existing activity, or changetimestampMsto force a new one.
Authentication — X-Stamp
Every request carries an X-Stamp header: a P-256 API-key signature over the exact raw request-body string.
X-Stamp: base64url(JSON({ publicKey, scheme: "SIGNATURE_SCHEME_TK_API_P256", signature }))signature— DER-encoded ECDSA-P256 overSHA-256(rawBodyString), hex.publicKey— the compressed P-256 public key (your registered API key), hex.
Because the signature covers the byte-exact body, stamp and POST the same string — never re-serialize between signing and sending. Use the SDK's createApiKeyStamper, or any Turnkey-protocol stamper (e.g. @turnkey/api-key-stamper).
import { createApiKeyStamper } from "@kryard/sdk";
const stamper = createApiKeyStamper({
apiPublicKey: process.env.KRYARD_API_PUBLIC_KEY!, // compressed P-256, hex
apiPrivateKey: process.env.KRYARD_API_PRIVATE_KEY!,
});
const body = JSON.stringify({
type: "ACTIVITY_TYPE_SIGN_TRANSACTION_V2",
timestampMs: String(Date.now()),
organizationId: process.env.KRYARD_ORG!,
parameters: { /* … */ },
});
const { stampHeaderName, stampHeaderValue } = await stamper.stamp(body);
await fetch("https://api.kryard.com/public/v1/submit/sign_transaction", {
method: "POST",
headers: { "content-type": "application/json", [stampHeaderName]: stampHeaderValue },
body, // the exact string that was stamped
});Using the SDK
@kryard/sdk wraps the stamp → POST → envelope-unwrap flow behind a typed KryardClient. Build it once from your org id and a stamper; the SDK examples on the pages below all call methods on this client:
import { KryardClient, createApiKeyStamper } from "@kryard/sdk";
const client = new KryardClient({
baseUrl: "https://api.kryard.com",
organizationId: process.env.KRYARD_ORG!,
stamper: createApiKeyStamper({
apiPublicKey: process.env.KRYARD_API_PUBLIC_KEY!, // compressed P-256, hex
apiPrivateKey: process.env.KRYARD_API_PRIVATE_KEY!,
}),
});Each method returns the unwrapped per-type result directly (no activity.result.… nesting) and throws ActivityError when the activity comes back ACTIVITY_STATUS_FAILED. Use the client for application code; drop to the raw X-Stamp + fetch path above only when you need wire-level control.
Idempotency
Idempotency is SHA-256(canonical_json(full_request_body)) scoped per organization. The hash covers the whole body, including timestampMs:
- Re-POST an identical body → the existing activity is returned (no double-execution).
- Need a genuinely new activity → bump
timestampMs.
Invariants
These hold for every request:
- The API never decrypts keys. Only the isolated signer holds KMS-decrypt permission; private keys are generated inside the signer and never returned in plaintext (export is HPKE-sealed).
- Every operation is an immutable activity — the unit of work, idempotency, and audit.
- Signing is bound to activity + policy. The signer re-verifies the payload matches the policy-evaluated input before signing, and refuses on mismatch (
SIGNER_REQUEST_MISMATCH).
Pages
- Creating keys —
create_private_keys, curves, address formats. - Signing —
sign_raw_payloadandsign_transaction. - Key export — HPKE-sealed
export_private_key. - Reference — the full curve / address-format / hash-function enums.
- Turnkey compatibility — cutover and scope.