> ## Documentation Index
> Fetch the complete documentation index at: https://docs.groundtech.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Transaction Approvals

> How customer approvals work, when they are required, and what to verify before signing.

Portfolio wallet withdrawals can require cryptographic approval before execution. When that happens, Ground creates a signing activity and exposes it through the Ground Portal and the Turnkey approval API.

<Info>
  The customer signer must generate the Turnkey approval stamp outside Ground's servers. Ground receives the stamp and validates it against the pending activity before forwarding the vote to Turnkey.
</Info>

## When approvals happen

* **Withdrawals** (`POST /v2/wallets/{id}/withdrawals`): each payout leg transitions to `pending_customer_approval` when it's ready for signing.

At most one customer approval can be outstanding per `walletId + chain`. If you create multiple withdrawals rapidly, later payouts queue behind the earlier approval automatically.

## What does not require approval

Strategy updates (`PATCH /v2/wallets/{id}/strategy`) do not require this customer approval flow. They are API-authenticated requests; later rebalances may still be executed by Ground under the wallet's configured policy.

## Detecting a pending approval

When a payout leg needs approval, `payoutLegs[].status` and the external payout step `state` become `pending_customer_approval`.

Poll `GET /v2/wallets/:id/withdrawals/:withdrawalId` or subscribe to the `portfolio_wallet.withdrawal.status_changed` webhook to detect when a payout enters `pending_customer_approval`. Then call `GET /v2/turnkey/activities/pending` to fetch the pending approval details and activity fingerprint for the authenticated organization.

## Approving an activity

1. Capture the pending payout leg/step from the withdrawal response or webhook.
2. Fetch the matching activity from `GET /v2/turnkey/activities/pending`.
3. **Verify the transaction details** (see [best practice below](#verify-before-you-approve)).
4. Request the Turnkey approval or rejection payload from `POST /v2/turnkey/activity-approval-request`.
5. Stamp `stampPayload` with the customer signer in customer-controlled infrastructure.
6. Submit the stamp with `POST /v2/turnkey/activities/{activityId}/vote`.
7. Processing continues automatically once approved.

Do not send customer private keys to Ground or any service outside the customer's control. Approval integrations should use the customer signer to produce a custody-provider approval stamp; raw signing credentials should never leave customer-controlled infrastructure.

Example API sequence:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X GET "$BASE_URL/v2/turnkey/activities/pending" \
  -H "Authorization: Bearer $GROUND_API_KEY"

curl -X POST "$BASE_URL/v2/turnkey/activity-approval-request" \
  -H "Authorization: Bearer $GROUND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "activityId": "018f4b7a-1111-7000-8000-000000000001",
    "action": "approve"
  }'
```

The approval-request response includes a `turnkeyRequest` object and a `stampPayload` string. Stamp `stampPayload` locally with the customer signer, then submit the resulting Turnkey stamp:

## Stamping the approval payload

Ground does not stamp the Turnkey approval for you. Your integration must create `customerApprovalStamp` by passing the exact `stampPayload` string to a Turnkey-compatible stamper owned by the customer.

For a customer-held Turnkey API key, stamp the payload from your own backend or signing service:

```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { ApiKeyStamper } from "@turnkey/api-key-stamper";

const approvalRequest = await fetch(`${BASE_URL}/v2/turnkey/activity-approval-request`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${GROUND_API_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    activityId: "018f4b7a-1111-7000-8000-000000000001",
    action: "approve"
  })
}).then((res) => res.json());

const stamper = new ApiKeyStamper({
  apiPublicKey: process.env.TURNKEY_API_PUBLIC_KEY,
  apiPrivateKey: process.env.TURNKEY_API_PRIVATE_KEY
});

const customerApprovalStamp = await stamper.stamp(approvalRequest.stampPayload);

await fetch(`${BASE_URL}/v2/turnkey/activities/${approvalRequest.activityId}/vote`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${GROUND_API_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    action: "approve",
    turnkeyRequest: approvalRequest.turnkeyRequest,
    customerApprovalStamp
  })
});
```

The stamp returned by Turnkey has this shape:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "stampHeaderName": "X-Stamp",
  "stampHeaderValue": "..."
}
```

