How to execute pre-signed transactions through the Passkallet relayer, including user approval (2FA).

Execute Transactions

The Execute Flow

After preparing a transaction (e.g. an ERC-20 transfer), you need to:

  1. Have the user sign the transaction data with their passkey
  2. Submit the signed transaction to the execute endpoint
  3. The transaction is either executed on-chain immediately, or held for the end-user to approve in Passkallet (see below) and executed once they confirm

User Approval (2FA)

Every wallet you create through the BaaS API belongs to an end-user who has a Passkallet identity. Before their funds move, that user confirms the transaction in Passkallet — a transversal second factor, like an authenticator app. You submit the signed transaction as usual; Passkallet holds it, notifies the user, and executes it only after they approve with their fingerprint or face.

What this means for your integration: the execute endpoint may return status: "PENDING_USER_APPROVAL" instead of a transaction hash. When it does, the transaction is not yet on-chain — you poll its status (or listen for the webhook) until the user approves or it expires. The signature you submitted is what gets executed; nothing is re-signed.

Execute a Transaction

Endpoint: POST /baas/transactions/execute

Required scope: transaction:execute

bash
curl -X POST https://gateway.dev.passkallet.com/sepolia/api/v1/passkallet/baas/transactions/execute \
  -H "x-api-key: pk_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4" \
  -H "Content-Type: application/json" \
  -d '{
    "walletAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18",
    "toAddress": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
    "value": "0",
    "functionSignature": "transfer(address,uint256)",
    "params": [
      "0x9876543210fedcba9876543210fedcba98765432",
      "1000000000000000000"
    ],
    "signature": "0xabc123...",
    "expiration": "1717200000",
    "fee": "0",
    "feeCoinAddress": "0x0000000000000000000000000000000000000000",
    "feeWithdrawAddress": "0x0000000000000000000000000000000000000000"
  }'
FieldTypeRequiredDescription
walletAddressstringYesSmart wallet address (must belong to your org)
toAddressstringYesTarget contract address
valuestringYesValue in wei to send (use "0" for token operations)
functionSignaturestringNo*Solidity function signature (e.g. "transfer(address,uint256)"). Required unless you provide raw "data".
paramsstring[]No*Array of function parameters as strings. Used with "functionSignature".
datastringNo*Raw 0x-prefixed calldata. Takes precedence over functionSignature/params — use it for calls with array/tuple arguments the flat params cannot express.
signaturestringYesPasskey signature of the transaction
expirationstringYesUnix timestamp (seconds) when the transaction expires
feestringYesFee amount
feeCoinAddressstringYesFee token address (use zero address for no fee)
feeWithdrawAddressstringYesAddress where fees are sent

* Calldata: provide either functionSignature + params or raw data. If both are present, data wins. If neither is present the request fails with 400. The executeHash the passkey signed already binds the exact calldata bytes, so raw data grants no authority beyond what a matching functionSignature/params would.

Response — held for user approval (the usual case):

json
{
  "statusCode": 200,
  "content": {
    "success": true,
    "status": "PENDING_USER_APPROVAL",
    "transactionUuid": "8f14e45f-ceea-467d-9a3f-2b6e1a9c7d20"
  }
}

The transaction is now waiting for the end-user to approve it in Passkallet. Keep the transactionUuid and follow Handling a Pending Approval below. No transactionHash exists yet — it is not on-chain.

Response — executed immediately:

json
{
  "statusCode": 200,
  "content": {
    "success": true,
    "transactionHash": "0xdef789abc123..."
  }
}

A transactionHash with no status means the transaction went on-chain right away. This happens for organization-operated wallets that have no linked end-user to approve.

Handling a Pending Approval

When you receive PENDING_USER_APPROVAL, wait for the user to confirm. There are two ways to know when they do:

  • Webhook (recommended): Passkallet sends a transaction.approved event when the user confirms and a transaction.rejected event if they decline. See Webhooks → Events.
  • Polling: if you cannot receive webhooks, check the transaction status periodically using the transactionUuid.

Once the user approves, Passkallet executes the transaction you submitted and the transaction hash becomes available. If the user rejects it, or it expires before they act, it never goes on-chain and you should surface that to them.

Transaction Expiration

  • The expiration must be in the future (otherwise: 400 Transaction already expired)
  • The expiration must be within 24 hours (otherwise: 400 Expiration too far in the future (max 24h))