> For the complete documentation index, see [llms.txt](https://hinkal-team.gitbook.io/hinkal/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://hinkal-team.gitbook.io/hinkal/hinkal-api/api-integration/turnkey.md).

# Turnkey

Turnkey gives your users a secure embedded wallet from a simple sign-in - email, passkey, or social login - with no seed phrase, and with private keys held inside hardware-isolated enclaves that are never exposed, not even to Turnkey. Because Hinkal only needs a standard signer to operate, a Turnkey wallet plugs in directly: the same wallet can hold a private, shielded balance and run confidential deposits, withdrawals, transfers, swaps, and private sends, while the keys stay protected by Turnkey the entire time.

Turnkey's role is unchanged. It manages keys, enforces policy, and produces signatures inside its secure infrastructure. Hinkal uses those signatures to authorize private operations and never takes custody of the key.

### Compatibility

| Environment | Supported | Notes                                         |
| ----------- | --------- | --------------------------------------------- |
| Browser     | ✅         | React, Next.js                                |
| Libraries   | ✅         | @turnkey/react-wallet-kit v2, @turnkey/ethers |

### How the integration works

Three parties are involved:

* **The user's Turnkey wallet** - produces every signature, inside Turnkey
* **The Hinkal API** - an HTTP service running inside a secure enclave. It decrypts the user's shielded balance, generates the zero-knowledge proofs, and builds the on-chain transactions. The wallet's private key never reaches it.
* **The relayer** - broadcasts private transactions, so the user's public wallet is never the on-chain sender of a confidential operation.

### Installation

```bash
npm install @turnkey/react-wallet-kit @turnkey/ethers ethers
```

```bash
yarn add @turnkey/react-wallet-kit @turnkey/ethers ethers
```

### 1. Get a signer from Turnkey

Every Hinkal request is authorized by the user's wallet. Turnkey exposes a wallet through `TurnkeySigner`, a drop-in `ethers.Signer`.

```tsx
import { useTurnkey } from "@turnkey/react-wallet-kit";
import { TurnkeySigner } from "@turnkey/ethers";
import { ethers } from "ethers";

const { wallets, httpClient, session } = useTurnkey();

// A Turnkey wallet can hold several accounts; select the EVM one
const evmAccount = wallets
  .flatMap((w) => w.accounts)
  .find((a) => a.addressFormat === "ADDRESS_FORMAT_ETHEREUM");

const account        = ethers.getAddress(evmAccount.address);
const organizationId = session?.organizationId ?? evmAccount.organizationId;
const chainId        = SUPPORTED_CHAINS[0].id;

const provider = new ethers.JsonRpcProvider(rpcUrl);
const signer   = new TurnkeySigner(
  { client: httpClient, organizationId, signWith: account },
  provider,
);
```

`TurnkeySigner` routes every signing request through Turnkey: `client` is the authenticated Turnkey client, `organizationId` identifies the org that owns the key, and `signWith` is the address to sign as. The result is a standard `ethers.Signer` - from here every Hinkal call is identical to any other wallet.

If the user has no EVM wallet yet, create one before selecting an account:

```tsx
await httpClient.createWallet({
  walletName: `Default Wallet ${Date.now()}`,
  accounts: [{
    curve:         "CURVE_SECP256K1",
    pathFormat:    "PATH_FORMAT_BIP32",
    path:          "m/44'/60'/0'/0/0",
    addressFormat: "ADDRESS_FORMAT_ETHEREUM",
  }],
});
```

### 2. How requests are authorized

Hinkal authorizes requests with two kinds of signature, both produced inside Turnkey. The enclave reconstructs the same data and verifies the signature server-side before doing anything.

#### Read signature (sessions and getters)

Read-only endpoints - fetching the shielded balance, getting fee structures, quoting a swap - use a personal-message signature. The signed message is exactly:

```
Authorize Hinkal session
Session ID: <nonce>
```

```tsx
const nonce     = crypto.randomUUID();
const signature = await signer.signMessage(
  `Authorize Hinkal session\nSession ID: ${nonce}`,
);
```

#### Transaction signature (EIP-712 typed data)

Each transaction is authorized with an EIP-712 typed-data signature that commits to the exact operation. The enclave rebuilds the identical structure and verifies it, so the user approves precisely what will execute.

The domain is:

```ts
const domain = { name: "Hinkal Enclave", chainId };
```

Each operation has its own primary type. The full set:

```ts
const types = {
  TokenAmount: [
    { name: "token",  type: "address" },
    { name: "amount", type: "int256"  },
  ],
  Deposit: [
    { name: "nonce",        type: "string"        },
    { name: "chainId",      type: "uint256"       },
    { name: "tokenAmounts", type: "TokenAmount[]" },
  ],
  Withdraw: [
    { name: "nonce",        type: "string"        },
    { name: "chainId",      type: "uint256"       },
    { name: "tokenAmounts", type: "TokenAmount[]" },
    { name: "recipient",    type: "string"        },
  ],
  Transfer: [
    { name: "nonce",        type: "string"        },
    { name: "chainId",      type: "uint256"       },
    { name: "tokenAmounts", type: "TokenAmount[]" },
    { name: "recipient",    type: "string"        },
  ],
  Swap: [
    { name: "nonce",        type: "string"        },
    { name: "chainId",      type: "uint256"       },
    { name: "tokenAmounts", type: "TokenAmount[]" },
  ],
};
```

When signing, include only the primary type you are using plus the types it references. A deposit, for example:

```tsx
const nonce     = crypto.randomUUID();
const signature = await signer.signTypedData(
  { name: "Hinkal Enclave", chainId },
  {
    Deposit:     types.Deposit,
    TokenAmount: types.TokenAmount,
  },
  { nonce, chainId, tokenAmounts: [{ token, amount }] },
);
```

Two rules the enclave enforces - the signature must match exactly:

* **Sort `tokenAmounts` by token address (checksummed, ascending)** before signing. The enclave re-sorts before verifying; an unsorted array produces a non-matching signature.
* **The `nonce` is single-use and expires after 60 seconds.** Generate a fresh UUID for every signed request; a reused or expired nonce is rejected server-side.

#### Write session (sign once for 24 hours)

If you open a write session, the user signs the session message once with an extra third line:

```
Authorize Hinkal session
Session ID: <nonce>
This signature can also be used to submit transactions.
```

That single signature authorizes every transaction for 24 hours — the per-transaction typed-data signature is skipped. Without a write session, each transaction is signed individually as shown above.

### 3. Read the shielded balance

The shielded balance is the user's private, encrypted holdings. Reading it uses a read signature passed as query parameters:

```tsx
const params = new URLSearchParams({
  address:   account,
  chainId:   String(chainId),
  signature,
  nonce,
});
const res = await fetch(`https://api.hinkal.io/balance?${params}`);
const { balances } = await res.json(); // { success: true, balances: [...] }
```

The enclave decrypts the user's shielded outputs internally and returns the balances.

### 4. Shield - deposit (public → private)

A deposit moves funds from the user's public Turnkey wallet into their shielded balance.

1. The wallet signs the `Deposit` typed data (or a write session is reused).
2. `POST /deposit` with the signature and details.
3. The enclave returns an unsigned transaction (`txData`).
4. The user's wallet signs and broadcasts it - a deposit is the public on-ramp into privacy, sent from the user's own address.

```tsx
const res = await fetch("https://api.hinkal.io/deposit", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address:        account,
    chainId,
    tokenAddresses, // string[] — same order as amounts
    amounts,        // string[] — token base units
  }),
});
const { txData } = await res.json(); // { success: true, txData: { to, data, value? } }

