Skip to content

Add Contacts and Contact Properties API#90

Merged
filipetoscano merged 3 commits intomainfrom
81-add-segmentcontacts-contacts-contact-properties-api
Nov 4, 2025
Merged

Add Contacts and Contact Properties API#90
filipetoscano merged 3 commits intomainfrom
81-add-segmentcontacts-contacts-contact-properties-api

Conversation

@filipetoscano
Copy link
Copy Markdown
Collaborator

@filipetoscano filipetoscano commented Nov 2, 2025

Summary by cubic

Added Contacts and Contact Properties APIs with pagination, segment membership, topic subscriptions, and property CRUD. The CLI now supports managing segments, topics, and properties, and contact listing is paginated.

  • New Features

    • Contacts: paginated list, add/remove segments, list/update topic subscriptions.
    • Contact Properties: create, retrieve, update, delete custom properties (string/number) with default values.
    • CLI: contact segment add/delete/list; contact topic list/set; contact-prop add/get/update/delete/list.
  • Migration

    • ContactListAsync now returns PaginatedResult and accepts PaginatedQuery; update callers to read Content.Data.
    • ApiServer ContactController returns PaginatedResult; CLI ContactList adjusted to use paginated data.

Written for commit 567cc7d. Summary will update automatically on new commits.

@filipetoscano filipetoscano linked an issue Nov 2, 2025 that may be closed by this pull request
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20 issues found across 26 files

Prompt for AI agents (all 20 issues)

Understand the root cause of the following 20 issues and fix them.


<file name="tools/Resend.Cli/Contact/Segment/ContactSegmentDeleteCommand.cs">

<violation number="1" location="tools/Resend.Cli/Contact/Segment/ContactSegmentDeleteCommand.cs:34">
Rule violated: **API Key Permission Check SDK Methods**

This new CLI command introduces the Resend SDK call `ContactRemoveFromSegmentAsync`, which is a new API operation that will fail in production if the existing API key lacks the necessary segment-removal permission. Please confirm and document that the production API key is authorized for this call to comply with our API key permission policy.</violation>
</file>

<file name="src/Resend/ResendClient.Contacts.cs">

<violation number="1" location="src/Resend/ResendClient.Contacts.cs:11">
Rule violated: **API Key Permission Check SDK Methods**

Per the API Key Permission Check SDK Methods guideline, adding this POST /audiences/{audienceId}/contacts call introduces new Resend API operations. Please confirm that the production API key used by this SDK has the necessary permissions/scopes for contact creation and the other new contact/segment/topic endpoints in this change so we avoid permission failures after deployment.</violation>

<violation number="2" location="src/Resend/ResendClient.Contacts.cs:37">
Interpolating email directly into the path allows reserved URI characters (e.g., %, ?) from valid addresses to break the request. Encode the email before building the URL.</violation>
</file>

<file name="src/Resend/IResend.cs">

<violation number="1" location="src/Resend/IResend.cs:558">
Rule violated: **API Key Permission Check SDK Methods**

These new Resend contact segment/topic/property SDK calls expand required provider capabilities. Please double-check with the Resend docs that your production API keys are granted the scopes needed for segment management, topic subscription updates, and custom property CRUD to avoid permission failures.</violation>
</file>

<file name="tools/Resend.Cli/Contact/Segment/ContactSegmentListCommand.cs">

<violation number="1" location="tools/Resend.Cli/Contact/Segment/ContactSegmentListCommand.cs:34">
Rule violated: **API Key Permission Check SDK Methods**

Calling the new Resend ContactListSegmentsAsync endpoint adds a third-party SDK operation whose required API key permissions need explicit confirmation. Please verify that the production keys include access for listing contact segments to avoid runtime failures.</violation>
</file>

<file name="src/Resend/ResendClient.ContactProps.cs">

<violation number="1" location="src/Resend/ResendClient.ContactProps.cs:10">
Rule violated: **API Key Permission Check SDK Methods**

These new SDK wrappers add list/create/retrieve/update/delete calls against Resend&#39;s `/contact-properties` endpoints. Per the API Key Permission Check SDK Methods guideline, please confirm the production Resend API keys already include permissions for managing contact properties so we do not ship an endpoint the keys cannot call.</violation>
</file>

<file name="tools/Resend.Cli/Contact/Segment/ContactSegmentAddCommand.cs">

<violation number="1" location="tools/Resend.Cli/Contact/Segment/ContactSegmentAddCommand.cs:34">
Rule violated: **API Key Permission Check SDK Methods**

