diff --git a/docs.json b/docs.json
index 21bd186b..bbaaeec6 100644
--- a/docs.json
+++ b/docs.json
@@ -225,7 +225,10 @@
"group": "Solution",
"pages": [
"solutions/key-management/encryption-key-storage",
- "solutions/key-management/enterprise-disaster-recovery"
+ "solutions/key-management/enterprise-disaster-recovery",
+ "solutions/key-management/programmable-credential-access",
+ "solutions/key-management/api-key-storage",
+ "solutions/key-management/mpc-keyshare-storage"
]
}
]
@@ -359,7 +362,8 @@
},
"features/wallets/pregenerated-wallets",
"features/wallets/claim-links",
- "features/wallets/aa-wallets"
+ "features/wallets/aa-wallets",
+ "features/secrets"
]
},
{
diff --git a/features/secrets.mdx b/features/secrets.mdx
new file mode 100644
index 00000000..9111f970
--- /dev/null
+++ b/features/secrets.mdx
@@ -0,0 +1,100 @@
+---
+title: "Secret storage"
+description: "Import, store, and export arbitrary secrets (passwords, credit cards, API keys) with policy-gated, end-to-end encrypted access."
+tag: "Beta"
+---
+
+import { FeatureCard } from '/snippets/feature-card.mdx'
+import { SolutionCard } from '/snippets/solution-card.mdx'
+import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx'
+
+
+
+Turnkey Secrets lets you store arbitrary sensitive data (passwords, credit card details, API keys, SSNs) encrypted end-to-end between your client and Turnkey's [secure enclaves](/security/secure-enclaves). The [policy engine](/features/policies/overview) evaluates every export, so you control exactly who can retrieve a secret, under what conditions, and with how many approvals. We designed the secret storage API for flexibility and programmability.
+
+Plaintext only ever exists inside the enclave and on the client that imported or exported it.
+## How it works
+
+The Secrets methods use the same enclave secure-channel pattern as [wallet import](/features/wallets/import-wallets) and [wallet export](/features/wallets/export-wallets). Every transfer is HPKE-encrypted to a single-use target key, so plaintext appears only inside the enclave and on the client holding the matching private key. See [Enclave secure channels](/security/enclave-secure-channels) for the canonical protocol details.
+
+What is specific to Secrets:
+
+- **Import**: your client encrypts the secret to a single-use ingress target key minted inside the enclave and submits only ciphertext. The enclave re-encrypts it for storage at rest and deletes the ingress key.
+- **Export**: the export request carries an ephemeral P-256 target public key. After policy evaluation approves the request, the enclave re-encrypts the secret to that key. The recipient key is fully configurable: it can belong to the requester, to another agent or service, or to a party that isn't an approver at all. Only the holder of the matching private key can decrypt the result; the payload is useless to anyone else, including the approvers themselves.
+- **Batch export is all-or-nothing**: the request succeeds only if every policy evaluation returns `ALLOW`. A `DENY`, or any evaluation without an `ALLOW` outcome, rejects the entire batch. Turnkey fails closed and never exports part of a batch.
+
+## Static properties
+
+Secrets are created with optional **static properties**: string key-value pairs that are immutably bound to the secret and visible to the policy engine. They let you write export policies against classes of secrets instead of individual IDs:
+
+```json
+{
+ "policyName": "Only the payments agent can export credit cards",
+ "effect": "EFFECT_ALLOW",
+ "consensus": "approvers.any(u, u.tags.contains('payment-agent'))",
+ "condition": "secret.static_properties['kind'] == 'creditCard' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'"
+}
+```
+
+## Importing a secret
+
+The `importSecret` method in [`@turnkey/sdk-server`](/sdks/typescript-sdk) and [`@turnkey/core`](/sdks/typescript-sdk) handles the full flow. It initializes the ingress key, verifies the enclave signature, encrypts the secret, and submits the ciphertext:
+
+```typescript
+const secretId = await turnkey.apiClient().importSecret({
+ plaintext: JSON.stringify({ number: "4242...", exp: "11/29", cvv: "123" }),
+ name: "corporateVisa",
+ staticProperties: {
+ kind: "creditCard",
+ requiresConsensus: "true",
+ },
+});
+```
+
+For sensitive material you want wiped from memory after encryption, pass a `Uint8Array` instead of a string. The SDK zeroizes the buffer after it produces the ciphertext.
+
+Under the hood this calls [init_import_secrets](/api-reference/activities/init-import-secrets) and [import_secrets](/api-reference/activities/import-secrets).
+
+## Exporting a secret
+
+`exportSecret` generates the ephemeral keypair, submits the export activity, decrypts the result, and zeroizes the key. It is a single call when policy allows the caller to export unilaterally:
+
+```typescript
+const plaintext = await turnkey.apiClient().exportSecret({
+ secretId,
+});
+```
+
+If the export requires additional approvals, `exportSecret` throws a consensus-needed error. For multi-party flows, including multiple agent instances that co-sign the same export with session keys, use the proposal SDK helpers described in [Programmable credential access](/solutions/key-management/programmable-credential-access).
+
+## Listing secrets
+
+[list_secrets](/api-reference/queries/list-secrets) returns metadata: IDs, names, static properties, and creation timestamps:
+
+```typescript
+const { secrets } = await turnkey.apiClient().listSecrets({});
+```
+
+## Multi-party approval
+
+Because export is an activity, it composes with everything the policy engine supports: [consensus](/features/policies/overview) across durable users, tag-based approver requirements, and [root quorum](/features/users/root-quorum). Model browser and payment agent roles as separate Turnkey users, then use session keys to authenticate their ephemeral instances. Policy can require both roles to approve before a credit card leaves the enclave, while the payload stays encrypted to only one instance. This makes credential delegation easy to model without treating each ephemeral agent instance as a separate user.
+
+## Security model
+
+Turnkey is a signing and encryption platform running inside secure enclaves, originally built to secure billions of dollars in digital assets. Secret storage is built from the same primitives:
+
+- **End-to-end encryption**: plaintext exists only in enclave memory and on your client. Transport in both directions uses HPKE to single-use P-256 target keys.
+- **Authenticated storage**: at-rest ciphertext is AES-256-GCM under a per-secret key derived from the enclave quorum key. The organization, secret ID, and cipher suite are bound into the authenticated data, so no one can substitute ciphertext across secrets or organizations.
+- **Signed provenance**: the enclave quorum key signs every stored secret and ingress key. Enclaves refuse anything they didn't produce.
+- **Quantum resistant internally, agile in transit**: secrets rest under AES-256-GCM, a quantum resistant cipher. The transport cipher suite is a field in import and export requests, designed to be extended over time, so Turnkey can adopt new transport protocols as they mature.
+- **Forward secrecy**: ingress and egress target keys are single-use. Compromising one exposes at most one payload.
+- **Full auditability**: every import, export, and approval is an activity that is attributed to the authenticating credential, logged, and queryable.
+
+## Next steps
+
+
+
+
+
+
+
diff --git a/images/solutions/dark/api-key-storage.svg b/images/solutions/dark/api-key-storage.svg
new file mode 100644
index 00000000..867c3f5f
--- /dev/null
+++ b/images/solutions/dark/api-key-storage.svg
@@ -0,0 +1,8 @@
+
diff --git a/images/solutions/dark/mpc-keyshare-storage.svg b/images/solutions/dark/mpc-keyshare-storage.svg
new file mode 100644
index 00000000..e313e2ca
--- /dev/null
+++ b/images/solutions/dark/mpc-keyshare-storage.svg
@@ -0,0 +1,9 @@
+
diff --git a/images/solutions/dark/programmable-credential-access.svg b/images/solutions/dark/programmable-credential-access.svg
new file mode 100644
index 00000000..d4360217
--- /dev/null
+++ b/images/solutions/dark/programmable-credential-access.svg
@@ -0,0 +1,10 @@
+
diff --git a/images/solutions/light/api-key-storage.svg b/images/solutions/light/api-key-storage.svg
new file mode 100644
index 00000000..a9e89a85
--- /dev/null
+++ b/images/solutions/light/api-key-storage.svg
@@ -0,0 +1,8 @@
+
diff --git a/images/solutions/light/mpc-keyshare-storage.svg b/images/solutions/light/mpc-keyshare-storage.svg
new file mode 100644
index 00000000..2a491da0
--- /dev/null
+++ b/images/solutions/light/mpc-keyshare-storage.svg
@@ -0,0 +1,9 @@
+
diff --git a/images/solutions/light/programmable-credential-access.svg b/images/solutions/light/programmable-credential-access.svg
new file mode 100644
index 00000000..f28bacc1
--- /dev/null
+++ b/images/solutions/light/programmable-credential-access.svg
@@ -0,0 +1,10 @@
+
diff --git a/snippets/secrets-beta-callout.mdx b/snippets/secrets-beta-callout.mdx
new file mode 100644
index 00000000..cac8c31f
--- /dev/null
+++ b/snippets/secrets-beta-callout.mdx
@@ -0,0 +1,7 @@
+export const SecretsBetaCallout = () => (
+
+ The Secrets API is currently in closed beta.{" "}
+ Contact us to get
+ onboarded.
+
+);
diff --git a/solutions/key-management/api-key-storage.mdx b/solutions/key-management/api-key-storage.mdx
new file mode 100644
index 00000000..3b68e9b1
--- /dev/null
+++ b/solutions/key-management/api-key-storage.mdx
@@ -0,0 +1,132 @@
+---
+title: "API key storage"
+description: "Programmatically store and gate access to your most sensitive API keys."
+tag: "Beta"
+---
+
+import { FeatureCard } from '/snippets/feature-card.mdx'
+import { SolutionCard } from '/snippets/solution-card.mdx'
+import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx'
+
+
+
+An exchange API key that can place orders, cancel orders, or move funds is a direct line to your balance sheet. It usually sits in an environment variable, a config file, or a vault that decrypts inside the infrastructure you are trying to protect. An attacker who compromises that infrastructure gets the key too.
+
+Turnkey keeps plaintext credentials out of your persistent storage and infrastructure intermediaries. The credential lives encrypted inside a [secure enclave](/security/secure-enclaves), and every retrieval is evaluated by the [policy engine](/features/policies/overview) before the enclave releases anything to an authorized recipient. You decide which service identities can retrieve which keys, under what conditions, and with how many approvals. This solution builds on [Secret storage](/features/secrets).
+
+Typical credentials this pattern protects:
+
+- Exchange and EMS API keys for trading operations (REST and WebSocket)
+- Long-lived JWTs and bearer tokens with broad authority
+- OAuth refresh tokens for backend services
+- Service account credentials for third party platforms
+
+## Why not a traditional secrets vault?
+
+| | Vault in your infrastructure | Turnkey |
+| :--- | :--- | :--- |
+| Where plaintext exists | Decrypted inside your infrastructure, where a vault admin or a compromised host can read it | Inside the enclave and transiently in the authorized recipient's memory after export |
+| Access control | Enforced by software you operate | Enforced by the policy engine inside the enclave, independent of your infrastructure |
+| Multi-party approval | Bolted on, if available | Native consensus: require m-of-n approvals before a key is released |
+| Who can read a released secret | Anyone who sees the response | Only the holder of the ephemeral key the payload is encrypted to, which approvers cannot read |
+| Audit trail | Vault logs you maintain | Every retrieval and approval is a signed, attributable Turnkey activity |
+
+## Key implementation decisions
+
+| Decision | What to consider | Learn more |
+| :--- | :--- | :--- |
+| **Key classification** | Bind static properties at import time (`exchange`, `permissions`, `environment`, ...) so policies target classes of keys, such as all withdrawal-capable keys, instead of individual IDs. | [Secret storage](/features/secrets) |
+| **Service identity** | Model each trading service or environment as a Turnkey user with its own API key or session keys, so retrieval permission is scoped per service. | [Sessions](/features/authentication/sessions) |
+| **Approval requirements** | Trade-only keys can allow unilateral retrieval by the trading service. Withdrawal-capable or production keys can require human or multi-party approval. | [Policy Engine](/features/policies/overview) |
+| **Recipient control** | The export payload is encrypted to a single ephemeral public key, so only the service that generated it can read the credential, even when other parties approve. | [Enclave secure channels](/security/enclave-secure-channels) |
+| **Rotation and revocation** | Rotate by importing the new key and deleting the old one. Revoke a service's access instantly by updating policy or removing its credentials. | |
+
+## Example: exchange trading key for a trading firm
+
+A trading firm holds a long-lived JWT for an execution management system that authorizes placing and canceling orders on crypto exchanges. The plaintext should not be stored at rest in the firm's infrastructure or exposed to intermediaries; after export, it exists transiently in the authorized trading service's memory while in use.
+
+| Need | How Turnkey solves it |
+| :--- | :--- |
+| No plaintext at rest or in intermediaries | The firm imports the JWT once over an end-to-end encrypted channel. Retrieval re-encrypts it to the authorized trading service's ephemeral key |
+| Only the trading service can retrieve it | Policy scopes retrieval of `kind == 'exchangeApiKey'` secrets to the trading service user |
+| High-risk keys need oversight | A consensus policy requires human approval before any withdrawal-capable key is released |
+| Every access is attributable | Each retrieval and approval is a signed activity, logged and queryable |
+
+### Policy: scope retrieval to the trading service
+
+```json
+{
+ "policyName": "Trading service can retrieve trade-only exchange keys",
+ "effect": "EFFECT_ALLOW",
+ "consensus": "approvers.any(u, u.id == '')",
+ "condition": "secret.static_properties['kind'] == 'exchangeApiKey' && secret.static_properties['permissions'] == 'trade' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'"
+}
+```
+
+### Implementation steps
+
+
+
+ Import the credential once, with static properties the policies above target. The client encrypts the plaintext to the enclave. Turnkey's API and database only ever see ciphertext:
+
+ ```typescript
+ const secretId = await turnkey.apiClient().importSecret({
+ plaintext: emsJwt,
+ name: "ems-trading-key",
+ staticProperties: {
+ kind: "exchangeApiKey",
+ permissions: "trade",
+ environment: "production",
+ },
+ });
+ ```
+
+ You can now delete any persisted plaintext copy from wherever it existed before.
+
+
+
+ The trading service authenticates with its own credentials and retrieves the key when it boots or opens a session. `exportSecret` generates the ephemeral keypair, submits the export activity, and decrypts the result into the authorized service's memory. Symmetric, bearer, HMAC, and OAuth credentials necessarily exist there transiently while in use:
+
+ ```typescript
+ const emsJwt = await turnkey.apiClient().exportSecret({
+ secretId,
+ });
+
+ // Use the JWT for REST or WebSocket calls, keep it in memory only
+ ```
+
+
+
+ For withdrawal-capable keys, add a consensus policy so no service can retrieve them alone:
+
+ ```json
+ {
+ "policyName": "Withdrawal-capable keys require a human approver",
+ "effect": "EFFECT_ALLOW",
+ "consensus": "approvers.any(u, u.id == '') && approvers.any(u, u.tags.contains('risk-admin'))",
+ "condition": "secret.static_properties['kind'] == 'exchangeApiKey' && secret.static_properties['permissions'] == 'withdraw' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'"
+ }
+ ```
+
+ The export stays in `ACTIVITY_STATUS_CONSENSUS_NEEDED` until a risk admin approves it from the dashboard or via [approve_activity](/api-reference/activities/approve-activity), and the released payload is still readable only by the trading service.
+
+
+
+ Rotate by importing the replacement key and deleting the old secret. Revoke a service by invalidating its session keys or removing its user. Policy changes take effect immediately, with no re-encryption or re-import of the stored keys.
+
+
+
+### Variant: trading on behalf of your users
+
+Some firms run trading strategies on behalf of other people rather than on their own exchange accounts. The same pattern supports a non-custodial setup: each end user gets their own [sub-organization](/features/sub-organizations) and imports their exchange API key into it directly, so the plaintext never passes through your infrastructure on the way in. A [delegated access](/features/policies/delegated-access/overview) user you control, scoped by policy to retrieving that key and nothing else, lets your trading strategy pull the credential at runtime while the end user retains control of their sub-organization.
+
+For a trust-minimized deployment, run the trading strategy itself inside [Turnkey Verifiable Cloud](/features/verifiable-cloud/overview). The strategy executes in a verifiable secure enclave, so end users can verify exactly what code receives their API key, and the plaintext exists only inside that enclave while the strategy runs, keeping the arrangement non-custodial end to end.
+
+## Next steps
+
+
+
+
+
+
+
diff --git a/solutions/key-management/mpc-keyshare-storage.mdx b/solutions/key-management/mpc-keyshare-storage.mdx
new file mode 100644
index 00000000..8da3427a
--- /dev/null
+++ b/solutions/key-management/mpc-keyshare-storage.mdx
@@ -0,0 +1,61 @@
+---
+title: "MPC keyshare storage"
+description: "Enclave-protected, policy-gated backup and recovery for MPC keyshare bundles, stored as opaque secrets."
+tag: "Beta"
+---
+
+import { FeatureCard } from '/snippets/feature-card.mdx'
+import { SolutionCard } from '/snippets/solution-card.mdx'
+import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx'
+
+
+
+Regulated custodians and institutions that sign with MPC systems distribute keyshares across parties, and typically rely on an independent backup or recovery provider so that losing one party's share does not mean losing funds. Turnkey Secrets can serve as that independent recovery store. An MPC party's keyshare bundle is imported as an opaque secret, held inside a [secure enclave](/security/secure-enclaves), and released only through a [policy-gated](/features/policies/overview) export encrypted to a designated recovery participant. You can use it instead of, or in addition to, another recovery store. This solution builds on [Secret storage](/features/secrets).
+
+## What Turnkey contributes
+
+| Need | How Turnkey solves it |
+| :--- | :--- |
+| Keyshare bundle never sits plaintext in your infrastructure | Enclave-protected opaque storage: the bundle is end-to-end encrypted into the enclave at import and only ever leaves re-encrypted to a designated recipient |
+| Recovery must not be unilateral | Policy-gated export with independently controllable approvals: require consensus from risk officers or recovery operators that you manage separately from the MPC system |
+| Only the recovery participant may read the released share | The export payload is encrypted to a single ephemeral public key generated by the designated recovery participant; approvers cannot read it |
+| Recovery events must be auditable | Every import, export, and approval is a signed, attributable activity, logged and queryable |
+
+## Classifying and gating keyshares
+
+Bind [static properties](/features/secrets#static-properties) at import time so policies target classes of keyshares instead of individual IDs:
+
+```typescript
+const secretId = await turnkey.apiClient().importSecret({
+ plaintext: keyshareBundle,
+ name: "treasury-signer-party-2-keyshare",
+ staticProperties: {
+ kind: "mpcKeyshare",
+ curve: "secp256k1",
+ environment: "production",
+ recoveryRole: "backupParty",
+ },
+});
+```
+
+Gate export on those properties. For example, require two recovery operators to approve before any production keyshare is released:
+
+```json
+{
+ "policyName": "Production MPC keyshares require two recovery operators",
+ "effect": "EFFECT_ALLOW",
+ "consensus": "approvers.filter(u, u.tags.contains('recovery-operator')).count() >= 2",
+ "condition": "secret.static_properties['kind'] == 'mpcKeyshare' && secret.static_properties['environment'] == 'production' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'"
+}
+```
+
+At recovery time, the designated recovery participant generates an ephemeral keypair and retrieves the bundle with `exportSecret`, or with the proposal helpers described in [Programmable credential access](/solutions/key-management/programmable-credential-access) when the export requires additional approvals. Only that participant can decrypt the released bundle, which then re-enters your MPC provider's own recovery procedure.
+
+## Next steps
+
+
+
+
+
+
+
diff --git a/solutions/key-management/overview.mdx b/solutions/key-management/overview.mdx
index f8ecba93..91876bec 100644
--- a/solutions/key-management/overview.mdx
+++ b/solutions/key-management/overview.mdx
@@ -68,6 +68,24 @@ Key management serves different needs depending on how your application uses cry
href="/solutions/key-management/enterprise-disaster-recovery"
description="Import and recover wallets with end-to-end encryption, quorum-controlled access, and a cryptographic audit trail for treasury recovery, provider migration, and failover."
/>
+
+
+
## Ready to build?
diff --git a/solutions/key-management/programmable-credential-access.mdx b/solutions/key-management/programmable-credential-access.mdx
new file mode 100644
index 00000000..2c32490b
--- /dev/null
+++ b/solutions/key-management/programmable-credential-access.mdx
@@ -0,0 +1,133 @@
+---
+title: "Programmable credential access"
+description: "A password manager built for machines: policy-gated, programmable access to secrets for humans, services, and AI agents."
+tag: "Beta"
+---
+
+import { FeatureCard } from '/snippets/feature-card.mdx'
+import { SolutionCard } from '/snippets/solution-card.mdx'
+import { SecretsBetaCallout } from '/snippets/secrets-beta-callout.mdx'
+
+
+
+When an AI agent needs a password, a card number, or an API key, teams face an unpleasant choice: hand the agent unfettered access to credentials, or put a human in the loop for every single request. Turnkey gives you a third option: a programmable access layer that evaluates every credential request against policies you control, inside a [secure enclave](/security/secure-enclaves) that never releases plaintext unless the policy allows it.
+
+Because policies and tags are fully dynamic, you can start restrictive and loosen as trust builds, without re-importing anything. Start with an agent that must ask for every secret: it requests access, a human approves that one request, and the export completes. When the same request keeps coming back, the human adds a policy that lets the agent export that class of secret on its own, and the prompts stop. Allow always and tightening back up are both just policy edits. This solution builds on [Secret storage](/features/secrets).
+
+## Access patterns
+
+One policy engine supports the full spectrum of trust models:
+
+| Pattern | How it works |
+| :--- | :--- |
+| **Unilateral agent** | A session key authenticates an ephemeral agent instance as a durable agent-role user whose policy permits direct export. |
+| **Agent → human** | An agent instance requests access; the export completes only after a human approves the pending activity. |
+| **Multi-agent consensus** | Instances of separate agent-role users must each sign the same export before the secret is released, and only the designated recipient can decrypt it. |
+
+## Key implementation decisions
+
+| Decision | What to consider | Learn more |
+| :--- | :--- | :--- |
+| **Secret classification** | Bind static properties at import time (`kind`, `environment`, `requiresConsensus`, ...) so policies target classes of secrets instead of individual IDs. | [Secret storage](/features/secrets) |
+| **Agent identity** | Model each agent type or role as a durable Turnkey user. A scoped, expiring session key authenticates each ephemeral instance of that user, making credential delegation easy to express in policy. | [Sessions](/features/authentication/sessions) |
+| **Consensus requirements** | Decide which secret classes need one agent role, several, or a specific combination of user tags (e.g. one `browser-agent` *and* one `payment-agent`). | [Policy Engine](/features/policies/overview) |
+| **Recipient control** | The export payload is encrypted to a single ephemeral public key. Decide which party generates that key. That party, and only that party, can read the secret. | [Enclave secure channels](/security/enclave-secure-channels) |
+| **Revocation** | Invalidate a session key to revoke one agent instance, or delete the agent-role user to revoke every instance of that role. | [Sessions](/features/authentication/sessions) |
+
+## Example: multi-agent consensus for payments
+
+Model a browser agent and a payment agent as durable Turnkey users, with session keys that authenticate their ephemeral instances. Policy requires both agent roles to approve before the enclave exports card details, and only the payment agent instance holds the decryption key. Neither instance can act alone, and the browser agent never sees the card.
+
+| Need | How Turnkey solves it |
+| :--- | :--- |
+| No single agent instance can exfiltrate the card | Consensus policy requires approvals from both durable agent-role users before the enclave re-encrypts the secret |
+| Approver ≠ recipient | The payload is encrypted to the payment agent instance's ephemeral key; the browser agent instance's approval releases a ciphertext it cannot read |
+| Agent instances act in parallel | Both instances sign and submit the byte-identical export request in any order. Turnkey matches them to the same activity |
+| Every access is attributable | Each export and approval is signed with a session key and logged under the durable agent-role user |
+
+### Policy: require two agent roles for credit card access
+
+```json
+{
+ "policyName": "Require two agent roles for credit card access",
+ "effect": "EFFECT_ALLOW",
+ "consensus": "approvers.any(u, u.tags.contains('browser-agent')) && approvers.any(u, u.tags.contains('payment-agent'))",
+ "condition": "secret.static_properties['requiresConsensus'] == 'true' && secret.static_properties['kind'] == 'creditCard' && activity.type == 'ACTIVITY_TYPE_EXPORT_SECRETS'"
+}
+```
+
+### Implementation steps
+
+
+
+ Import the card once, with static properties that the policy above targets. Any client with import permission can do this. Here, a backend service imports it:
+
+ ```typescript
+ const secretId = await turnkey.apiClient().importSecret({
+ plaintext: JSON.stringify({ number: "4242...", exp: "11/29", cvv: "123" }),
+ name: "corporateVisa",
+ staticProperties: {
+ kind: "creditCard",
+ requiresConsensus: "true",
+ },
+ });
+ ```
+
+
+
+ The payment agent instance, the intended recipient, generates an ephemeral keypair and builds the proposal. `createExportSecretsProposal` is a local call: it produces the canonical request body and its fingerprint, with no network round trip.
+
+ ```typescript
+ import { generateP256KeyPair } from "@turnkey/crypto";
+
+ const { publicKey, privateKey } = generateP256KeyPair();
+
+ const proposal = paymentAgent.createExportSecretsProposal({
+ secrets: [{ secretId }],
+ targetPublicKey: publicKey,
+ organizationId,
+ });
+ ```
+
+ The proposal is plain JSON and contains no key material. Share it with co-signing agent instances over any channel.
+
+
+
+ Each agent instance stamps the identical proposal body with the session key for its durable agent-role user and submits. Order doesn't matter: the first submission creates the activity, and every subsequent identical submission counts as an approval.
+
+ ```typescript
+ await Promise.all([
+ paymentAgent.submitExportSecrets(proposal),
+ browserAgent.submitExportSecrets(proposal),
+ ]);
+ ```
+
+ Until the consensus expression is satisfied, the activity reports `ACTIVITY_STATUS_CONSENSUS_NEEDED` and no secret leaves the enclave.
+
+
+
+ Once policy is satisfied, the enclave re-encrypts the card to the payment agent instance's ephemeral key. Only that instance can decrypt:
+
+ ```typescript
+ const [cardJson] = await paymentAgent.awaitExportedSecrets({
+ proposal,
+ embeddedPrivateKey: privateKey,
+ });
+
+ const card = JSON.parse(cardJson);
+ ```
+
+
+
+
+ For direct human approval instead of a second agent role, skip the co-signing step: the activity stays in `CONSENSUS_NEEDED` until the human approves it from the dashboard or via [approve_activity](/api-reference/activities/approve-activity).
+
+
+## Next steps
+
+