// User broadcasts with their Turnkey wallet (signed inside Turnkey)
const tx = await signer.sendTransaction(txData);
await tx.wait();
```

After confirmation, the funds are in the user's shielded balance.

### 5. Unshield - withdraw (private → public)

A withdrawal sends funds from the shielded balance to any public address. The user does not broadcast it: the enclave builds and proves the transaction and the relayer broadcasts it, so the user's wallet is never the on-chain sender.

1. The wallet signs the `Withdraw` typed data (or a write session is reused).
2. `POST /withdraw`.
3. The response is the resulting `txHash`.

```tsx
const res = await fetch("https://api.hinkal.io/withdraw", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address:          account,
    chainId,
    tokenAddresses,
    amounts,          // positive values; the enclave negates them internally
    recipientAddress, // public address receiving the funds
    feeToken,         // token used to pay the relayer fee
    feeStructure,     // from GET /get-fee-structure
  }),
});
const { txHash } = await res.json(); // { success: true, txHash }
```

### 6. Transfer (private → private)

A transfer moves funds from the user's shielded balance to another user's shielded balance. Both sides stay private - there is no public on-chain link between sender and recipient, and the amount is not visible. The user does not broadcast it: the enclave proves the transaction and the relayer broadcasts it.

The recipient is identified by their **recipient info** - a private identifier the recipient obtains from `GET /recipient-info`(authorized by their own session signature) and shares with the sender. It is not a public address.

```tsx
// The recipient fetches their own recipient info and shares it with the sender:
const params = new URLSearchParams({
  address: recipientAccount, chainId: String(chainId), signature, nonce,
});
const { recipientInfo } = await (
  await fetch(`https://api.hinkal.io/recipient-info?${params}`)
).json();
```

The sender then signs the `Transfer` typed data and submits:

```tsx
const res = await fetch("https://api.hinkal.io/transfer", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address:          account,
    chainId,
    tokenAddresses,
    amounts,          // positive values; the enclave negates them internally
    recipientAddress, // the recipient's recipientInfo string
    feeToken,         // token used to pay the relayer fee
    feeStructure,     // from GET /get-fee-structure
  }),
});
const { txHash } = await res.json(); // { success: true, txHash }
```

The enclave proves and the relayer broadcasts.

### 7. Swap (within the shielded balance)

A swap exchanges one token for another inside the shielded balance. It is a two-call flow: first fetch a quote, then execute.

```tsx
// 1. Quote — read signature
const quoteParams = new URLSearchParams({
  signature, nonce, address: account, chainId: String(chainId),
  inputTokenAddress, outputTokenAddress, amount,
});
const quote = await (
  await fetch(`https://api.hinkal.io/get-swap-data?${quoteParams}`)
).json();
// quote: { externalActionId, swapData, outSwapAmount }