The new ContactAddToSegmentAsync SDK call introduces a segment-modification operation; please ensure the production API keys are confirmed to have the required permissions so this command does not fail at runtime.</violation>
</file>

<file name="tools/Resend.Cli/ContactProp/ContactPropDeleteCommand.cs">

<violation number="1" location="tools/Resend.Cli/ContactProp/ContactPropDeleteCommand.cs:29">
Rule violated: **API Key Permission Check SDK Methods**

This command adds a new call to `ContactPropDeleteAsync` on the Resend SDK. Please confirm the production API key has the required permission to delete contact properties so this operation does not fail after deployment.</violation>
</file>

<file name="tools/Resend.Cli/ContactProp/ContactPropAddCommand.cs">

<violation number="1" location="tools/Resend.Cli/ContactProp/ContactPropAddCommand.cs:55">
Rule violated: **API Key Permission Check SDK Methods**

This command now invokes the Resend SDK’s `ContactPropCreateAsync` to create contact properties. Please confirm that the production API keys have the contact-property create permission so this new operation does not fail after deployment.</violation>
</file>

<file name="tools/Resend.Cli/Contact/Topic/ContactTopicListCommand.cs">

<violation number="1" location="tools/Resend.Cli/Contact/Topic/ContactTopicListCommand.cs:34">
Rule violated: **API Key Permission Check SDK Methods**

The new ContactListTopicsAsync call adds a Resend SDK operation; please confirm the production API keys have the necessary permissions to list contact topics so this command doesn’t fail after deployment.</violation>
</file>

<file name="tools/Resend.Cli/ContactProp/ContactPropRetrieveCommand.cs">

<violation number="1" location="tools/Resend.Cli/ContactProp/ContactPropRetrieveCommand.cs:35">
Rule violated: **API Key Permission Check SDK Methods**

This command introduces a new Resend SDK operation (`ContactPropRetrieveAsync`). Please ensure the production API keys are updated/verified to include contact property retrieval permissions before release to avoid runtime authorization failures.</violation>
</file>

<file name="tools/Resend.Cli/ContactProp/ContactPropListCommand.cs">

<violation number="1" location="tools/Resend.Cli/ContactProp/ContactPropListCommand.cs:28">
Rule violated: **API Key Permission Check SDK Methods**

`ContactPropListAsync` is a new Resend SDK call that will run under existing production API keys. Please confirm those keys include permission to list contact properties so the CLI doesn’t start failing with permission errors after deployment.</violation>

<violation number="2" location="tools/Resend.Cli/ContactProp/ContactPropListCommand.cs:54">
Escape the default value before passing it to Spectre.Console.Markup so user-provided fallback strings containing markup characters do not break rendering.</violation>
</file>

<file name="tools/Resend.Cli/ContactProp/ContactPropUpdateCommand.cs">

<violation number="1" location="tools/Resend.Cli/ContactProp/ContactPropUpdateCommand.cs:42">
`DefaultValue` is initialized to null, so running the command without `--default` clears the property&#39;s existing fallback value instead of leaving it unchanged.</violation>

<violation number="2" location="tools/Resend.Cli/ContactProp/ContactPropUpdateCommand.cs:57">
Rule violated: **API Key Permission Check SDK Methods**

Introducing this ContactPropUpdateAsync call adds a new Resend Contact Properties API operation; please confirm the production API keys are provisioned for contact property updates so the CLI doesn’t hit permission failures after deployment.</violation>
</file>

<file name="tools/Resend.Cli/Contact/Topic/ContactTopicSetCommand.cs">

<violation number="1" location="tools/Resend.Cli/Contact/Topic/ContactTopicSetCommand.cs:58">
Rule violated: **API Key Permission Check SDK Methods**

ContactTopicSetCommand now calls the Resend SDK’s ContactUpdateTopicsAsync to mutate contact topic subscriptions. Per the “API Key Permission Check SDK Methods” rule, we need confirmation that the production API keys are authorized to update topic subscriptions; otherwise this new command can fail once deployed. Please verify the keys include this capability.</violation>
</file>

<file name="tools/Resend.Cli/Program.cs">

<violation number="1" location="tools/Resend.Cli/Program.cs:13">
Rule violated: **API Key Permission Check SDK Methods**

