Skip to main content

Overview

Turnkey provides two primary ways to sign and broadcast transactions:
  1. Using the React handler (handleSendTransaction) from @turnkey/sdk-react-wallet-kit
    This gives you:
    • modals
    • spinner + chain logo
    • success screen
    • explorer link
    • built-in polling
  2. Using low-level functions in @turnkey/sdk-core
    You manually call:
    • signAndSendTransaction → submit
    • pollTransactionStatus → wait for inclusion
This page shows both flows with full code included.

1. Using handleSendTransaction (React)

This handler wraps everything: intent creation, signing, Turnkey submission, polling, modal UX, and final success UI.

Step 1 — Configure the Provider

import { TurnkeyProvider } from "@turnkey/sdk-react-wallet-kit";

const turnkeyConfig = {
  apiBaseUrl: "https://api.turnkey.com",
  defaultOrganizationId: process.env.NEXT_PUBLIC_TURNKEY_ORG_ID,
  rpId: window.location.hostname,
  iframeUrl: "https://auth.turnkey.com",
};

export default function App({ children }) {
  return (
    <TurnkeyProvider config={turnkeyConfig}>
      {children}
    </TurnkeyProvider>
  );
}

Step 2 — Use handleSendTransaction inside your UI

const { handleSendTransaction, wallets } = useTurnkey();

const walletAccount = wallets[0].accounts[0];

await handleSendTransaction({
  organizationId: "<org-id>",
  from: walletAccount.address,
  to: "0xRecipient",
  value: "1000000000000000",
  data: "0x",
  caip2: "eip155:8453",
  sponsor: true,
});
This automatically:
  • opens Turnkey modal
  • shows chain logo
  • polls until INCLUDED
  • displays success page + explorer link

2. Using @turnkey/sdk-core directly (non-React)

For custom frameworks, Node.js servers, or full manual control. You will call:

signAndSendTransaction(params)

→ returns { sendTransactionStatusId }

pollTransactionStatus(params)

→ returns { txHash, status }

Step 1 — Create a client

import { Turnkey } from "@turnkey/sdk-core";

const client = new Turnkey({
  apiBaseUrl: "https://api.turnkey.com",
  defaultOrganizationId: process.env.TURNKEY_ORG_ID,
});

Step 2 — Submit the transaction

const { sendTransactionStatusId } = await client.signAndSendTransaction({
  walletAccount,
  organizationId: "<org-id>",
  from: walletAccount.address,
  to: "0xRecipient",
  caip2: "eip155:8453",
  sponsor: true,
  value: "0",
  data: "0x",
  nonce: "0",
});

Step 3 — Poll for inclusion

const { txHash, status } = await client.pollTransactionStatus({
  sendTransactionStatusId,
});

console.log("Mined:", txHash);

3. Core Code (full implementations)

signAndSendTransaction — from @turnkey/sdk-core

/**
 * Signs and submits an Ethereum transaction using Turnkey.
 *
 * Behavior:
 * - Constructs transaction intent (sponsored or EIP-1559)
 * - Submits via Turnkey
 * - Returns { sendTransactionStatusId }
 *
 * Does NOT poll — caller must poll via pollTransactionStatus.
 */
signAndSendTransaction = async (
  params: SignAndSendTransactionParams
): Promise<SignAndSendResult> => {
  const {
    organizationId,
    from,
    to,
    caip2,
    sponsor,
    value,
    data,
    nonce,
    gasLimit,
    maxFeePerGas,
    maxPriorityFeePerGas,
    walletAccount,
  } = params;

  return withTurnkeyErrorHandling(
    async () => {
      const intent = {
        from,
        to,
        caip2,
        ...(value ? { value } : {}),
        ...(data ? { data } : {}),
      };

      if (sponsor) {
        intent["sponsor"] = true;
      } else {
        if (nonce) intent["nonce"] = nonce;
        if (gasLimit) intent["gasLimit"] = gasLimit;
        if (maxFeePerGas) intent["maxFeePerGas"] = maxFeePerGas;
        if (maxPriorityFeePerGas)
          intent["maxPriorityFeePerGas"] = maxPriorityFeePerGas;
      }

      const resp = await this.httpClient.ethSendTransaction({
        ...intent,
        ...(organizationId && { organizationId }),
      });

      return { sendTransactionStatusId: resp.sendTransactionStatusId };
    },
    {
      errorMessage: "Failed to submit transaction",
      errorCode: TurnkeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR,
    }
  );
};

pollTransactionStatus — from @turnkey/sdk-core

/**
 * Polls Turnkey until a transaction reaches a terminal state.
 *
 * Terminal states:
 * - COMPLETED / INCLUDED → resolves { txHash, status }
 * - FAILED / CANCELLED → rejects
 */
pollTransactionStatus({
  httpClient,
  organizationId,
  sendTransactionStatusId,
}: PollTransactionStatusParams): Promise<{
  txHash: string;
  status: string;
}> {
  return withTurnkeyErrorHandling(
    async () => {
      return new Promise((resolve, reject) => {
        const ref = setInterval(async () => {
          try {
            const resp = await httpClient.getSendTransactionStatus({
              organizationId,
              sendTransactionStatusId,
            });

            const status = resp?.txStatus;
            const txHash = resp?.eth?.txHash;
            const txError = resp?.txError;

            if (!status) return;

            if (txError || status === "FAILED" || status === "CANCELLED") {
              clearInterval(ref);
              reject(txError || `Transaction ${status}`);
              return;
            }

            if (status === "COMPLETED" || status === "INCLUDED") {
              clearInterval(ref);
              resolve({ txHash: txHash!, status });
            }
          } catch (e) {
            console.warn("polling error:", e);
          }
        }, 500);
      });
    },
    {
      errorMessage: "Failed to poll transaction status",
      errorCode: TurnkeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR,
    }
  );
}

4. Full React Handler (uses the above two functions)

const handleSendTransaction = useCallback(
  async (params: HandleSendTransactionParams): Promise<void> => {
    const s = await getSession();
    const organizationId = params.organizationId || s?.organizationId;

    const {
      from,
      to,
      value,
      data,
      caip2,
      sponsor,
      gasLimit,
      maxFeePerGas,
      maxPriorityFeePerGas,
      nonce: providedNonce,
      successPageDuration = 2000,
    } = params;

    const { nonce } = await generateNonces({
      from: from,
      rpcUrl: DEFAULT_RPC_BY_CHAIN[caip2],
      providedNonce,
    });

    return new Promise((resolve, reject) => {
      const SendTxContainer = () => {
        const cleanedData =
          data && data !== "0x" && data !== "" ? data : undefined;

        const action = async () => {
          const { sendTransactionStatusId } =
            await client.signAndSendTransaction({
              walletAccount,
              organizationId,
              from,
              to,
              caip2,
              sponsor: !!sponsor,
              value,
              data: cleanedData,
              nonce,
              gasLimit,
              maxFeePerGas,
              maxPriorityFeePerGas,
            });

          const { txHash } = await client.pollTransactionStatus({
            httpClient: client.httpClient,
            organizationId,
            sendTransactionStatusId,
          });

          return { txHash };
        };

        return (
          <SendTransactionPage
            icon={<img src={getChainLogo(caip2)} className="h-10 w-10" />}
            action={action}
            caip2={caip2}
            successPageDuration={successPageDuration}
            onSuccess={() => resolve()}
            onError={(err) => reject(err)}
          />
        );
      };

      pushPage({
        key: "Send Transaction",
        content: <SendTxContainer />,
        preventBack: true,
        showTitle: false,
      });
    });
  },
  [pushPage, client]
);