// 2. Execute — Swap typed-data signature (or write session)
const res = await fetch("https://api.hinkal.io/swap", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address:          account,
    chainId,
    tokenAddresses,   // [inputToken, outputToken]
    amounts,          // [inputAmount (negative), expectedOutputAmount (positive, after Hinkal fee)]
    externalActionId: quote.externalActionId,
    swapData:         quote.swapData,
    feeToken,
    feeStructure,
  }),
});
const { txHash } = await res.json();
```

As with withdrawals and transfers, the enclave proves and the relayer broadcasts.

### 8. Private send (one deposit → many private payouts)

A private send deposits public funds once and has the enclave pay out privately to one or more recipients - the on-chain link between sender and recipients is never visible.

1. `POST /private-send` with the token, recipients, and amounts → returns an order (`orderId`, `approvalAddress`, `serializedTx`, `amountIn`, `amountOut`, `fee`).
2. Approve the ERC20 spend if `approvalAddress` is set.
3. Sign and broadcast the deposit with the user's Turnkey wallet.
4. Poll `GET /private-send/{orderId}` until every scheduled payout completes.

```tsx
// 1. Prepare the order (write session or per-transaction signature)
const res = await fetch("https://api.hinkal.io/private-send", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    signature,
    nonce,
    address:      account,
    chainId,
    tokenAddress,        // single token
    recipients,          // [{ address, amount }, ...] amounts in base units
    feeToken,            // optional — defaults to tokenAddress
    // txCompletionTime, // optional Unix-seconds deadline
  }),
});
const order = await res.json();
// { orderId, approvalAddress, serializedTx, amountIn, amountOut, fee }

// 2. Approve if required (EVM)
if (order.approvalAddress) {
  const token = new ethers.Contract(tokenAddress, ERC20_ABI, signer);
  await (await token.approve(order.approvalAddress, BigInt(order.amountIn))).wait();
}

// 3. Broadcast the deposit with the Turnkey wallet (signed inside Turnkey)
const tx = ethers.Transaction.from(
  "0x" + Buffer.from(order.serializedTx, "base64").toString("hex"),
);
tx.nonce = await signer.provider.getTransactionCount(account, "pending");
const depositHash = await signer.provider.send(
  "eth_sendRawTransaction",
  [await signer.signTransaction(tx)],
);

// 4. Poll until all payouts settle
const result = await pollOrder(order.orderId); // GET /private-send/{orderId}
```

`amountIn` is what leaves the wallet; `amountOut` is what recipients collectively receive; the difference is the protocol fee. Amounts are in the token's smallest unit. The user broadcasts the single deposit; the enclave then proves and the relayer broadcasts each private payout.

### Summary

| Operation                    | Endpoint                            | Signature             | Who broadcasts                     |
| ---------------------------- | ----------------------------------- | --------------------- | ---------------------------------- |
| Read balance                 | `GET /balance`                      | Read (message)        | —                                  |
| Deposit (shield)             | `POST /deposit`                     | `Deposit` typed data  | The user's wallet                  |
| Withdraw (unshield)          | `POST /withdraw`                    | `Withdraw` typed data | Relayer                            |
| Transfer (private → private) | `POST /transfer`                    | `Transfer` typed data | Relayer                            |
| Swap                         | `GET /get-swap-data` + `POST /swap` | `Swap` typed data     | Relayer                            |
| Private send                 | `POST /private-send` + poll         | session or per-tx     | User (deposit) + relayer (payouts) |

<br>