Registering ContactPropCommand adds CLI flows that call the new Resend contact property APIs (e.g., ContactPropCreateAsync, ContactPropRetrieveAsync, ContactPropUpdateAsync). Please confirm the production API keys have the necessary permissions for these operations to avoid post-deploy failures.</violation>

<violation number="2" location="tools/Resend.Cli/Program.cs:13">
The mock API server (`Resend.ApiServer`) is missing implementations for the new Contact Properties, Segments, and Topics APIs. The client and CLI tools have been updated to call these endpoints, but they will fail because the server does not handle the corresponding routes (e.g., `/contact-properties`). This creates a significant contract violation between the client and its test server.</violation>
</file>

<file name="tools/Resend.ApiServer/Controllers/ContactController.cs">

<violation number="1" location="tools/Resend.ApiServer/Controllers/ContactController.cs:128">
There is a client-server contract mismatch in the pagination implementation for listing contacts. The client sends pagination parameters (`limit`, `before`, `after`), but the mock API server&#39;s `ContactList` endpoint does not accept them, rendering the pagination feature non-functional and untestable against the mock server.</violation>
</file>

<file name="src/Resend/ContactPropertyData.cs">

<violation number="1" location="src/Resend/ContactPropertyData.cs:9">
Update the XML summary to describe the property type to keep the documentation accurate.</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

