Skip to content

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.

EndpointActivity typePage
POST /public/v1/submit/create_private_keysACTIVITY_TYPE_CREATE_PRIVATE_KEYS_V2Creating keys
POST /public/v1/submit/sign_raw_payloadACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2Signing
POST /public/v1/submit/sign_transactionACTIVITY_TYPE_SIGN_TRANSACTION_V2Signing
POST /public/v1/submit/export_private_keyACTIVITY_TYPE_EXPORT_PRIVATE_KEYKey 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 }:

json
{
  "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; read result.<type>Result.
  • ACTIVITY_STATUS_FAILED — terminal; read failure.{code,message}.
  • ACTIVITY_STATUS_PENDING — retryable (a transient signer error). Re-submit the same body to collapse onto the existing activity, or change timestampMs to 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 over SHA-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).

ts
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:

ts
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

Wallet infrastructure for gasless, sponsored transactions — a Turnkey-compatible signer and a managed EIP-7702 relay.