Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/tkhq/sdk/llms.txt

Use this file to discover all available pages before exploring further.

All Turnkey mutation endpoints are asynchronous — when you call a mutation, the API immediately returns an activity object. Your code must poll until that activity reaches a terminal state. The exceptions are the private key signing endpoints (signTransaction, signRawPayload, signRawPayloads), which are synchronous and return results directly.

Activity statuses

Every activity has a status field that progresses through the following states:
StatusMeaning
ACTIVITY_STATUS_CREATEDActivity was received and is being processed
ACTIVITY_STATUS_PENDINGActivity is pending consensus or awaiting a quorum response
ACTIVITY_STATUS_CONSENSUS_NEEDEDActivity requires additional approvals before it can proceed
ACTIVITY_STATUS_COMPLETEDActivity finished successfully — results are available
ACTIVITY_STATUS_FAILEDActivity failed; check the error details
ACTIVITY_STATUS_REJECTEDActivity was rejected by a policy or consensus vote
The three terminal statuses (COMPLETED, FAILED, REJECTED) are exported as the TERMINAL_ACTIVITY_STATUSES constant from @turnkey/http.

createActivityPoller

createActivityPoller is the recommended way to poll activities when using TurnkeyClient. It wraps any client mutation method, submits the request, then polls until the activity reaches a terminal status.
import { TurnkeyClient } from "@turnkey/http";
import { createActivityPoller, TurnkeyActivityError } from "@turnkey/http";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";

const client = new TurnkeyClient(
  { baseUrl: "https://api.turnkey.com" },
  new ApiKeyStamper({ apiPublicKey: "...", apiPrivateKey: "..." }),
);

const activityPoller = createActivityPoller({
  client,
  requestFn: client.createPrivateKeys,
});

try {
  const activity = await activityPoller({
    organizationId: "<Your organization id>",
    parameters: {
      privateKeys: [
        {
          privateKeyName: "my-key",
          curve: "CURVE_SECP256K1",
          addressFormats: ["ADDRESS_FORMAT_ETHEREUM"],
          privateKeyTags: [],
        },
      ],
    },
  });

  // activity.status === "ACTIVITY_STATUS_COMPLETED"
  const privateKeyId =
    activity.result.createPrivateKeysResultV2?.privateKeys?.[0]?.privateKeyId;
  console.log(privateKeyId);
} catch (error) {
  if (error instanceof TurnkeyActivityError) {
    console.log(error.activityId);     // ID of the failed/blocked activity
    console.log(error.activityStatus); // the terminal status
    console.log(error.activityType);   // the type of activity
    console.log(error.cause);          // underlying Error, if any
  }
}
client
TurnkeyClient
required
The TurnkeyClient instance used for polling.
requestFn
(input: I) => Promise<TActivityResponse>
required
The client method to call. Must be a method that returns an activity response — for example, client.createPrivateKeys.
refreshIntervalMs
number
How often to poll, in milliseconds. Defaults to 500.

withAsyncPolling

withAsyncPolling is an older helper that wraps the lower-level TurnkeyApi static fetchers (not the TurnkeyClient instance methods). It is still functional but is considered deprecated in favor of createActivityPoller.
import { withAsyncPolling, TurnkeyActivityError } from "@turnkey/http";
import { TurnkeyApi } from "@turnkey/http";

// Wrap the static fetcher with built-in polling
const fetcher = withAsyncPolling({
  request: TurnkeyApi.createPrivateKeys,
});

try {
  const activity = await fetcher({
    body: {
      /* ... */
    },
  });

  console.log(
    activity.result.createPrivateKeysResultV2?.privateKeys?.[0]?.privateKeyId,
  );
} catch (error) {
  if (error instanceof TurnkeyActivityError) {
    // Handle failed, rejected, or consensus-blocked activities
  }
}

TurnkeyActivityError

When an activity reaches ACTIVITY_STATUS_FAILED, ACTIVITY_STATUS_REJECTED, or ACTIVITY_STATUS_CONSENSUS_NEEDED, the poller throws a TurnkeyActivityError:
class TurnkeyActivityError extends Error {
  activityId: string | undefined;      // ID of the activity
  activityStatus: string | undefined;  // terminal status reached
  activityType: string | undefined;    // e.g. "ACTIVITY_TYPE_CREATE_PRIVATE_KEYS"
  cause: Error | undefined;            // underlying error, if any
}
There is also a dedicated TurnkeyActivityConsensusNeededError subclass that is thrown specifically when status is ACTIVITY_STATUS_CONSENSUS_NEEDED. It has the same properties.

Handling consensus

Some organizations require multi-party approval (consensus) before certain activities can complete. When this happens:
  1. The poller throws TurnkeyActivityError with activityStatus === "ACTIVITY_STATUS_CONSENSUS_NEEDED".
  2. Store the activityId from the error for later retrieval.
  3. Once approvals are collected out-of-band, call client.getActivity({ activityId, organizationId }) to re-fetch the final status.
  4. Use the helper functions to extract results from the completed activity:
import {
  getSignatureFromActivity,
  getSignaturesFromActivity,
  getSignedTransactionFromActivity,
  assertActivityCompleted,
} from "@turnkey/http";

// Re-fetch and assert the activity is complete
const response = await client.getActivity({ activityId, organizationId });
const activity = response.activity;

assertActivityCompleted(activity); // throws if not COMPLETED

// For ACTIVITY_TYPE_SIGN_RAW_PAYLOAD or ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2
const signature = getSignatureFromActivity(activity);
// signature: { r, s, v }

// For ACTIVITY_TYPE_SIGN_TRANSACTION or ACTIVITY_TYPE_SIGN_TRANSACTION_V2
const signedTx = getSignedTransactionFromActivity(activity);

Best practices

  • Always catch TurnkeyActivityError when using a poller. Network errors or API errors will surface as plain Error or TurnkeyRequestError instances.
  • Keep refreshIntervalMs at the default 500ms for most use cases. Decrease it only if your application is latency-sensitive and you are willing to pay for the extra requests.
  • For consensus-gated activities, do not rely on the poller to eventually return — it will throw immediately when consensus is needed. Design your flow to handle this case explicitly.
  • Use TERMINAL_ACTIVITY_STATUSES to check whether a status you have retrieved is final before deciding whether to poll again.

Build docs developers (and LLMs) love