diff --git a/src/Resend/BroadcastClickedLink.cs b/src/Resend/BroadcastClickedLink.cs
new file mode 100644
index 0000000..a3c880f
--- /dev/null
+++ b/src/Resend/BroadcastClickedLink.cs
@@ -0,0 +1,34 @@
+using System.Text.Json.Serialization;
+
+namespace Resend;
+
+///
+/// A single clicked link in a broadcast clicked-links list response.
+///
+public class BroadcastClickedLink
+{
+ ///
+ /// An opaque cursor for this row, used only for pagination.
+ /// It does not identify any entity in Resend.
+ ///
+ [JsonPropertyName( "id" )]
+ public string Id { get; set; } = default!;
+
+ ///
+ /// The URL that was clicked.
+ ///
+ [JsonPropertyName( "url" )]
+ public string Url { get; set; } = default!;
+
+ ///
+ /// Total number of clicks on this URL.
+ ///
+ [JsonPropertyName( "clicks" )]
+ public int Clicks { get; set; }
+
+ ///
+ /// Number of unique clicks on this URL.
+ ///
+ [JsonPropertyName( "unique_clicks" )]
+ public int UniqueClicks { get; set; }
+}
diff --git a/src/Resend/IResend.cs b/src/Resend/IResend.cs
index 1d6523c..2492b14 100644
--- a/src/Resend/IResend.cs
+++ b/src/Resend/IResend.cs
@@ -853,6 +853,15 @@ public interface IResend
/// Response.
Task BroadcastDeleteAsync( Guid broadcastId, CancellationToken cancellationToken = default );
+ ///
+ /// Lists the links clicked in a broadcast.
+ ///
+ /// Broadcast identifier.
+ /// Pagination query.
+ /// Cancelation token.
+ /// Page of clicked links.
+ Task>> BroadcastClickedLinksAsync( Guid broadcastId, PaginatedQuery? query = null, CancellationToken cancellationToken = default );
+
#endregion
#region Receiving
diff --git a/src/Resend/ResendClient.cs b/src/Resend/ResendClient.cs
index 9273723..9fd4a9d 100644
--- a/src/Resend/ResendClient.cs
+++ b/src/Resend/ResendClient.cs
@@ -489,6 +489,34 @@ public ResendClient( IOptionsSnapshot options, HttpClient h
}
+ ///
+ public Task>> BroadcastClickedLinksAsync( Guid broadcastId, PaginatedQuery? query = null, CancellationToken cancellationToken = default )
+ {
+ var baseUrl = $"/broadcasts/{broadcastId}/clicked-links";
+ var url = baseUrl;
+
+ if ( query != null )
+ {
+ var qs = new Dictionary();
+
+ if ( query.Limit.HasValue == true )
+ qs.Add( "limit", query.Limit.Value.ToString() );
+
+ if ( query.Before != null )
+ qs.Add( "before", query.Before );
+
+ if ( query.After != null )
+ qs.Add( "after", query.After );
+
+ url = QueryHelpers.AddQueryString( baseUrl, qs );
+ }
+
+ var req = new HttpRequestMessage( HttpMethod.Get, url );
+
+ return Execute, PaginatedResult>( req, ( x ) => x, cancellationToken );
+ }
+
+
///
private async Task Execute( HttpRequestMessage req, CancellationToken cancellationToken )
{
diff --git a/tests/Resend.Tests/ResendClientTests.cs b/tests/Resend.Tests/ResendClientTests.cs
index 4f2010d..12f69cb 100644
--- a/tests/Resend.Tests/ResendClientTests.cs
+++ b/tests/Resend.Tests/ResendClientTests.cs
@@ -499,4 +499,39 @@ public async Task BroadcastDelete()
Assert.NotNull( resp );
}
+
+
+ ///
+ [Fact]
+ public async Task BroadcastClickedLinks()
+ {
+ var broadcastId = Guid.NewGuid();
+
+ var resp = await _resend.BroadcastClickedLinksAsync( broadcastId );
+
+ Assert.NotNull( resp );
+ Assert.True( resp.Success );
+ Assert.False( resp.Content.HasMore );
+ Assert.Equal( 2, resp.Content.Data.Count );
+ Assert.Equal( "https://resend.com/pricing", resp.Content.Data[ 0 ].Url );
+ Assert.Equal( 42, resp.Content.Data[ 0 ].Clicks );
+ Assert.Equal( 30, resp.Content.Data[ 0 ].UniqueClicks );
+ }
+
+
+ ///
+ [Fact]
+ public async Task BroadcastClickedLinks_WithQuery()
+ {
+ var broadcastId = Guid.NewGuid();
+
+ var resp = await _resend.BroadcastClickedLinksAsync( broadcastId, new PaginatedQuery()
+ {
+ Limit = 10,
+ After = "cursor-value",
+ } );
+
+ Assert.NotNull( resp );
+ Assert.True( resp.Success );
+ }
}
diff --git a/tools/Resend.ApiServer/Controllers/BroadcastController.cs b/tools/Resend.ApiServer/Controllers/BroadcastController.cs
index 9bf1feb..47a45c3 100644
--- a/tools/Resend.ApiServer/Controllers/BroadcastController.cs
+++ b/tools/Resend.ApiServer/Controllers/BroadcastController.cs
@@ -144,4 +144,40 @@ public ListOf BroadcastList()
Data = list,
};
}
+
+
+ ///
+ [HttpGet]
+ [Route( "broadcasts/{broadcastId}/clicked-links" )]
+ public PaginatedResult BroadcastClickedLinks(
+ [FromRoute] Guid broadcastId,
+ [FromQuery] string? limit = null,
+ [FromQuery] string? before = null,
+ [FromQuery] string? after = null
+ )
+ {
+ _logger.LogDebug( "BroadcastClickedLinks" );
+
+ return new PaginatedResult()
+ {
+ HasMore = false,
+ Data =
+ [
+ new BroadcastClickedLink()
+ {
+ Id = "b2Zmc2V0OjA",
+ Url = "https://resend.com/pricing",
+ Clicks = 42,
+ UniqueClicks = 30,
+ },
+ new BroadcastClickedLink()
+ {
+ Id = "b2Zmc2V0OjE",
+ Url = "https://resend.com/docs",
+ Clicks = 17,
+ UniqueClicks = 15,
+ },
+ ],
+ };
+ }
}
diff --git a/tools/Resend.Cli/Broadcast/BroadcastClickedLinksCommand.cs b/tools/Resend.Cli/Broadcast/BroadcastClickedLinksCommand.cs
new file mode 100644
index 0000000..dd050ed
--- /dev/null
+++ b/tools/Resend.Cli/Broadcast/BroadcastClickedLinksCommand.cs
@@ -0,0 +1,85 @@
+using McMaster.Extensions.CommandLineUtils;
+using Spectre.Console;
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json;
+
+namespace Resend.Cli.Broadcast;
+
+///
+[Command( "clicked-links", Description = "Lists the clicked links of a broadcast" )]
+public class BroadcastClickedLinksCommand
+{
+ private readonly IResend _resend;
+
+ ///
+ [Argument( 0, Description = "Broadcast identifier" )]
+ [Required]
+ public Guid? BroadcastId { get; set; }
+
+ ///
+ [Option( "-l|--limit", CommandOptionType.SingleValue, Description = "Number of links to return" )]
+ public int? Limit { get; set; }
+
+ ///
+ [Option( "-b|--before", CommandOptionType.SingleValue, Description = "Links before cursor" )]
+ public string? BeforeId { get; set; }
+
+ ///
+ [Option( "-a|--after", CommandOptionType.SingleValue, Description = "Links after cursor" )]
+ public string? AfterId { get; set; }
+
+ ///
+ [Option( "-j|--json", CommandOptionType.NoValue, Description = "Emit output as JSON array" )]
+ public bool InJson { get; set; }
+
+
+ ///
+ public BroadcastClickedLinksCommand( IResend resend )
+ {
+ _resend = resend;
+ }
+
+
+ ///
+ public async Task OnExecuteAsync()
+ {
+ var q = new PaginatedQuery()
+ {
+ Limit = this.Limit,
+ Before = this.BeforeId,
+ After = this.AfterId,
+ };
+
+ var res = await _resend.BroadcastClickedLinksAsync( this.BroadcastId!.Value, q );
+ var rows = res.Content.Data;
+
+ if ( this.InJson == true )
+ {
+ var jso = new JsonSerializerOptions() { WriteIndented = true };
+ var json = JsonSerializer.Serialize( rows, jso );
+
+ Console.WriteLine( json );
+ }
+ else
+ {
+ var table = new Table();
+ table.Border = TableBorder.SimpleHeavy;
+ table.AddColumn( "Url" );
+ table.AddColumn( "Clicks" );
+ table.AddColumn( "Unique clicks" );
+
+ foreach ( var c in rows )
+ {
+ table.AddRow(
+ new Markup( Markup.Escape( c.Url ) ),
+ new Markup( c.Clicks.ToString() ),
+ new Markup( c.UniqueClicks.ToString() )
+ );
+ }
+
+ AnsiConsole.Write( table );
+ }
+
+ return 0;
+ }
+}
diff --git a/tools/Resend.Cli/BroadcastCommand.cs b/tools/Resend.Cli/BroadcastCommand.cs
index 4ee2259..0c94715 100644
--- a/tools/Resend.Cli/BroadcastCommand.cs
+++ b/tools/Resend.Cli/BroadcastCommand.cs
@@ -6,6 +6,7 @@ namespace Resend.Cli;
[Command( "broadcast", Description = "Broadcast management" )]
[Subcommand( typeof( Broadcast.BroadcastAddCommand ))]
[Subcommand( typeof( Broadcast.BroadcastCancelCommand ) )]
+[Subcommand( typeof( Broadcast.BroadcastClickedLinksCommand ) )]
[Subcommand( typeof( Broadcast.BroadcastDeleteCommand ) )]
[Subcommand( typeof( Broadcast.BroadcastListCommand ) )]
[Subcommand( typeof( Broadcast.BroadcastRetrieveCommand ))]