/// <summary />
public async Task<int> OnExecuteAsync()
{
await _resend.ContactRemoveFromSegmentAsync( this.ContactId!.Value, this.SegmentId!.Value );
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: API Key Permission Check SDK Methods

This new CLI command introduces the Resend SDK call ContactRemoveFromSegmentAsync, which is a new API operation that will fail in production if the existing API key lacks the necessary segment-removal permission. Please confirm and document that the production API key is authorized for this call to comply with our API key permission policy.

Prompt for AI agents
Address the following comment on tools/Resend.Cli/Contact/Segment/ContactSegmentDeleteCommand.cs at line 34:

<comment>This new CLI command introduces the Resend SDK call `ContactRemoveFromSegmentAsync`, which is a new API operation that will fail in production if the existing API key lacks the necessary segment-removal permission. Please confirm and document that the production API key is authorized for this call to comply with our API key permission policy.</comment>

<file context>
@@ -0,0 +1,38 @@
+    /// &lt;summary /&gt;
+    public async Task&lt;int&gt; OnExecuteAsync()
+    {
+        await _resend.ContactRemoveFromSegmentAsync( this.ContactId!.Value, this.SegmentId!.Value );
+
+        return 0;
</file context>
Fix with Cubic

public partial class ResendClient
{
/// <inheritdoc/>
public Task<ResendResponse<Guid>> ContactAddAsync( Guid audienceId, ContactData data, CancellationToken cancellationToken = default )
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: API Key Permission Check SDK Methods

Per the API Key Permission Check SDK Methods guideline, adding this POST /audiences/{audienceId}/contacts call introduces new Resend API operations. Please confirm that the production API key used by this SDK has the necessary permissions/scopes for contact creation and the other new contact/segment/topic endpoints in this change so we avoid permission failures after deployment.

Prompt for AI agents
Address the following comment on src/Resend/ResendClient.Contacts.cs at line 11:

<comment>Per the API Key Permission Check SDK Methods guideline, adding this POST /audiences/{audienceId}/contacts call introduces new Resend API operations. Please confirm that the production API key used by this SDK has the necessary permissions/scopes for contact creation and the other new contact/segment/topic endpoints in this change so we avoid permission failures after deployment.</comment>

<file context>
@@ -0,0 +1,199 @@
+public partial class ResendClient
+{
+    /// &lt;inheritdoc/&gt;
+    public Task&lt;ResendResponse&lt;Guid&gt;&gt; ContactAddAsync( Guid audienceId, ContactData data, CancellationToken cancellationToken = default )
+    {
+        if ( data.Email == null )
</file context>
Fix with Cubic

/// <param name="query">Paginated query.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>List of segments.</returns>
Task<ResendResponse<PaginatedResult<Segment>>> ContactListSegmentsAsync( Guid contactId, PaginatedQuery? query = null, CancellationToken cancellationToken = default );
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: API Key Permission Check SDK Methods

These new Resend contact segment/topic/property SDK calls expand required provider capabilities. Please double-check with the Resend docs that your production API keys are granted the scopes needed for segment management, topic subscription updates, and custom property CRUD to avoid permission failures.

Prompt for AI agents
Address the following comment on src/Resend/IResend.cs at line 558:

<comment>These new Resend contact segment/topic/property SDK calls expand required provider capabilities. Please double-check with the Resend docs that your production API keys are granted the scopes needed for segment management, topic subscription updates, and custom property CRUD to avoid permission failures.</comment>

<file context>
@@ -531,19 +531,112 @@ public interface IResend
+    /// &lt;param name=&quot;query&quot;&gt;Paginated query.&lt;/param&gt;
+    /// &lt;param name=&quot;cancellationToken&quot;&gt;Cancellation token.&lt;/param&gt;
+    /// &lt;returns&gt;List of segments.&lt;/returns&gt;
+    Task&lt;ResendResponse&lt;PaginatedResult&lt;Segment&gt;&gt;&gt; ContactListSegmentsAsync( Guid contactId, PaginatedQuery? query = null, CancellationToken cancellationToken = default );
+
+    /// &lt;summary&gt;
</file context>
Fix with Cubic

/// <summary />
public async Task<int> OnExecuteAsync()
{
var res = await _resend.ContactListSegmentsAsync( this.ContactId!.Value );
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: API Key Permission Check SDK Methods

Calling the new Resend ContactListSegmentsAsync endpoint adds a third-party SDK operation whose required API key permissions need explicit confirmation. Please verify that the production keys include access for listing contact segments to avoid runtime failures.

Prompt for AI agents
Address the following comment on tools/Resend.Cli/Contact/Segment/ContactSegmentListCommand.cs at line 34:

<comment>Calling the new Resend ContactListSegmentsAsync endpoint adds a third-party SDK operation whose required API key permissions need explicit confirmation. Please verify that the production keys include access for listing contact segments to avoid runtime failures.</comment>

<file context>
@@ -0,0 +1,66 @@
+    /// &lt;summary /&gt;
+    public async Task&lt;int&gt; OnExecuteAsync()
+    {
+        var res = await _resend.ContactListSegmentsAsync( this.ContactId!.Value );
+        var rows = res.Content.Data;
+
</file context>
Fix with Cubic

public partial class ResendClient
{
/// <inheritdoc />
public Task<ResendResponse<PaginatedResult<ContactProperty>>> ContactPropListAsync( PaginatedQuery? query = null, CancellationToken cancellationToken = default )
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: API Key Permission Check SDK Methods

These new SDK wrappers add list/create/retrieve/update/delete calls against Resend's /contact-properties endpoints. Per the API Key Permission Check SDK Methods guideline, please confirm the production Resend API keys already include permissions for managing contact properties so we do not ship an endpoint the keys cannot call.

Prompt for AI agents
Address the following comment on src/Resend/ResendClient.ContactProps.cs at line 10:

<comment>These new SDK wrappers add list/create/retrieve/update/delete calls against Resend&#39;s `/contact-properties` endpoints. Per the API Key Permission Check SDK Methods guideline, please confirm the production Resend API keys already include permissions for managing contact properties so we do not ship an endpoint the keys cannot call.</comment>

<file context>
@@ -0,0 +1,74 @@
+public partial class ResendClient
+{
+    /// &lt;inheritdoc /&gt;
+    public Task&lt;ResendResponse&lt;PaginatedResult&lt;ContactProperty&gt;&gt;&gt; ContactPropListAsync( PaginatedQuery? query = null, CancellationToken cancellationToken = default )
+    {
+        var baseUrl = &quot;/contact-properties&quot;;
</file context>
Fix with Cubic

[HttpGet]
[Route( "audiences/{audienceId}/contacts" )]
public ListOf<Contact> ContactList( [FromRoute] Guid audienceId )
public PaginatedResult<Contact> ContactList( [FromRoute] Guid audienceId )
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a client-server contract mismatch in the pagination implementation for listing contacts. The client sends pagination parameters (limit, before, after), but the mock API server's ContactList endpoint does not accept them, rendering the pagination feature non-functional and untestable against the mock server.

Prompt for AI agents
Address the following comment on tools/Resend.ApiServer/Controllers/ContactController.cs at line 128:

<comment>There is a client-server contract mismatch in the pagination implementation for listing contacts. The client sends pagination parameters (`limit`, `before`, `after`), but the mock API server&#39;s `ContactList` endpoint does not accept them, rendering the pagination feature non-functional and untestable against the mock server.</comment>

<file context>
@@ -125,7 +125,7 @@ public ActionResult ContactDeleteByEmail( [FromRoute] Guid audienceId, string em
     [HttpGet]
     [Route( &quot;audiences/{audienceId}/contacts&quot; )]
-    public ListOf&lt;Contact&gt; ContactList( [FromRoute] Guid audienceId )
+    public PaginatedResult&lt;Contact&gt; ContactList( [FromRoute] Guid audienceId )
     {
         _logger.LogDebug( &quot;ContactList&quot; );
</file context>
Fix with Cubic

public class ContactPropertyData
{
/// <summary>
/// Property key.
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the XML summary to describe the property type to keep the documentation accurate.

Prompt for AI agents
Address the following comment on src/Resend/ContactPropertyData.cs at line 9:

<comment>Update the XML summary to describe the property type to keep the documentation accurate.</comment>

<file context>
@@ -0,0 +1,31 @@
+public class ContactPropertyData
+{
+    /// &lt;summary&gt;
+    /// Property key.
+    /// &lt;/summary&gt;
+    /// &lt;remarks&gt;
</file context>
Suggested change
/// Property key.
/// Property type.
Fix with Cubic

new Markup( c.Id.ToString() ),
new Markup( c.Key ),
new Markup( c.PropertyType.ToString() ),
new Markup( c.DefaultValue?.ToString() ?? "" ),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escape the default value before passing it to Spectre.Console.Markup so user-provided fallback strings containing markup characters do not break rendering.

Prompt for AI agents
Address the following comment on tools/Resend.Cli/ContactProp/ContactPropListCommand.cs at line 54:

<comment>Escape the default value before passing it to Spectre.Console.Markup so user-provided fallback strings containing markup characters do not break rendering.</comment>

<file context>
@@ -0,0 +1,64 @@
+                   new Markup( c.Id.ToString() ),
+                   new Markup( c.Key ),
+                   new Markup( c.PropertyType.ToString() ),
+                   new Markup( c.DefaultValue?.ToString() ?? &quot;&quot; ),
+                   new Markup( c.MomentCreated.ToString() )
+                );
</file context>
Suggested change
new Markup( c.DefaultValue?.ToString() ?? "" ),
new Markup( Markup.Escape( c.DefaultValue?.ToString() ?? string.Empty ) ),
Fix with Cubic

/*
*
*/
object? def = null;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DefaultValue is initialized to null, so running the command without --default clears the property's existing fallback value instead of leaving it unchanged.

Prompt for AI agents
Address the following comment on tools/Resend.Cli/ContactProp/ContactPropUpdateCommand.cs at line 42:

<comment>`DefaultValue` is initialized to null, so running the command without `--default` clears the property&#39;s existing fallback value instead of leaving it unchanged.</comment>

<file context>
@@ -0,0 +1,64 @@
+        /*
+         * 
+         */
+        object? def = null;
+
+        if ( this.Default != null )
</file context>
Suggested change
object? def = null;
object? def = prop.DefaultValue;
Fix with Cubic

/// <inheritdoc/>
public Task<ResendResponse<Contact>> ContactRetrieveByEmailAsync( Guid audienceId, string email, CancellationToken cancellationToken = default )
{
var path = $"/audiences/{audienceId}/contacts/{email}";
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interpolating email directly into the path allows reserved URI characters (e.g., %, ?) from valid addresses to break the request. Encode the email before building the URL.

Prompt for AI agents
Address the following comment on src/Resend/ResendClient.Contacts.cs at line 37:

<comment>Interpolating email directly into the path allows reserved URI characters (e.g., %, ?) from valid addresses to break the request. Encode the email before building the URL.</comment>

<file context>
@@ -0,0 +1,199 @@
+    /// &lt;inheritdoc/&gt;
+    public Task&lt;ResendResponse&lt;Contact&gt;&gt; ContactRetrieveByEmailAsync( Guid audienceId, string email, CancellationToken cancellationToken = default )
+    {
+        var path = $&quot;/audiences/{audienceId}/contacts/{email}&quot;;
+        var req = new HttpRequestMessage( HttpMethod.Get, path );
+
</file context>
Suggested change
var path = $"/audiences/{audienceId}/contacts/{email}";
var path = $"/audiences/{audienceId}/contacts/{Uri.EscapeDataString( email )}";
Fix with Cubic

@filipetoscano filipetoscano merged commit 76aa0b9 into main Nov 4, 2025
2 checks passed
@filipetoscano filipetoscano deleted the 81-add-segmentcontacts-contacts-contact-properties-api branch November 4, 2025 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Segment/Contacts, Contacts, Contact Properties API

2 participants