diff --git a/CHANGELOG.md b/CHANGELOG.md
index cedc180..9af97c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this library adheres to Rust's notion of
 
 ## [Unreleased]
 
+## [0.4.1] - 2024-12-06
+### Added
+- `zcash_note_encryption::try_output_recovery_with_pkd_esk`
+
 ## [0.4.0] - 2023-06-06
 ### Changed
 - The `esk` and `ephemeral_key` arguments have been removed from 
diff --git a/Cargo.lock b/Cargo.lock
index c04cb40..0d41754 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -150,7 +150,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
 [[package]]
 name = "zcash_note_encryption"
-version = "0.4.0"
+version = "0.4.1"
 dependencies = [
  "chacha20",
  "chacha20poly1305",
diff --git a/Cargo.toml b/Cargo.toml
index 34d359e..fccff47 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "zcash_note_encryption"
 description = "Note encryption for Zcash transactions"
-version = "0.4.0"
+version = "0.4.1"
 authors = [
     "Jack Grigg <jack@electriccoin.co>",
     "Kris Nuttycombe <kris@electriccoin.co>"
diff --git a/src/batch.rs b/src/batch.rs
index ad70416..59577b5 100644
--- a/src/batch.rs
+++ b/src/batch.rs
@@ -75,12 +75,11 @@ where
             key_chunk
                 .iter()
                 .zip(ivks.iter().enumerate())
-                .filter_map(|(key, (i, ivk))| {
+                .find_map(|(key, (i, ivk))| {
                     key.as_ref()
                         .and_then(|key| decrypt_inner(domain, ivk, ephemeral_key, output, key))
                         .map(|out| (out, i))
                 })
-                .next()
         })
         .collect::<Vec<Option<_>>>()
 }
diff --git a/src/lib.rs b/src/lib.rs
index 16c089b..d5b1274 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -636,8 +636,6 @@ pub fn try_output_recovery_with_ock<D: Domain, Output: ShieldedOutput<D, ENC_CIP
     output: &Output,
     out_ciphertext: &[u8; OUT_CIPHERTEXT_SIZE],
 ) -> Option<(D::Note, D::Recipient, D::Memo)> {
-    let enc_ciphertext = output.enc_ciphertext();
-
     let mut op = OutPlaintextBytes([0; OUT_PLAINTEXT_SIZE]);
     op.0.copy_from_slice(&out_ciphertext[..OUT_PLAINTEXT_SIZE]);
 
@@ -653,6 +651,27 @@ pub fn try_output_recovery_with_ock<D: Domain, Output: ShieldedOutput<D, ENC_CIP
     let pk_d = D::extract_pk_d(&op)?;
     let esk = D::extract_esk(&op)?;
 
+    try_output_recovery_with_pkd_esk(domain, pk_d, esk, output)
+}
+
+/// Recovery of the full note plaintext by the sender.
+///
+/// Attempts to decrypt and validate the given shielded output using the given `pk_d` and `esk`. If
+/// successful, the corresponding note and memo are returned, along with the address to which the
+/// note was sent.
+///
+/// Implements part of section 4.19.3 of the
+/// [Zcash Protocol Specification](https://zips.z.cash/protocol/nu5.pdf#decryptovk).
+/// For decryption using a Full Viewing Key see [`try_output_recovery_with_ovk`].
+pub fn try_output_recovery_with_pkd_esk<
+    D: Domain,
+    Output: ShieldedOutput<D, ENC_CIPHERTEXT_SIZE>,
+>(
+    domain: &D,
+    pk_d: D::DiversifiedTransmissionKey,
+    esk: D::EphemeralSecretKey,
+    output: &Output,
+) -> Option<(D::Note, D::Recipient, D::Memo)> {
     let ephemeral_key = output.ephemeral_key();
     let shared_secret = D::ka_agree_enc(&esk, &pk_d);
     // The small-order point check at the point of output parsing rejects
@@ -660,6 +679,7 @@ pub fn try_output_recovery_with_ock<D: Domain, Output: ShieldedOutput<D, ENC_CIP
     // be okay.
     let key = D::kdf(shared_secret, &ephemeral_key);
 
+    let enc_ciphertext = output.enc_ciphertext();
     let mut plaintext = NotePlaintextBytes([0; NOTE_PLAINTEXT_SIZE]);
     plaintext
         .0