Skip to main content
When a user approves wallet delegation in the Dynamic SDK, Dynamic sends their encrypted signing credentials to your server via webhook. This guide walks through the full setup.

How it works

  1. User taps “Grant wallet access” in your app — calls initDelegationProcess() in the Dynamic SDK.
  2. Dynamic encrypts the user’s signing credentials with your server’s public key and POSTs them to your webhook endpoint.
  3. Your server decrypts and stores the credentials.
  4. On each agent request, your server fetches those credentials and signs on behalf of the user.
  5. If the user revokes, Dynamic fires another webhook and you delete the credentials.

Step 1: Generate a key pair

Your server needs an RSA key pair. Dynamic encrypts the credentials with your public key; your server decrypts them with the private key.
openssl genrsa -out delegation_private.pem 4096
openssl rsa -in delegation_private.pem -pubout -out delegation_public.pem
Never commit the private key. Add it to .env, escaping newlines as \n:
DYNAMIC_DELEGATION_PRIVATE_KEY="<your RSA private key PEM with newlines escaped as \\n>"

Step 2: Register the webhook in the Dynamic dashboard

  1. Go to app.dynamic.xyz → your environment → Integrations > Webhooks.
  2. Click Add endpoint and set the URL to your server’s webhook route.
  3. Paste the contents of delegation_public.pem into the Public key field.
  4. Enable wallet.delegation.created and wallet.delegation.revoked.
  5. Save and copy the webhook secret.
DYNAMIC_WEBHOOK_SECRET="whsec_..."
For local development, use ngrok (ngrok http 3001) to expose your server and register the ngrok URL as the webhook endpoint.

Step 3: Verify the webhook signature

Every request from Dynamic includes an x-dynamic-signature-256 header. Verify it before processing:
const payloadSignature = crypto
  .createHmac("sha256", webhookSecret)
  .update(JSON.stringify(rawPayload))
  .digest("hex");

if (!crypto.timingSafeEqual(
  Buffer.from(`sha256=${payloadSignature}`),
  Buffer.from(signature)
)) {
  return c.json({ error: "Invalid signature" }, 401);
}

Step 4: Decrypt and store

Dynamic encrypts the credentials with your public key before sending them. Decrypt and store per user:
case "wallet.delegation.created": {
  const {
    walletId, chain, publicKey, userId,
    encryptedDelegatedShare, encryptedWalletApiKey
  } = event.data;

  const { delegatedShare, walletApiKey } = decryptMaterials(
    encryptedDelegatedShare,
    encryptedWalletApiKey
  );

  await storeDelegation({
    userId, walletId,
    address: publicKey,
    chain, walletApiKey,
    keyShare: delegatedShare
  });
}
Store credentials keyed by userId (the Dynamic user ID). Use ON CONFLICT (user_id, chain) DO UPDATE so re-delegation updates rather than duplicates.

Step 5: Handle revocation

When the user revokes in the Dynamic SDK, Dynamic fires wallet.delegation.revoked:
case "wallet.delegation.revoked": {
  await deleteDelegation(event.data.userId);
}
After deletion, return 403 to the user until they re-delegate.

Step 6: Use the credentials

Before each agent action, fetch the credentials for the requesting user:
const record = await getDelegation(user.sub); // user.sub = Dynamic user ID from JWT
if (!record) {
  return c.json({ error: "No delegation found. Please grant wallet access first." }, 403);
}

// Pass to your signing/agent logic
const creds = {
  walletId: record.walletId,
  walletAddress: record.address,
  walletApiKey: record.walletApiKey,
  keyShare: record.keyShare,
};

Webhook events

EventTriggerAction
pingWebhook registration or testNo side effects
wallet.delegation.createdUser approves delegationDecrypt + store credentials
wallet.delegation.revokedUser revokes delegationDelete credentials

Environment variables

VariableWhere to get it
DYNAMIC_WEBHOOK_SECRETDynamic dashboard → Integrations → Webhooks
DYNAMIC_DELEGATION_PRIVATE_KEYYour generated RSA private key
DYNAMIC_ENVIRONMENT_IDDynamic dashboard → Overview
DYNAMIC_API_KEYDynamic dashboard → Overview