Server-only
This page covers your server webhook handler. The client triggers delegation; your server verifies, decrypts, and stores materials.
When a delegation is triggered, your endpoint receives a webhook named wallet.delegation.created. The delegated materials are in data.
{
"messageId" : "f44da9f0-a5b5-47f6-965f-f04af51c903e" ,
"eventId" : "2cf779a8-89da-486f-974e-2b77b738e4ac" ,
"eventName" : "wallet.delegation.created" ,
"timestamp" : "2025-10-01T15:13:26.348Z" ,
"webhookId" : "9a31fefc-64e4-4551-81da-1502eacc852d" ,
"userId" : "7eb7843b-2a4d-4f69-b95e-d219f0662fda" ,
"environmentId" : "53728749-1f19-4cab-becf-b88f952c3a3c" ,
"environmentName" : "sandbox" ,
"data" : {
"chain" : "EVM" ,
"encryptedDelegatedShare" : {
"alg" : "HYBRID-RSA-AES-256" ,
"iv" : "dzePdAUMQd6lWQngEXWPdQ" ,
"ct" : "pJIT5UU...XcWeYsXhygL2QbQcWZK6Rs5_CuiCDb_dHC_7P1tC..." ,
"tag" : "Yq8bpMU8huIx7UzUUUgI9Q" ,
"ek" : "uix2E6E...Keru7HWqeu7ktw"
},
"encryptedWalletApiKey" : {
"alg" : "HYBRID-RSA-AES-256" ,
"ct" : "PzeliI...0kB9C0" ,
"ek" : "iWJgZQ...rxt" ,
"iv" : "RpC5nw1b4udgJqnC1p0evQ" ,
"kid" : "dynamic_rsa_lSuvWlCy" ,
"tag" : "-ZtmOG6gYTzS53wVMNK0Ig"
},
"publicKey" : "0xd74ff800a3c6f66ecd217118aaa6fb1c916fa4e2" ,
"userId" : "7eb7843b-2a4d-4f69-b95e-d219f0662fda" ,
"walletId" : "25193936-3ecd-4c1b-84e6-9eabc82e53c2"
}
}
Verify → Decrypt → Store
Verify the webhook signature. See Validate webhook signatures .
Decrypt data.encryptedDelegatedShare and data.encryptedWalletApiKey.
Store userId, walletId, and decrypted materials securely (e.g., envelope encryption, KMS, at-rest encryption).
Encryption fields
alg: hybrid (RSA‑OAEP + AES‑256‑GCM); iv: AES IV; ct: ciphertext; tag: GCM tag; ek: encrypted content‑encryption key; kid: key identifier for rotation.
Example: Node (using Dynamic SDK)
If your server is Node.js, the easiest approach is to use our SDK helper which handles all the decryption logic for you:
npm install @dynamic-labs-wallet/node
import { decryptDelegatedWebhookData } from '@dynamic-labs-wallet/node' ;
// In your webhook handler, after verifying the signature
const webhookData = req . body ; // The webhook payload
const { decryptedDelegatedShare , decryptedWalletApiKey } =
decryptDelegatedWebhookData ({
privateKeyPem: process . env . YOUR_PRIVATE_KEY , // Your RSA private key
encryptedDelegatedKeyShare: webhookData . data . encryptedDelegatedShare ,
encryptedWalletApiKey: webhookData . data . encryptedWalletApiKey ,
});
// Now securely store these decrypted materials
// decryptedDelegatedShare: ServerKeyShare object
// decryptedWalletApiKey: string
Manual Decryption (any language)
If you don’t use Node.js, you can decrypt the materials yourself in any language that supports standard crypto primitives. The encryption scheme is hybrid RSA-OAEP + AES-256-GCM. Here’s a reference implementation in TypeScript:
import crypto from 'node:crypto' ;
type EncryptedPayload = {
alg : string ; iv : string ; ct : string ; tag : string ; ek : string ; kid ?: string ;
};
function decryptAesGcm ( encryptedKey : Buffer , ivB64 : string , ctB64 : string , tagB64 : string ) {
const iv = Buffer . from ( ivB64 , 'base64url' );
const ciphertext = Buffer . from ( ctB64 , 'base64url' );
const tag = Buffer . from ( tagB64 , 'base64url' );
const decipher = crypto . createDecipheriv ( 'aes-256-gcm' , encryptedKey , iv );
decipher . setAuthTag ( tag );
const plaintext = Buffer . concat ([ decipher . update ( ciphertext ), decipher . final ()]);
return plaintext ;
}
function rsaOaepDecryptEk ( privateKeyPem : string , ekB64 : string ) {
return crypto . privateDecrypt (
{ key: privateKeyPem , oaepHash: 'sha256' , padding: crypto . constants . RSA_PKCS1_OAEP_PADDING },
Buffer . from ( ekB64 , 'base64url' )
);
}
export function decryptMaterials (
privateKeyPem : string ,
share : EncryptedPayload ,
apiKeyEnc : EncryptedPayload
) {
const shareKey = rsaOaepDecryptEk ( privateKeyPem , share . ek );
const walletApiKeyKey = rsaOaepDecryptEk ( privateKeyPem , apiKeyEnc . ek );
const delegatedShare = decryptAesGcm ( shareKey , share . iv , share . ct , share . tag );
const walletApiKey = decryptAesGcm ( walletApiKeyKey , apiKeyEnc . iv , apiKeyEnc . ct , apiKeyEnc . tag );
return {
delegatedShare: JSON . parse ( delegatedShare . toString ( 'utf8' )),
walletApiKey: walletApiKey . toString ( 'utf8' ),
};
}
Steps to replicate in your language of choice:
Base64url-decode the ek field
RSA-OAEP decrypt (SHA-256) the decoded ek using your private key — this gives you the AES content-encryption key
Base64url-decode iv, ct, and tag
AES-256-GCM decrypt using the content-encryption key, IV, ciphertext, and auth tag
Repeat for both encryptedDelegatedShare and encryptedWalletApiKey
If a delivery fails, you can replay it from the dashboard. Use the eventId as an idempotency key.
Best Practices for Secure Storage
After decrypting the delegated materials, proper storage is critical. The delegatedShare and walletApiKey, in combination with your Dynamic developer API key, provide full signing authority and must be protected with defense-in-depth strategies.
Recommended Storage Approaches
1. Envelope Encryption with Cloud KMS (Recommended)
Use a cloud Key Management Service to encrypt the decrypted materials before storing them in your database. AWS KMS Example: import { KMSClient , EncryptCommand , DecryptCommand } from '@aws-sdk/client-kms' ;
const kmsClient = new KMSClient ({ region: 'us-east-1' });
async function encryptWithKMS ( plaintext : string , keyId : string ) {
const command = new EncryptCommand ({
KeyId: keyId ,
Plaintext: Buffer . from ( plaintext ),
});
const response = await kmsClient . send ( command );
return response . CiphertextBlob ; // Store this in your database
}
async function decryptWithKMS ( ciphertext : Uint8Array ) {
const command = new DecryptCommand ({
CiphertextBlob: ciphertext ,
});
const response = await kmsClient . send ( command );
return Buffer . from ( response . Plaintext ). toString ( 'utf8' );
}
Benefits:
Centralized key management with automatic rotation
Hardware-backed security (FIPS 140-2 Level 3)
Audit logging of all encryption/decryption operations
Fine-grained IAM policies
2. Google Cloud KMS & Secret Manager
Similar to AWS KMS, but integrated with Google Cloud’s ecosystem. import { SecretManagerServiceClient } from '@google-cloud/secret-manager' ;
import { KeyManagementServiceClient } from '@google-cloud/kms' ;
const secretClient = new SecretManagerServiceClient ();
const kmsClient = new KeyManagementServiceClient ();
async function storeSecret ( projectId : string , secretId : string , payload : string ) {
const [ version ] = await secretClient . addSecretVersion ({
parent: `projects/ ${ projectId } /secrets/ ${ secretId } ` ,
payload: {
data: Buffer . from ( payload , 'utf8' ),
},
});
return version . name ;
}
Microsoft Azure’s managed secrets and key management service. import { SecretClient } from '@azure/keyvault-secrets' ;
import { DefaultAzureCredential } from '@azure/identity' ;
const credential = new DefaultAzureCredential ();
const vaultUrl = `https:// ${ vaultName } .vault.azure.net` ;
const client = new SecretClient ( vaultUrl , credential );
async function storeSecret ( name : string , value : string ) {
await client . setSecret ( name , value );
}
Security Requirements Checklist
Regardless of your storage method, follow these requirements:
Never log plaintext materials — redact delegatedShare and walletApiKey from all logs, error messages, and monitoring
Encrypt at rest — use AES-256-GCM or equivalent; ensure database/storage has encryption enabled
Encrypt in transit — all communication must use TLS 1.3
Implement access controls — restrict which services and roles can decrypt materials
Enable audit logging — track all access to encrypted materials with timestamps and actor identity
Separate encryption keys — don’t reuse keys across environments (dev/staging/prod)
Use unique encryption per record — generate new IVs for each encryption operation
Implement key rotation — rotate encryption keys periodically (e.g., every 90 days)
Plan for key compromise — document incident response for key material exposure
Secure deletion — overwrite secrets in memory after use; use secure deletion for storage
Storage Schema Example
CREATE TABLE delegated_shares (
id UUID PRIMARY KEY ,
user_id VARCHAR ( 255 ) NOT NULL ,
wallet_id VARCHAR ( 255 ) NOT NULL ,
chain VARCHAR ( 10 ) NOT NULL ,
-- Encrypted with KMS/Vault
encrypted_delegated_share BYTEA NOT NULL ,
encrypted_wallet_api_key BYTEA NOT NULL ,
-- Metadata for key management
encryption_key_id VARCHAR ( 255 ) NOT NULL ,
encryption_algorithm VARCHAR ( 50 ) DEFAULT 'AES-256-GCM' ,
-- Audit fields
created_at TIMESTAMP NOT NULL DEFAULT NOW (),
last_accessed_at TIMESTAMP ,
access_count INTEGER DEFAULT 0 ,
-- Indexes
INDEX idx_user_wallet (user_id, wallet_id),
INDEX idx_created_at (created_at)
);
What NOT to Do
Never store plaintext shares in databases, files, or environment variables
Never commit encryption keys or shares to version control
Never use the same encryption key across all users
Never skip signature verification before storing materials
Never rely solely on database encryption without application-level encryption
Never expose decrypted materials through APIs or logs
What's next? Learn how to use the delegated materials in Developer Actions.