For a passkey signer, run the stamping step in the browser with Turnkey's WebAuthn stamper and submit the returned stamp to your backend before calling the vote endpoint:

```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { WebauthnStamper } from "@turnkey/webauthn-stamper";

const stamper = new WebauthnStamper({
  rpId: window.location.hostname,
  userVerification: "preferred"
});

const customerApprovalStamp = await stamper.stamp(approvalRequest.stampPayload);
```

Never send `TURNKEY_API_PRIVATE_KEY`, raw passkey material, or any other customer signing secret to Ground. Ground only needs the `customerApprovalStamp` object, the original `turnkeyRequest`, and the intended `action`.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST "$BASE_URL/v2/turnkey/activities/018f4b7a-1111-7000-8000-000000000001/vote" \
  -H "Authorization: Bearer $GROUND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "approve",
    "turnkeyRequest": {
      "type": "ACTIVITY_TYPE_APPROVE_ACTIVITY",
      "timestampMs": "1782302400000",
      "organizationId": "turnkey-suborg-id",
      "parameters": {
        "fingerprint": "activity-fingerprint"
      }
    },
    "customerApprovalStamp": {
      "stampHeaderName": "X-Stamp",
      "stampHeaderValue": "..."
    }
  }'
```

## Verify before you approve

<Warning>
  Never blindly approve a Turnkey activity. Always verify the transaction details match what you expect before signing.
</Warning>

When your system receives a `pending_customer_approval` signal, it should verify the transaction before approving. This is critical because an approval is an irreversible cryptographic signature -- once signed and broadcast, the transaction cannot be reversed.

**What to verify:**

1. **Destination address** -- confirm the pending activity's `destinationAddress` matches the address your system originally submitted in the withdrawal request. Reject if it doesn't match.
2. **Amount** -- confirm the displayed amount is reasonable for the withdrawal and yield source. Do not assume it must exactly match the originally requested amount for asynchronous sources.
3. **Chain and token** -- confirm `destinationChain` and `destinationToken` match your expectations.
4. **Withdrawal correlation** -- match the pending approval back to a withdrawal your system actually initiated. Reject orphaned or unexpected approval requests.

**Example verification flow:**

```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
async function verifyAndApprove(approvalActivity, originalWithdrawal, signedApproval) {
  // 1. Correlate: did we actually request this withdrawal?
  if (!originalWithdrawal) {
    throw new Error("No matching withdrawal found -- rejecting approval");
  }

  // 2. Verify destination address
  if (approvalActivity.destinationAddress.toLowerCase() !== originalWithdrawal.destinationAddress.toLowerCase()) {
    throw new Error("Destination address mismatch -- rejecting approval");
  }

  // 3. Verify chain
  if (approvalActivity.destinationChain !== originalWithdrawal.destinationChain) {
    throw new Error("Chain mismatch -- rejecting approval");
  }

  // 4. Request POST /v2/turnkey/activity-approval-request.
  // 5. Locally stamp response.stampPayload, then submit it to:
  // POST /v2/turnkey/activities/{activityId}/vote
  return true;
}
```

This pattern protects against compromised intermediaries, replay attacks, and bugs that could cause funds to be sent to unintended destinations. Treat every approval request as untrusted input until your system has independently verified it.

## Automation patterns

Two patterns work well for handling approvals:

1. **Webhook-driven** -- subscribe to `portfolio_wallet.withdrawal.status_changed`. When a payout leg or step enters `pending_customer_approval`, fetch `GET /v2/turnkey/activities/pending`, verify the pending approval details, request `POST /v2/turnkey/activity-approval-request`, stamp the returned `stampPayload` locally, and submit the vote.
2. **Polling-driven** -- poll `GET /v2/wallets/:id/withdrawals/:withdrawalId` and check `payoutLegs[].status` / `payoutLegs[].steps[].state`.

Recommended: use webhooks as the primary trigger, with polling as a backstop (webhooks are best-effort delivery).

See [Webhooks](/docs/portfolio-wallets/webhooks) for how to register and verify webhook deliveries.

## Status updates

Subscribe to these events to monitor progress:

* `portfolio_wallet.withdrawal.status_changed`
* `portfolio_wallet.withdrawal.payout.status_changed`

If approval-related execution fails, the withdrawal eventually moves to `failed` with a `failureReason`.
