# 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.

::: info Base URL
Production: `https://api.kryard.com`. If you already speak Turnkey, cutover is a
single `TURNKEY_BASE_URL` swap — see [Turnkey compatibility](/signing/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](/signing/creating-keys) |
| `POST /public/v1/submit/sign_raw_payload` | `ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2` | [Signing](/signing/signing) |
| `POST /public/v1/submit/sign_transaction` | `ACTIVITY_TYPE_SIGN_TRANSACTION_V2` | [Signing](/signing/signing) |
| `POST /public/v1/submit/export_private_key` | `ACTIVITY_TYPE_EXPORT_PRIVATE_KEY` | [Key export](/signing/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 }`:

```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
  }
}
```

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

- [Creating keys](/signing/creating-keys) — `create_private_keys`, curves, address formats.
- [Signing](/signing/signing) — `sign_raw_payload` and `sign_transaction`.
- [Key export](/signing/key-export) — HPKE-sealed `export_private_key`.
- [Reference](/signing/reference) — the full curve / address-format / hash-function enums.
- [Turnkey compatibility](/signing/turnkey-compatibility) — cutover and scope.
