From a7ccb58f10c126c125c10f32703e33d49927d218 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 21 Aug 2026 19:37:13 -0300 Subject: [PATCH 1/5] feat: add Broadcasts.recipients method to list broadcast recipients --- examples/broadcasts.rb | 3 + lib/resend/broadcasts.rb | 19 ++++++ spec/broadcasts_spec.rb | 125 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) diff --git a/examples/broadcasts.rb b/examples/broadcasts.rb index 4161808..b2b2c13 100644 --- a/examples/broadcasts.rb +++ b/examples/broadcasts.rb @@ -74,6 +74,9 @@ puts "html: #{retrieved[:html]}" puts "text: #{retrieved[:text]}" +recipients = Resend::Broadcasts.recipients(broadcast[:id], { type: "delivered" }) +puts "recipients: #{recipients[:data]}" + if retrieved[:status] == 'draft' Resend::Broadcasts.remove(broadcast[:id]) puts "removed #{broadcast[:id]}" diff --git a/lib/resend/broadcasts.rb b/lib/resend/broadcasts.rb index 4459356..3959880 100644 --- a/lib/resend/broadcasts.rb +++ b/lib/resend/broadcasts.rb @@ -57,6 +57,25 @@ def get(broadcast_id = "") path = "broadcasts/#{broadcast_id}" Resend::Request.new(path, {}, "get").perform end + + # https://resend.com/docs/api-reference/broadcasts/list-broadcast-recipients + # @param broadcast_id [String] the broadcast id + # @param params [Hash] the parameters + # @option params [String] :type the recipient event type to filter by (required): + # sent, delivered, opened, clicked, bounced, complained, unsubscribed, suppressed + # @option params [String] :email filter recipients by email address (optional) + # @option params [String] :bounce_type filter bounced recipients by bounce type (optional, + # only valid when type is bounced): permanent, transient, undetermined + # @option params [Integer] :limit the maximum number of results to return (optional) + # @option params [String] :after the cursor for pagination (optional) + # @option params [String] :before the cursor for pagination (optional) + def recipients(broadcast_id = "", params = {}) + raise ArgumentError, "type is required" if params[:type].nil? + + base_path = "broadcasts/#{broadcast_id}/recipients" + path = Resend::PaginationHelper.build_paginated_path(base_path, params) + Resend::Request.new(path, {}, "get").perform + end end end end diff --git a/spec/broadcasts_spec.rb b/spec/broadcasts_spec.rb index d9e7a3c..629c0ce 100644 --- a/spec/broadcasts_spec.rb +++ b/spec/broadcasts_spec.rb @@ -168,6 +168,131 @@ end end + describe "recipients" do + it "should raise when type is missing" do + expect do + Resend::Broadcasts.recipients("559ac32e-9ef5-46fb-82a1-b76b840c0f7b", {}) + end.to raise_error(ArgumentError, "type is required") + end + + it "should list recipients for a basic event type" do + resp = { + "object": "list", + "has_more": false, + "data": [ + { + "id" => "b2Zmc2V0OjA", + "contact_id" => "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email" => "carter@example.com" + } + ] + } + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + recipients = Resend::Broadcasts.recipients( + "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + { type: "sent" } + ) + + expect(recipients[:object]).to eql("list") + expect(recipients[:has_more]).to eql(false) + expect(recipients[:data].length).to eql(1) + expect(recipients[:data][0]["id"]).to eql("b2Zmc2V0OjA") + expect(recipients[:data][0]["contact_id"]).to eql("e169aa45-1ecf-4183-9955-b1499d5701d3") + expect(recipients[:data][0]["email"]).to eql("carter@example.com") + end + + it "should list recipients with count for opened type" do + resp = { + "object": "list", + "has_more": false, + "data": [ + { + "id" => "b2Zmc2V0OjA", + "contact_id" => "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email" => "carter@example.com", + "count" => 3 + } + ] + } + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + recipients = Resend::Broadcasts.recipients( + "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + { type: "opened" } + ) + + expect(recipients[:data][0]["count"]).to eql(3) + end + + it "should list recipients with clicked_links for clicked type" do + resp = { + "object": "list", + "has_more": false, + "data": [ + { + "id" => "b2Zmc2V0OjA", + "contact_id" => "e169aa45-1ecf-4183-9955-b1499d5701d3", + "email" => "carter@example.com", + "count" => 2, + "clicked_links" => [ + { "url" => "https://resend.com/pricing", "clicks" => 2 } + ] + } + ] + } + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + recipients = Resend::Broadcasts.recipients( + "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + { type: "clicked" } + ) + + expect(recipients[:data][0]["count"]).to eql(2) + expect(recipients[:data][0]["clicked_links"]).to eql( + [{ "url" => "https://resend.com/pricing", "clicks" => 2 }] + ) + end + + it "should list recipients with bounce_type for bounced type" do + resp = { + "object": "list", + "has_more": false, + "data": [ + { + "id" => "b2Zmc2V0OjA", + "contact_id" => nil, + "email" => "carter@example.com", + "bounce_type" => "permanent" + } + ] + } + allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) + + recipients = Resend::Broadcasts.recipients( + "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + { type: "bounced", bounce_type: "permanent" } + ) + + expect(recipients[:data][0]["contact_id"]).to eql(nil) + expect(recipients[:data][0]["bounce_type"]).to eql("permanent") + end + + it "should raise when broadcast is not found" do + resp = { + "statusCode" => 404, + "name" => "not_found", + "message" => "Broadcast not found" + } + allow(resp).to receive(:body).and_return(resp) + allow(HTTParty).to receive(:send).and_return(resp) + + expect do + Resend::Broadcasts.recipients("missing-id", { type: "sent" }) + end.to raise_error(Resend::Error::NotFoundError, /Broadcast not found/) + end + end + describe "get broadcast" do it "should retrieve a broadcast" do From bcad54c481729005ba7d4b2a65517c06ec6cd474 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 21 Aug 2026 21:55:03 -0300 Subject: [PATCH 2/5] fix: verify request path in recipients specs, clarify example timing --- examples/broadcasts.rb | 2 ++ spec/broadcasts_spec.rb | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/examples/broadcasts.rb b/examples/broadcasts.rb index b2b2c13..f61efae 100644 --- a/examples/broadcasts.rb +++ b/examples/broadcasts.rb @@ -74,6 +74,8 @@ puts "html: #{retrieved[:html]}" puts "text: #{retrieved[:text]}" +# Note: the broadcast above was only scheduled ("in 1 min"), so this will be +# empty until it actually sends. Query recipients after delivery in real usage. recipients = Resend::Broadcasts.recipients(broadcast[:id], { type: "delivered" }) puts "recipients: #{recipients[:data]}" diff --git a/spec/broadcasts_spec.rb b/spec/broadcasts_spec.rb index 629c0ce..f5a2f7a 100644 --- a/spec/broadcasts_spec.rb +++ b/spec/broadcasts_spec.rb @@ -187,6 +187,11 @@ } ] } + expect(Resend::Request).to receive(:new).with( + "broadcasts/559ac32e-9ef5-46fb-82a1-b76b840c0f7b/recipients?type=sent", + {}, + "get" + ).and_call_original allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) recipients = Resend::Broadcasts.recipients( @@ -267,6 +272,11 @@ } ] } + expect(Resend::Request).to receive(:new).with( + "broadcasts/559ac32e-9ef5-46fb-82a1-b76b840c0f7b/recipients?type=bounced&bounce_type=permanent", + {}, + "get" + ).and_call_original allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp) recipients = Resend::Broadcasts.recipients( From 38a50f976ee96eb745eae848383db3b138699fd4 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 21 Aug 2026 22:58:01 -0300 Subject: [PATCH 3/5] fix: wait for scheduled send before querying recipients in example --- examples/broadcasts.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/broadcasts.rb b/examples/broadcasts.rb index f61efae..bbffadd 100644 --- a/examples/broadcasts.rb +++ b/examples/broadcasts.rb @@ -74,9 +74,12 @@ puts "html: #{retrieved[:html]}" puts "text: #{retrieved[:text]}" -# Note: the broadcast above was only scheduled ("in 1 min"), so this will be -# empty until it actually sends. Query recipients after delivery in real usage. -recipients = Resend::Broadcasts.recipients(broadcast[:id], { type: "delivered" }) +# The broadcast above was scheduled ("in 1 min"), so wait for it to actually +# send before querying recipients. "sent" is the earliest status guaranteed to +# exist once it fires; "delivered" depends on the receiving server's own +# timing and isn't guaranteed by any fixed wait. +sleep 65 +recipients = Resend::Broadcasts.recipients(broadcast[:id], { type: "sent" }) puts "recipients: #{recipients[:data]}" if retrieved[:status] == 'draft' From ebd46f878657d7a0bd3a217ff88f1396183c5a25 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 21 Aug 2026 22:59:13 -0300 Subject: [PATCH 4/5] fix: simplify comment on the sleep before querying recipients --- examples/broadcasts.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/broadcasts.rb b/examples/broadcasts.rb index bbffadd..0a979db 100644 --- a/examples/broadcasts.rb +++ b/examples/broadcasts.rb @@ -74,10 +74,7 @@ puts "html: #{retrieved[:html]}" puts "text: #{retrieved[:text]}" -# The broadcast above was scheduled ("in 1 min"), so wait for it to actually -# send before querying recipients. "sent" is the earliest status guaranteed to -# exist once it fires; "delivered" depends on the receiving server's own -# timing and isn't guaranteed by any fixed wait. +# wait the time for the broadcast to be sent sleep 65 recipients = Resend::Broadcasts.recipients(broadcast[:id], { type: "sent" }) puts "recipients: #{recipients[:data]}" From b6c8b67f4a138ad55f896f5f9a238d64cceb26d0 Mon Sep 17 00:00:00 2001 From: dielduarte Date: Fri, 21 Aug 2026 23:02:31 -0300 Subject: [PATCH 5/5] fix: remove recipients example (needs an arbitrary sleep to demo meaningfully) --- examples/broadcasts.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/examples/broadcasts.rb b/examples/broadcasts.rb index 0a979db..4161808 100644 --- a/examples/broadcasts.rb +++ b/examples/broadcasts.rb @@ -74,11 +74,6 @@ puts "html: #{retrieved[:html]}" puts "text: #{retrieved[:text]}" -# wait the time for the broadcast to be sent -sleep 65 -recipients = Resend::Broadcasts.recipients(broadcast[:id], { type: "sent" }) -puts "recipients: #{recipients[:data]}" - if retrieved[:status] == 'draft' Resend::Broadcasts.remove(broadcast[:id]) puts "removed #{broadcast[:id]}"